@neta-art/neta-search-cli 0.2.43 → 0.2.45

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.
Files changed (3) hide show
  1. package/README.md +7 -1
  2. package/neta-search.mjs +791 -103
  3. package/package.json +1 -1
package/neta-search.mjs CHANGED
@@ -2660,7 +2660,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2660
2660
  const negativeNumberArg = (arg) => {
2661
2661
  if (!/^-(\d+|\d*\.\d+)(e[+-]?\d+)?$/.test(arg)) return false;
2662
2662
  return !this._getCommandAndAncestors().some(
2663
- (cmd) => cmd.options.map((opt) => opt.short).some((short2) => /^-\d$/.test(short2))
2663
+ (cmd) => cmd.options.map((opt) => opt.short).some((short3) => /^-\d$/.test(short3))
2664
2664
  );
2665
2665
  };
2666
2666
  let activeVariadicOption = null;
@@ -7021,8 +7021,8 @@ var require_util2 = __commonJS({
7021
7021
  function createDeferredPromise() {
7022
7022
  let res;
7023
7023
  let rej;
7024
- const promise = new Promise((resolve2, reject) => {
7025
- res = resolve2;
7024
+ const promise = new Promise((resolve3, reject) => {
7025
+ res = resolve3;
7026
7026
  rej = reject;
7027
7027
  });
7028
7028
  return { promise, resolve: res, reject: rej };
@@ -8526,8 +8526,8 @@ Content-Type: ${value.type || "application/octet-stream"}\r
8526
8526
  });
8527
8527
  }
8528
8528
  });
8529
- const busboyResolve = new Promise((resolve2, reject) => {
8530
- busboy.on("finish", resolve2);
8529
+ const busboyResolve = new Promise((resolve3, reject) => {
8530
+ busboy.on("finish", resolve3);
8531
8531
  busboy.on("error", (err) => reject(new TypeError(err)));
8532
8532
  });
8533
8533
  if (this.body !== null) for await (const chunk of consumeBody(this[kState].body)) busboy.write(chunk);
@@ -9061,9 +9061,9 @@ var require_dispatcher_base = __commonJS({
9061
9061
  }
9062
9062
  close(callback) {
9063
9063
  if (callback === void 0) {
9064
- return new Promise((resolve2, reject) => {
9064
+ return new Promise((resolve3, reject) => {
9065
9065
  this.close((err, data) => {
9066
- return err ? reject(err) : resolve2(data);
9066
+ return err ? reject(err) : resolve3(data);
9067
9067
  });
9068
9068
  });
9069
9069
  }
@@ -9101,12 +9101,12 @@ var require_dispatcher_base = __commonJS({
9101
9101
  err = null;
9102
9102
  }
9103
9103
  if (callback === void 0) {
9104
- return new Promise((resolve2, reject) => {
9104
+ return new Promise((resolve3, reject) => {
9105
9105
  this.destroy(err, (err2, data) => {
9106
9106
  return err2 ? (
9107
9107
  /* istanbul ignore next: should never error */
9108
9108
  reject(err2)
9109
- ) : resolve2(data);
9109
+ ) : resolve3(data);
9110
9110
  });
9111
9111
  });
9112
9112
  }
@@ -10166,16 +10166,16 @@ var require_client = __commonJS({
10166
10166
  return this[kNeedDrain] < 2;
10167
10167
  }
10168
10168
  async [kClose]() {
10169
- return new Promise((resolve2) => {
10169
+ return new Promise((resolve3) => {
10170
10170
  if (!this[kSize]) {
10171
- resolve2(null);
10171
+ resolve3(null);
10172
10172
  } else {
10173
- this[kClosedResolve] = resolve2;
10173
+ this[kClosedResolve] = resolve3;
10174
10174
  }
10175
10175
  });
10176
10176
  }
10177
10177
  async [kDestroy](err) {
10178
- return new Promise((resolve2) => {
10178
+ return new Promise((resolve3) => {
10179
10179
  const requests = this[kQueue].splice(this[kPendingIdx]);
10180
10180
  for (let i = 0; i < requests.length; i++) {
10181
10181
  const request3 = requests[i];
@@ -10186,7 +10186,7 @@ var require_client = __commonJS({
10186
10186
  this[kClosedResolve]();
10187
10187
  this[kClosedResolve] = null;
10188
10188
  }
10189
- resolve2();
10189
+ resolve3();
10190
10190
  };
10191
10191
  if (this[kHTTP2Session] != null) {
10192
10192
  util.destroy(this[kHTTP2Session], err);
@@ -10766,7 +10766,7 @@ var require_client = __commonJS({
10766
10766
  });
10767
10767
  }
10768
10768
  try {
10769
- const socket = await new Promise((resolve2, reject) => {
10769
+ const socket = await new Promise((resolve3, reject) => {
10770
10770
  client[kConnector]({
10771
10771
  host,
10772
10772
  hostname,
@@ -10778,7 +10778,7 @@ var require_client = __commonJS({
10778
10778
  if (err) {
10779
10779
  reject(err);
10780
10780
  } else {
10781
- resolve2(socket2);
10781
+ resolve3(socket2);
10782
10782
  }
10783
10783
  });
10784
10784
  });
@@ -11402,12 +11402,12 @@ upgrade: ${upgrade}\r
11402
11402
  cb();
11403
11403
  }
11404
11404
  }
11405
- const waitForDrain = () => new Promise((resolve2, reject) => {
11405
+ const waitForDrain = () => new Promise((resolve3, reject) => {
11406
11406
  assert2(callback === null);
11407
11407
  if (socket[kError]) {
11408
11408
  reject(socket[kError]);
11409
11409
  } else {
11410
- callback = resolve2;
11410
+ callback = resolve3;
11411
11411
  }
11412
11412
  });
11413
11413
  if (client[kHTTPConnVersion] === "h2") {
@@ -11752,8 +11752,8 @@ var require_pool_base = __commonJS({
11752
11752
  if (this[kQueue].isEmpty()) {
11753
11753
  return Promise.all(this[kClients].map((c) => c.close()));
11754
11754
  } else {
11755
- return new Promise((resolve2) => {
11756
- this[kClosedResolve] = resolve2;
11755
+ return new Promise((resolve3) => {
11756
+ this[kClosedResolve] = resolve3;
11757
11757
  });
11758
11758
  }
11759
11759
  }
@@ -12331,7 +12331,7 @@ var require_readable = __commonJS({
12331
12331
  if (this.closed) {
12332
12332
  return Promise.resolve(null);
12333
12333
  }
12334
- return new Promise((resolve2, reject) => {
12334
+ return new Promise((resolve3, reject) => {
12335
12335
  const signalListenerCleanup = signal ? util.addAbortListener(signal, () => {
12336
12336
  this.destroy();
12337
12337
  }) : noop;
@@ -12340,7 +12340,7 @@ var require_readable = __commonJS({
12340
12340
  if (signal && signal.aborted) {
12341
12341
  reject(signal.reason || Object.assign(new Error("The operation was aborted"), { name: "AbortError" }));
12342
12342
  } else {
12343
- resolve2(null);
12343
+ resolve3(null);
12344
12344
  }
12345
12345
  }).on("error", noop).on("data", function(chunk) {
12346
12346
  limit -= chunk.length;
@@ -12362,11 +12362,11 @@ var require_readable = __commonJS({
12362
12362
  throw new TypeError("unusable");
12363
12363
  }
12364
12364
  assert2(!stream[kConsume]);
12365
- return new Promise((resolve2, reject) => {
12365
+ return new Promise((resolve3, reject) => {
12366
12366
  stream[kConsume] = {
12367
12367
  type,
12368
12368
  stream,
12369
- resolve: resolve2,
12369
+ resolve: resolve3,
12370
12370
  reject,
12371
12371
  length: 0,
12372
12372
  body: []
@@ -12401,12 +12401,12 @@ var require_readable = __commonJS({
12401
12401
  }
12402
12402
  }
12403
12403
  function consumeEnd(consume2) {
12404
- const { type, body, resolve: resolve2, stream, length } = consume2;
12404
+ const { type, body, resolve: resolve3, stream, length } = consume2;
12405
12405
  try {
12406
12406
  if (type === "text") {
12407
- resolve2(toUSVString(Buffer.concat(body)));
12407
+ resolve3(toUSVString(Buffer.concat(body)));
12408
12408
  } else if (type === "json") {
12409
- resolve2(JSON.parse(Buffer.concat(body)));
12409
+ resolve3(JSON.parse(Buffer.concat(body)));
12410
12410
  } else if (type === "arrayBuffer") {
12411
12411
  const dst = new Uint8Array(length);
12412
12412
  let pos = 0;
@@ -12414,12 +12414,12 @@ var require_readable = __commonJS({
12414
12414
  dst.set(buf, pos);
12415
12415
  pos += buf.byteLength;
12416
12416
  }
12417
- resolve2(dst.buffer);
12417
+ resolve3(dst.buffer);
12418
12418
  } else if (type === "blob") {
12419
12419
  if (!Blob2) {
12420
12420
  Blob2 = __require("buffer").Blob;
12421
12421
  }
12422
- resolve2(new Blob2(body, { type: stream[kContentType] }));
12422
+ resolve3(new Blob2(body, { type: stream[kContentType] }));
12423
12423
  }
12424
12424
  consumeFinish(consume2);
12425
12425
  } catch (err) {
@@ -12674,9 +12674,9 @@ var require_api_request = __commonJS({
12674
12674
  };
12675
12675
  function request3(opts, callback) {
12676
12676
  if (callback === void 0) {
12677
- return new Promise((resolve2, reject) => {
12677
+ return new Promise((resolve3, reject) => {
12678
12678
  request3.call(this, opts, (err, data) => {
12679
- return err ? reject(err) : resolve2(data);
12679
+ return err ? reject(err) : resolve3(data);
12680
12680
  });
12681
12681
  });
12682
12682
  }
@@ -12849,9 +12849,9 @@ var require_api_stream = __commonJS({
12849
12849
  };
12850
12850
  function stream(opts, factory, callback) {
12851
12851
  if (callback === void 0) {
12852
- return new Promise((resolve2, reject) => {
12852
+ return new Promise((resolve3, reject) => {
12853
12853
  stream.call(this, opts, factory, (err, data) => {
12854
- return err ? reject(err) : resolve2(data);
12854
+ return err ? reject(err) : resolve3(data);
12855
12855
  });
12856
12856
  });
12857
12857
  }
@@ -13132,9 +13132,9 @@ var require_api_upgrade = __commonJS({
13132
13132
  };
13133
13133
  function upgrade(opts, callback) {
13134
13134
  if (callback === void 0) {
13135
- return new Promise((resolve2, reject) => {
13135
+ return new Promise((resolve3, reject) => {
13136
13136
  upgrade.call(this, opts, (err, data) => {
13137
- return err ? reject(err) : resolve2(data);
13137
+ return err ? reject(err) : resolve3(data);
13138
13138
  });
13139
13139
  });
13140
13140
  }
@@ -13223,9 +13223,9 @@ var require_api_connect = __commonJS({
13223
13223
  };
13224
13224
  function connect3(opts, callback) {
13225
13225
  if (callback === void 0) {
13226
- return new Promise((resolve2, reject) => {
13226
+ return new Promise((resolve3, reject) => {
13227
13227
  connect3.call(this, opts, (err, data) => {
13228
- return err ? reject(err) : resolve2(data);
13228
+ return err ? reject(err) : resolve3(data);
13229
13229
  });
13230
13230
  });
13231
13231
  }
@@ -16847,7 +16847,7 @@ var require_fetch = __commonJS({
16847
16847
  async function dispatch({ body }) {
16848
16848
  const url = requestCurrentURL(request3);
16849
16849
  const agent = fetchParams.controller.dispatcher;
16850
- return new Promise((resolve2, reject) => agent.dispatch(
16850
+ return new Promise((resolve3, reject) => agent.dispatch(
16851
16851
  {
16852
16852
  path: url.pathname + url.search,
16853
16853
  origin: url.origin,
@@ -16923,7 +16923,7 @@ var require_fetch = __commonJS({
16923
16923
  }
16924
16924
  }
16925
16925
  }
16926
- resolve2({
16926
+ resolve3({
16927
16927
  status,
16928
16928
  statusText,
16929
16929
  headersList: headers[kHeadersList],
@@ -16966,7 +16966,7 @@ var require_fetch = __commonJS({
16966
16966
  const val = headersList[n + 1].toString("latin1");
16967
16967
  headers[kHeadersList].append(key, val);
16968
16968
  }
16969
- resolve2({
16969
+ resolve3({
16970
16970
  status,
16971
16971
  statusText: STATUS_CODES[status],
16972
16972
  headersList: headers[kHeadersList],
@@ -24863,14 +24863,14 @@ var __asyncValues = function(o) {
24863
24863
  }, i);
24864
24864
  function verb(n) {
24865
24865
  i[n] = o[n] && function(v) {
24866
- return new Promise(function(resolve2, reject) {
24867
- v = o[n](v), settle(resolve2, reject, v.done, v.value);
24866
+ return new Promise(function(resolve3, reject) {
24867
+ v = o[n](v), settle(resolve3, reject, v.done, v.value);
24868
24868
  });
24869
24869
  };
24870
24870
  }
24871
- function settle(resolve2, reject, d, v) {
24871
+ function settle(resolve3, reject, d, v) {
24872
24872
  Promise.resolve(v).then(function(v2) {
24873
- resolve2({ value: v2, done: d });
24873
+ resolve3({ value: v2, done: d });
24874
24874
  }, reject);
24875
24875
  }
24876
24876
  };
@@ -25395,14 +25395,14 @@ var __asyncValues2 = function(o) {
25395
25395
  }, i);
25396
25396
  function verb(n) {
25397
25397
  i[n] = o[n] && function(v) {
25398
- return new Promise(function(resolve2, reject) {
25399
- v = o[n](v), settle(resolve2, reject, v.done, v.value);
25398
+ return new Promise(function(resolve3, reject) {
25399
+ v = o[n](v), settle(resolve3, reject, v.done, v.value);
25400
25400
  });
25401
25401
  };
25402
25402
  }
25403
- function settle(resolve2, reject, d, v) {
25403
+ function settle(resolve3, reject, d, v) {
25404
25404
  Promise.resolve(v).then(function(v2) {
25405
- resolve2({ value: v2, done: d });
25405
+ resolve3({ value: v2, done: d });
25406
25406
  }, reject);
25407
25407
  }
25408
25408
  };
@@ -26137,14 +26137,14 @@ var __asyncValues3 = function(o) {
26137
26137
  }, i);
26138
26138
  function verb(n) {
26139
26139
  i[n] = o[n] && function(v) {
26140
- return new Promise(function(resolve2, reject) {
26141
- v = o[n](v), settle(resolve2, reject, v.done, v.value);
26140
+ return new Promise(function(resolve3, reject) {
26141
+ v = o[n](v), settle(resolve3, reject, v.done, v.value);
26142
26142
  });
26143
26143
  };
26144
26144
  }
26145
- function settle(resolve2, reject, d, v) {
26145
+ function settle(resolve3, reject, d, v) {
26146
26146
  Promise.resolve(v).then(function(v2) {
26147
- resolve2({ value: v2, done: d });
26147
+ resolve3({ value: v2, done: d });
26148
26148
  }, reject);
26149
26149
  }
26150
26150
  };
@@ -26782,17 +26782,17 @@ function closedOrError(reason) {
26782
26782
  return isCancel ? closed() : error(reason);
26783
26783
  }
26784
26784
  function connect2(authority, http2SessionOptions) {
26785
- let resolve2;
26785
+ let resolve3;
26786
26786
  let reject;
26787
26787
  const conn = new Promise((res, rej) => {
26788
- resolve2 = res;
26788
+ resolve3 = res;
26789
26789
  reject = rej;
26790
26790
  });
26791
26791
  const newConn = http2.connect(authority, http2SessionOptions);
26792
26792
  newConn.on("connect", onConnect);
26793
26793
  newConn.on("error", onError);
26794
26794
  function onConnect() {
26795
- resolve2 === null || resolve2 === void 0 ? void 0 : resolve2(newConn);
26795
+ resolve3 === null || resolve3 === void 0 ? void 0 : resolve3(newConn);
26796
26796
  cleanup();
26797
26797
  }
26798
26798
  function onError(err) {
@@ -26878,13 +26878,13 @@ function ready(conn, options) {
26878
26878
  },
26879
26879
  verify() {
26880
26880
  conn.ref();
26881
- return new Promise((resolve2) => {
26881
+ return new Promise((resolve3) => {
26882
26882
  commonPing(() => {
26883
26883
  if (streamCount == 0)
26884
26884
  conn.unref();
26885
- resolve2(true);
26885
+ resolve3(true);
26886
26886
  });
26887
- conn.once("error", () => resolve2(false));
26887
+ conn.once("error", () => resolve3(false));
26888
26888
  });
26889
26889
  },
26890
26890
  abort(reason) {
@@ -27015,7 +27015,7 @@ function createNodeHttpClient(options) {
27015
27015
  function createNodeHttp1Client(httpOptions) {
27016
27016
  return async function request3(req) {
27017
27017
  const sentinel = createSentinel(req.signal);
27018
- return new Promise((resolve2, reject) => {
27018
+ return new Promise((resolve3, reject) => {
27019
27019
  sentinel.catch((e) => {
27020
27020
  reject(e);
27021
27021
  });
@@ -27026,7 +27026,7 @@ function createNodeHttp1Client(httpOptions) {
27026
27026
  response.on("error", sentinel.reject);
27027
27027
  sentinel.catch((reason) => response.destroy(connectErrorFromNodeReason(reason)));
27028
27028
  const trailer = new Headers();
27029
- resolve2({
27029
+ resolve3({
27030
27030
  status: (_a = response.statusCode) !== null && _a !== void 0 ? _a : 0,
27031
27031
  header: nodeHeaderToWebHeader(response.headers),
27032
27032
  body: h1ResponseIterable(sentinel, response, trailer),
@@ -27041,7 +27041,7 @@ function createNodeHttp2Client(sessionProvider) {
27041
27041
  return function request3(req) {
27042
27042
  const sentinel = createSentinel(req.signal);
27043
27043
  const sessionManager = sessionProvider(req.url);
27044
- return new Promise((resolve2, reject) => {
27044
+ return new Promise((resolve3, reject) => {
27045
27045
  sentinel.catch((e) => {
27046
27046
  reject(e);
27047
27047
  });
@@ -27055,7 +27055,7 @@ function createNodeHttp2Client(sessionProvider) {
27055
27055
  body: h2ResponseIterable(sentinel, stream, sessionManager),
27056
27056
  trailer: h2ResponseTrailer(stream)
27057
27057
  };
27058
- resolve2(response);
27058
+ resolve3(response);
27059
27059
  });
27060
27060
  });
27061
27061
  });
@@ -27120,7 +27120,7 @@ function h2Request(sentinel, sm, url, method, headers, options, onStream) {
27120
27120
  return;
27121
27121
  }
27122
27122
  const rstCode = reason instanceof ConnectError && reason.code == Code.Canceled ? H2Code.CANCEL : H2Code.INTERNAL_ERROR;
27123
- return new Promise((resolve2) => stream.close(rstCode, resolve2));
27123
+ return new Promise((resolve3) => stream.close(rstCode, resolve3));
27124
27124
  });
27125
27125
  stream.on("error", function h2StreamError(e) {
27126
27126
  if (stream.writableEnded && unwrapNodeErrorChain(e).map(getNodeErrorProps).some((p) => p.code == "ERR_STREAM_WRITE_AFTER_END")) {
@@ -27172,11 +27172,11 @@ function h2ResponseIterable(sentinel, response, sm) {
27172
27172
  }
27173
27173
  async function sinkRequest(request3, nodeRequest, sentinel) {
27174
27174
  if (request3.body === void 0) {
27175
- await new Promise((resolve2) => nodeRequest.end(resolve2));
27175
+ await new Promise((resolve3) => nodeRequest.end(resolve3));
27176
27176
  return;
27177
27177
  }
27178
27178
  const it = request3.body[Symbol.asyncIterator]();
27179
- return new Promise((resolve2) => {
27179
+ return new Promise((resolve3) => {
27180
27180
  writeNext();
27181
27181
  function writeNext() {
27182
27182
  if (sentinel.isRejected()) {
@@ -27184,7 +27184,7 @@ async function sinkRequest(request3, nodeRequest, sentinel) {
27184
27184
  }
27185
27185
  it.next().then((r) => {
27186
27186
  if (r.done === true) {
27187
- nodeRequest.end(resolve2);
27187
+ nodeRequest.end(resolve3);
27188
27188
  return;
27189
27189
  }
27190
27190
  nodeRequest.write(r.value, "binary", function(e) {
@@ -27212,8 +27212,8 @@ function createSentinel(signal) {
27212
27212
  let rej;
27213
27213
  let resolved = false;
27214
27214
  let rejected = false;
27215
- const p = new Promise((resolve2, reject) => {
27216
- res = resolve2;
27215
+ const p = new Promise((resolve3, reject) => {
27216
+ res = resolve3;
27217
27217
  rej = reject;
27218
27218
  });
27219
27219
  const c = {
@@ -29000,42 +29000,552 @@ metadata:
29000
29000
  }
29001
29001
 
29002
29002
  // cli/src/skills/submit.ts
29003
- import { existsSync as existsSync5, lstatSync as lstatSync2, readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
29004
- import { basename as basename2, join as join6 } from "node:path";
29005
- var SUBMISSION_PATH = "/v1/skills/submission";
29003
+ import { existsSync as existsSync6, lstatSync as lstatSync3, readFileSync as readFileSync6 } from "node:fs";
29004
+ import { join as join7 } from "node:path";
29005
+
29006
+ // cli/src/skills/limits.ts
29006
29007
  var MAX_FILES = 1e3;
29007
29008
  var MAX_FILE_BYTES = 20 << 20;
29008
29009
  var MAX_TOTAL_BYTES = 100 << 20;
29009
- function precheck(dir) {
29010
- const problems = [];
29011
- const md = join6(dir, "SKILL.md");
29012
- if (!existsSync5(md)) return ["there is no SKILL.md"];
29013
- const raw = readFileSync5(md, "utf8");
29014
- const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(raw);
29015
- if (!fm) problems.push("SKILL.md has no frontmatter (it must start with --- and have name / description)");
29016
- 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)");
29017
- const links = [];
29010
+
29011
+ // cli/src/skills/lint.ts
29012
+ import { existsSync as existsSync5, lstatSync as lstatSync2, readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
29013
+ import { basename as basename2, join as join6, posix, resolve as resolve2 } from "node:path";
29014
+ var CJK_RANGE = "\\u3400-\\u9fff\\uf900-\\ufaff\\u3040-\\u30ff\\uac00-\\ud7af";
29015
+ var CJK = new RegExp(`[${CJK_RANGE}]`);
29016
+ var CJK_G = new RegExp(`[${CJK_RANGE}]`, "g");
29017
+ var CJK_RUN4 = new RegExp(`[${CJK_RANGE}]{4,}`);
29018
+ var NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
29019
+ function resolveSkillDir(arg) {
29020
+ const cands = arg ? [arg, join6(SKILLS_DIR, arg), join6(DRAFTS_DIR, arg)] : ["."];
29021
+ for (const c of cands) if (existsSync5(join6(c, "SKILL.md"))) return c;
29022
+ return null;
29023
+ }
29024
+ function readFiles(dir) {
29025
+ const files = [];
29026
+ const symlinks = [];
29018
29027
  const walk = (rel) => {
29019
29028
  for (const ent of readdirSync3(join6(dir, rel), { withFileTypes: true })) {
29020
29029
  const childRel = rel ? `${rel}/${ent.name}` : ent.name;
29021
29030
  const st = lstatSync2(join6(dir, childRel));
29022
- if (st.isSymbolicLink()) links.push(childRel);
29023
- else if (st.isDirectory() && !skipSegment(ent.name, true, basename2(rel))) walk(childRel);
29031
+ if (st.isSymbolicLink()) {
29032
+ if (!skipSegment(ent.name, false, basename2(rel))) symlinks.push(childRel);
29033
+ continue;
29034
+ }
29035
+ if (skipSegment(ent.name, st.isDirectory(), basename2(rel))) continue;
29036
+ if (st.isDirectory()) {
29037
+ walk(childRel);
29038
+ continue;
29039
+ }
29040
+ if (!st.isFile()) continue;
29041
+ let text = null;
29042
+ if (st.size < 2e6) {
29043
+ const b = readFileSync5(join6(dir, childRel));
29044
+ if (!b.includes(0)) text = b.toString("utf8");
29045
+ }
29046
+ files.push({ rel: childRel, size: st.size, text });
29024
29047
  }
29025
29048
  };
29026
29049
  walk("");
29027
- if (links.length) problems.push(`symbolic links are not accepted: ${links.slice(0, 5).join(", ")}${links.length > 5 ? ` (+${links.length - 5} more)` : ""}`);
29050
+ return { files, symlinks };
29051
+ }
29052
+ var lineOf = (text, idx) => text.slice(0, Math.max(0, idx)).split("\n").length;
29053
+ var mb = (n) => `${(n / (1 << 20)).toFixed(1)} MB`;
29054
+ function frontmatter(raw) {
29055
+ const text = raw.replace(/\r\n/g, "\n");
29056
+ if (!text.startsWith("---\n")) return null;
29057
+ const end = text.indexOf("\n---\n", 4);
29058
+ const block = end >= 0 ? text.slice(4, end) : text.endsWith("\n---") ? text.slice(4, -4) : null;
29059
+ if (block == null) return null;
29060
+ const fm = {};
29061
+ let key = "";
29062
+ for (const l of block.split("\n")) {
29063
+ const m = /^([A-Za-z_][\w.-]*):\s*(.*)$/.exec(l);
29064
+ if (m) {
29065
+ key = m[1];
29066
+ fm[key] = m[2].replace(/^["']|["']$/g, "");
29067
+ } else if (key && /^\s+\S/.test(l)) fm[key] = `${fm[key]} ${l.trim()}`.trim();
29068
+ }
29069
+ for (const k of Object.keys(fm)) if (/^[>|][-+]?$/.test(fm[k].split(" ")[0] ?? "")) fm[k] = fm[k].replace(/^[>|][-+]?\s*/, "");
29070
+ const bodyAt = end >= 0 ? end + 5 : text.length;
29071
+ return { fm, body: text.slice(bodyAt), bodyStart: lineOf(text, bodyAt) };
29072
+ }
29073
+ var SECRET_RULES = [
29074
+ { kind: "AWS access key", re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/, captures: false },
29075
+ { kind: "GitHub token", re: /\b(?:gh[pousr]_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{60,})\b/, captures: false },
29076
+ { kind: "GitLab token", re: /\bglpat-[A-Za-z0-9_-]{20,}\b/, captures: false },
29077
+ { kind: "Slack token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/, captures: false },
29078
+ { kind: "Anthropic key", re: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/, captures: false },
29079
+ { kind: "OpenAI key", re: /\bsk-(?:proj-|svcacct-)?[A-Za-z0-9_-]{32,}\b/, captures: false },
29080
+ { kind: "Google API key", re: /\bAIza[0-9A-Za-z_-]{35}\b/, captures: false },
29081
+ { kind: "Stripe key", re: /\b(?:sk|rk)_live_[A-Za-z0-9]{20,}\b/, captures: false },
29082
+ { kind: "connection string with a password", re: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqps?):\/\/[^\s:/@]+:([^\s@/]+)@/g, captures: true },
29083
+ { kind: "credentials embedded in a URL", re: /\bhttps?:\/\/[^\s:/@]+:([^\s@/]+)@/g, captures: true }
29084
+ ];
29085
+ var PRIVATE_KEY_LABEL = /-----BEGIN (?:RSA |EC |DSA |OPENSSH |ENCRYPTED |PGP )?PRIVATE KEY(?: BLOCK)?-----/g;
29086
+ var PLACEHOLDER = /^(?:\$\{?[A-Z0-9_]+\}?|<[^>]*>|\{\{[^}]*\}\}|x+|\*+|\.+|pass(?:word)?|passwd|pwd|secret|changeme|example|your[-_]?password|\[[^\]]*\])$/i;
29087
+ function privateKeyHit(text) {
29088
+ for (const m of text.matchAll(PRIVATE_KEY_LABEL)) {
29089
+ const start = m.index + m[0].length;
29090
+ let w = text.slice(start, start + 400).replace(/\r/g, "");
29091
+ w = w.replace(/\\+[rn]/g, "\n");
29092
+ w = w.replace(/["'`]?[ \t]*\+?[ \t]*\n[ \t]*\+?[ \t]*["'`]?/g, "");
29093
+ w = w.replace(/["'`][ \t]*\+[ \t]*["'`]/g, "");
29094
+ w = w.replace(/^["'` ,;]+/, "");
29095
+ if (/[A-Za-z0-9+/]{40}/.test(w)) return m.index;
29096
+ }
29097
+ return -1;
29098
+ }
29099
+ function scanSecret(text) {
29100
+ const pk = privateKeyHit(text);
29101
+ if (pk >= 0) return { kind: "private key", index: pk };
29102
+ for (const r of SECRET_RULES) {
29103
+ if (!r.captures) {
29104
+ const m = r.re.exec(text);
29105
+ if (m) return { kind: r.kind, index: m.index };
29106
+ continue;
29107
+ }
29108
+ for (const m of text.matchAll(r.re)) if (!PLACEHOLDER.test(m[1])) return { kind: r.kind, index: m.index };
29109
+ }
29110
+ return null;
29111
+ }
29112
+ var SKILL_DIRS = "references|scripts|tools|templates|assets|bin|runtime|evals|examples|hub|lib|src";
29113
+ var PREFIX = String.raw`(?:\.\/|\$\{?[A-Za-z_]+\}?\/|<skill[-_ ]?dir>\/)?`;
29114
+ var REF_IN_CODE = new RegExp(String.raw`(?:\x60|\]\()` + PREFIX + String.raw`((?:${SKILL_DIRS})\/[^\s\x60)'"]+)`, "g");
29115
+ var REF_IN_CMD = new RegExp(String.raw`\b(?:node|python3?|bash|sh|deno|bun)\s+` + PREFIX + String.raw`([A-Za-z0-9_.\-/]+\.(?:m?js|cjs|ts|py|sh))\b`, "g");
29116
+ var ABS_WORKSPACE = /(^|[\s`'"(])(\/workspace\/[^\s`'")]*)/g;
29117
+ var PLATFORM_MODS = /* @__PURE__ */ new Set(["neta", "cohub_base"]);
29118
+ var ABS_MODS = /\/mods\/([a-z0-9_-]+)\//g;
29119
+ var NEGATED = /\b(never|not|don'?t|instead of|avoid|rather than)\b/i;
29120
+ var CREATES = /\b(save[sd]? (it )?(as|to)|write[s]? (it )?to|creat\w* |output[s]? to|generat\w* |produc\w* |render\w* to|export\w* to)\W*$/i;
29121
+ var leadIn = (t, idx) => {
29122
+ const w = t.slice(Math.max(0, idx - 160), idx);
29123
+ const cut = Math.max(w.lastIndexOf("\n\n"), w.search(/[.!?]\s[^.!?]*$/));
29124
+ return cut >= 0 ? w.slice(cut) : w;
29125
+ };
29126
+ function checkPaths(dir, files, add) {
29127
+ const all = files.map((f) => f.rel);
29128
+ const tops = new Set(all.map((r) => r.split("/")[0]));
29129
+ const exists = (p) => existsSync5(join6(dir, p)) || all.some((r) => r === p || r.endsWith(`/${p}`) || r.startsWith(`${p}/`) || r.includes(`/${p}/`));
29130
+ const docs = files.filter((f) => f.text && (f.rel === "SKILL.md" || /^references\//.test(f.rel) && /\.md$/i.test(f.rel)));
29131
+ const mods = /* @__PURE__ */ new Map();
29132
+ for (const f of docs) {
29133
+ const t = f.text;
29134
+ const seen = /* @__PURE__ */ new Set();
29135
+ const report = (p, idx) => {
29136
+ const before = t.slice(Math.max(0, idx - 60), idx);
29137
+ if (CREATES.test(before) || /['\u2019]s\s*$/.test(before.replace(/[`\s]+$/, "")) || /['\u2019]s\s+`?$/.test(before)) return;
29138
+ const clean = p.replace(/[.,:;]+$/, "").replace(/\/$/, "");
29139
+ if (seen.has(clean) || /[<>*{}$]|\.\.\.|\u2026|node_modules|(^|\/)\./.test(clean) || !tops.has(clean.split("/")[0])) return;
29140
+ seen.add(clean);
29141
+ if (!exists(clean)) add({ level: "warn", check: "paths", file: f.rel, line: lineOf(t, idx), message: `refers to ${clean}, which is not in the skill.`, fix: "Fix the path, or add the file. Paths are relative to the skill folder." });
29142
+ };
29143
+ for (const m of t.matchAll(REF_IN_CODE)) report(m[1], m.index);
29144
+ for (const m of t.matchAll(REF_IN_CMD)) {
29145
+ const p = m[1];
29146
+ if (p.startsWith("/") || !new RegExp(`^(?:${SKILL_DIRS})/`).test(p)) continue;
29147
+ report(p, m.index);
29148
+ }
29149
+ for (const m of t.matchAll(ABS_WORKSPACE)) {
29150
+ if (NEGATED.test(leadIn(t, m.index))) continue;
29151
+ add({ level: "warn", check: "paths", file: f.rel, line: lineOf(t, m.index), message: `hard-codes the absolute path ${m[2]}; the skill runs from different places (installed, used without installing, from a mod) and in different worlds.`, fix: "Use relative paths: the skill's own files relative to the skill folder (the folder that holds this SKILL.md), working files relative to the current folder." });
29152
+ break;
29153
+ }
29154
+ for (const m of t.matchAll(ABS_MODS)) if (!PLATFORM_MODS.has(m[1]) && !mods.has(m[1]) && !NEGATED.test(leadIn(t, m.index))) mods.set(m[1], { file: f.rel, line: lineOf(t, m.index) });
29155
+ }
29156
+ for (const [mod, at] of mods) add({ level: "warn", check: "paths", file: at.file, line: at.line, message: `depends on files under /mods/${mod}/; it only works in worlds that mount the ${mod} mod.`, fix: "Fine if that is intended (for example /mods/neta in Neta worlds); otherwise ship what you need inside the skill." });
29157
+ }
29158
+ var KINDS = ["web", "media", "material", "text", "roleplay", "per-request"];
29159
+ var CHECK_TYPES = ["answer", "cmd", "no-cmd", "skill-added", "no-skill-added"];
29160
+ function checkMatrix(files, add) {
29161
+ const mf = files.find((f) => f.rel === "evals/matrix.json");
29162
+ const F = "evals/matrix.json";
29163
+ const w = (message, fix) => add({ level: "warn", check: "test-declarations", file: F, line: 0, message, fix });
29164
+ if (!mf) {
29165
+ add({ level: "warn", check: "test-declarations", file: F, line: 0, message: "no test declarations: if Neta reviews this skill, it has to guess what each route should deliver.", fix: "Add evals/matrix.json with your scenarios and what each delivers (skill-creator writes one for you)." });
29166
+ return;
29167
+ }
29168
+ let mx;
29169
+ try {
29170
+ mx = JSON.parse(mf.text ?? "");
29171
+ } catch (e) {
29172
+ w(`not valid JSON (${e.message}).`);
29173
+ return;
29174
+ }
29175
+ const list = mx?.scenarios?.list ?? [];
29176
+ if (!list.length) w("scenarios.list is empty.");
29177
+ const ids = new Set(list.map((s) => s?.id));
29178
+ const top = mx?.primaryKind?.kind;
29179
+ for (const s of list) {
29180
+ if (!s?.id) {
29181
+ w("a scenario has no id.");
29182
+ continue;
29183
+ }
29184
+ const k = s.primaryKind?.kind;
29185
+ if (!k && !top) w(`scenario ${s.id} does not say what it delivers (primaryKind, on the scenario or once for the whole skill).`);
29186
+ else if (k && !KINDS.includes(k)) w(`scenario ${s.id}: primaryKind.kind "${k}" is not one of ${KINDS.join(", ")}.`);
29187
+ if (k === "text" && !(mx.checks?.[s.id]?.require?.length || mx.checks?.[s.id]?.oneOf?.length))
29188
+ w(`scenario ${s.id} delivers text but declares no checks, so any reply would look like a pass.`, `Add checks.${s.id}.require with what the reply must do (cmd / no-cmd / skill-added / answer).`);
29189
+ if (top && k && top !== "per-request" && k !== top && !mx.scenarios?.overrides?.[s.id]?.primaryKind)
29190
+ w(`scenario ${s.id} delivers ${k} but the skill as a whole says ${top}.`, "Make them agree, or set primaryKind to per-request at the top.");
29191
+ }
29192
+ for (const [col, sid] of Object.entries(mx?.scenarios?.assign ?? {})) if (!ids.has(sid)) w(`assign.${col} points at "${String(sid)}", which is not a scenario id.`);
29193
+ if (mx?.questionnaire && !["always", "conditional", "no"].includes(mx.questionnaire.mode)) w(`questionnaire.mode "${mx.questionnaire.mode}" must be always, conditional or no.`);
29194
+ const assertIds = /* @__PURE__ */ new Set();
29195
+ const checkA = (a, where) => {
29196
+ if (!a?.id || !CHECK_TYPES.includes(a.type)) {
29197
+ w(`${where}: each check needs an id and a type (${CHECK_TYPES.join(", ")}).`);
29198
+ return;
29199
+ }
29200
+ assertIds.add(a.id);
29201
+ if ((a.type === "cmd" || a.type === "no-cmd") && !a.match) w(`${where}.${a.id}: ${a.type} needs match.`);
29202
+ if (a.type === "answer" && !a.point) w(`${where}.${a.id}: answer needs point.`);
29203
+ for (const re of [a.match, a.handle]) if (re) {
29204
+ try {
29205
+ new RegExp(re, "i");
29206
+ } catch (e) {
29207
+ w(`${where}.${a.id}: invalid regular expression (${e.message}).`);
29208
+ }
29209
+ }
29210
+ };
29211
+ for (const [sid, c] of Object.entries(mx?.checks ?? {})) {
29212
+ if (sid.startsWith("_")) continue;
29213
+ if (!ids.has(sid)) w(`checks.${sid}: there is no scenario with that id.`);
29214
+ for (const a of c?.require ?? []) checkA(a, `checks.${sid}`);
29215
+ for (const b of c?.oneOf ?? []) for (const a of b?.require ?? []) checkA(a, `checks.${sid}.oneOf.${b?.id}`);
29216
+ }
29217
+ for (const r of Array.isArray(mx?.rules) ? mx.rules : []) {
29218
+ for (const cid of r?.checks ?? []) if (!assertIds.has(cid)) w(`rule ${r.id}: check "${cid}" is not declared under checks.`);
29219
+ if (r?.risk === "high" && !(r.checks ?? []).length && !r.waived) w(`rule ${r.id} is high-risk but nothing tests it.`, 'Add a check id, or waived: "<why it is not tested>".');
29220
+ }
29221
+ }
29222
+ function checkWriting(body, bodyStart, add) {
29223
+ const S = body;
29224
+ const has = (re) => re.test(S);
29225
+ const at = (re) => {
29226
+ const m = re.exec(S);
29227
+ return m ? lineOf(S, m.index) + bodyStart - 1 : 0;
29228
+ };
29229
+ const w = (line, message, fix) => add({ level: "warn", check: "writing", file: "SKILL.md", line, message, fix });
29230
+ if (!has(/user'?s language|language of the user|same language as the user/i) && !has(new RegExp("\\u7528\\u6237\\u7684?\\u8bed\\u8a00|\\u8ddf\\u968f\\u7528\\u6237")))
29231
+ w(0, "nothing says replies and the deliverable's text follow the user's language.", `Add: "Reply in the user's language. Text the user sees in the deliverable follows the user's language."`);
29232
+ const makesWork = has(/neta(\/neta)?\s+(work\s+(init|publish)|generate)|\bwork init\b/i);
29233
+ if (!has(/^#+[^\n]*(deliver|publish|output|hand ?(off|over)|result)/im) && !has(new RegExp("^#+[^\\n]*(\\u4ea4\\u4ed8|\\u53d1\\u5e03)", "m")))
29234
+ w(0, "no section that says what the user gets at the end and how it reaches them.", "Add a Deliver section: the kind of deliverable and the steps that hand it over.");
29235
+ if (makesWork && !has(/interaction-components/i))
29236
+ w(at(/deliver/i), "makes a Work but does not say to hand it over with an interaction-components card pointing at it.", "Say: hand over with an interaction-components card whose target is the Work (its id comes from the publish output).");
29237
+ if (has(/neta(\/neta)?\s+generate\s+(image|video|audio|3d)\b/i) && !has(/--work-title/))
29238
+ w(at(/neta(\/neta)?\s+generate/i), "generates media without --work-title, so the Work is titled with the prompt's first words.", 'Pass --work-title "<title>" (again with --target-work when re-rendering).');
29239
+ const OOS = new RegExp("^#+[^\\n]*(out of scope|not in scope|explicitly not|not doing|do not|don'?t|never|boundar|won'?t do|will not do|rules|guardrails|limits|\\u4e0d\\u505a|\\u7981\\u6b62|\\u786c\\u89c4\\u5219|\\u8fb9\\u754c|\\u4e0d\\u8981)[^\\n]*$", "im");
29240
+ const oosAt = S.search(OOS);
29241
+ if (oosAt < 0) w(0, "no Out of scope section.", "Add one: no changes to world settings, no deleting existing files, nothing beyond the deliverable.");
29242
+ else {
29243
+ const NEG = "(?:don'?t|do not|never|no|must not|without|\\u4e0d|\\u7981\\u6b62|\\u522b)";
29244
+ const worldOk = new RegExp(`${NEG}[^\\n]{0,60}(world[^\\n]{0,20}(setting|config)|WORLD\\.md|visual ?style|\\u4e16\\u754c(\\u8bbe\\u5b9a|\\u8bbe\\u7f6e|\\u89c2|\\u914d\\u7f6e))`, "i").test(S);
29245
+ const delOk = new RegExp(`${NEG}[^\\n]{0,60}(delet|remov|overwrit|\\u5220|\\u8986\\u76d6)`, "i").test(S);
29246
+ if (!worldOk || !delOk)
29247
+ w(lineOf(S, oosAt) + bodyStart - 1, 'Out of scope does not say "do not change world settings" and "do not delete existing files".', "Add both lines.");
29248
+ }
29249
+ if (!has(/counts? as given|already given|given when|if (a |any )?required .{0,40}missing|ask (once|only)|no questions|never asks?|do(es)? not ask|don'?t ask|without asking|questionnaire/i) && !has(new RegExp("\\u4f55\\u65f6\\u505c\\u4e0b\\u6765\\u95ee|\\u7f3a.{0,8}\\u95ee|\\u7b97\\u5df2\\u7ed9|\\u4e0d\\u95ee|\\u95ee\\u5377")))
29250
+ w(0, "no rule for when to ask the user: which inputs are required, and when each counts as already given.", "List the required inputs with 'counts as given when'; missing \u2192 ask once before building; all given \u2192 build to delivery without asking.");
29251
+ const WAIT = /(wait (for|until) (the )?user'?s? (confirm\w*|approv\w*|go-?ahead|reply|response|answer)|wait for (confirmation|approval)|(get|ask for|request|obtain) (the user'?s? )?(confirmation|approval|sign-?off) before|do not (start|proceed|render|generate|build)\w* until (the user|they)|stop and wait)/i;
29252
+ S.split("\n").forEach((l, i) => {
29253
+ if (WAIT.test(l) && !/\b(if|unless|only when|missing)\b/i.test(l))
29254
+ w(i + bodyStart, `stops for confirmation every time: "${l.trim().slice(0, 100)}"`, "Stop only when something required is missing; otherwise state the default and keep going.");
29255
+ });
29256
+ }
29257
+ var SHIPPED_DIR = /^(templates?|assets|runtime|public|static|web|site)\//;
29258
+ var UI_EXT = /\.(html?|m?js|jsx|tsx|vue|svelte|svg)$/i;
29259
+ var isShipped = (r) => !/(^|\/)(vendor|third_party|node_modules|scripts|tools|tests?|__tests__|i18n|locales?|lang)\//.test(r) && !/\.(test|spec)\.|selftest|\.(zh|ja|ko)([-_][a-z]+)?\.[a-z]+$/i.test(r) && // 测试、按语言命名的语言文件
29260
+ UI_EXT.test(r) && (SHIPPED_DIR.test(r) || /\.(html?|vue|svelte|svg)$/i.test(r));
29261
+ var NOT_UI_LINE = /\bconsole\.(log|warn|error|info|debug)\(|lang(uage)?\s*===?\s*['"](zh|ja|ko)|['"](zh|ja|ko)['"]\s*===?\s*\w*lang/i;
29262
+ var LOCALE_KEY = /^\s*['"]?(zh|ja|ko|zh[-_](?:cn|tw|hans|hant)|ja[-_]jp|ko[-_]kr)['"]?\s*:/i;
29263
+ function uiLines(text) {
29264
+ const out = [];
29265
+ let block = "";
29266
+ let dictDepth = 0;
29267
+ text.split("\n").forEach((raw, i) => {
29268
+ let l = raw;
29269
+ if (block === "c") {
29270
+ const e = l.indexOf("*/");
29271
+ if (e < 0) return;
29272
+ l = l.slice(e + 2);
29273
+ block = "";
29274
+ }
29275
+ if (block === "h") {
29276
+ const e = l.indexOf("-->");
29277
+ if (e < 0) return;
29278
+ l = l.slice(e + 3);
29279
+ block = "";
29280
+ }
29281
+ l = l.replace(/\/\*.*?\*\//g, "").replace(/<!--.*?-->/g, "");
29282
+ const c0 = l.indexOf("/*");
29283
+ if (c0 >= 0) {
29284
+ l = l.slice(0, c0);
29285
+ block = "c";
29286
+ }
29287
+ const h0 = l.indexOf("<!--");
29288
+ if (h0 >= 0) {
29289
+ l = l.slice(0, h0);
29290
+ block = "h";
29291
+ }
29292
+ l = l.replace(/(^|[^:'"])\/\/.*$/, "$1");
29293
+ if (dictDepth > 0) {
29294
+ dictDepth += (l.match(/\{/g) ?? []).length - (l.match(/\}/g) ?? []).length;
29295
+ return;
29296
+ }
29297
+ if (LOCALE_KEY.test(l)) {
29298
+ const d = (l.match(/\{/g) ?? []).length - (l.match(/\}/g) ?? []).length;
29299
+ if (d > 0) dictDepth = d;
29300
+ return;
29301
+ }
29302
+ out.push({ line: i + 1, text: l });
29303
+ });
29304
+ return out;
29305
+ }
29306
+ function checkHardcodedUi(files, add) {
29307
+ for (const f of files) {
29308
+ if (!f.text || !isShipped(f.rel)) continue;
29309
+ const hits = uiLines(f.text).filter((x) => CJK_RUN4.test(x.text) && !NOT_UI_LINE.test(x.text));
29310
+ const lang = /<html[^>]*lang=["'](zh|ja|ko)/i.exec(f.text);
29311
+ if (!hits.length && !lang) continue;
29312
+ const first = hits[0] ?? { line: lineOf(f.text, lang.index), text: "hard-coded <html lang>" };
29313
+ const n = hits.length || 1;
29314
+ add({ level: "warn", check: "writing", file: f.rel, line: first.line, message: `${n} line(s) of non-English text the user will see: "${first.text.trim().slice(0, 80)}"`, fix: "Put UI text in a locale table (English plus your users' languages) chosen from the user's language; set <html lang> from it." });
29315
+ }
29316
+ }
29317
+ var QUOTED = new RegExp(`(\u201C[^\u201D]*\u201D|\u300C[^\u300D]*\u300D|"[^"]*")`, "g");
29318
+ var CJK4 = new RegExp(`[${CJK_RANGE}][^"\u201D\u300D]*?(?:[${CJK_RANGE}][^"\u201D\u300D]*?){3}`);
29319
+ var AGENT_SAYS = new RegExp("\\b(ask(s|ed)?\\s+(the user|them|whether|which|if)|tell (the user|them)|reply with|respond with|say(ing)?\\s*[:\\uff1a]|message\\s*[:\\uff1a]|greet|options?\\s*[:\\uff1a]|choices?\\s*[:\\uff1a])|\\u95ee\\u7528\\u6237|\\u8be2\\u95ee|\\u56de\\u590d|\\u544a\\u8bc9\\u7528\\u6237|\\u5bf9\\u7528\\u6237\\u8bf4|\\u63d0\\u793a\\u7528\\u6237|\\u9009\\u9879", "i");
29320
+ var USER_SAYS = new RegExp("(user|they|users)\\s*(says?|said|types?|typed|writes?|wrote|asks?)\\s*$|\\u7528\\u6237(\\u8bf4|\\u5199|\\u70b9\\u540d|\\u95ee)\\s*$", "i");
29321
+ function checkUserPhrases(files, add) {
29322
+ const docs = files.filter((f) => f.text && (f.rel === "SKILL.md" || /^references\//.test(f.rel) && /\.md$/i.test(f.rel)) && !/\.(zh|ja|ko|zh-cn|zh-tw)\.md$/i.test(f.rel));
29323
+ for (const f of docs) {
29324
+ let inFm = false, fence = false, n = 0, first = 0, sample = "";
29325
+ const headings = (f.text.match(/^#+\s+.*$/gm) ?? []).map((h) => h.replace(/^#+\s+/, "").trim());
29326
+ const isHeading = (q) => {
29327
+ const x = q.slice(1, -1).trim();
29328
+ return x.length > 0 && headings.some((h) => h.includes(x));
29329
+ };
29330
+ f.text.split("\n").forEach((l, i) => {
29331
+ if (i === 0 && l.trim() === "---") {
29332
+ inFm = true;
29333
+ return;
29334
+ }
29335
+ if (inFm) {
29336
+ if (l.trim() === "---") inFm = false;
29337
+ return;
29338
+ }
29339
+ if (/^\s*```/.test(l)) {
29340
+ fence = !fence;
29341
+ return;
29342
+ }
29343
+ if (fence || !AGENT_SAYS.test(l)) return;
29344
+ const said = [...l.matchAll(QUOTED)].some((m) => CJK4.test(m[0]) && !isHeading(m[0]) && !USER_SAYS.test(l.slice(0, m.index).replace(/[\s:\uff1a,\uff0c]+$/, "")));
29345
+ if (said) {
29346
+ n++;
29347
+ if (!first) {
29348
+ first = i + 1;
29349
+ sample = l.trim().slice(0, 90);
29350
+ }
29351
+ }
29352
+ });
29353
+ if (n) add({ level: "warn", check: "user-phrases", file: f.rel, line: first, message: `${n} line(s) give the agent ready-made non-English words to say to the user (questions, options or fixed replies), e.g. "${sample}". Agents copy these, so users writing in another language get asked in this one.`, fix: `Describe what to ask instead (e.g. "ask which style the user wants"), or add "say it in the user's language" next to it.` });
29354
+ }
29355
+ }
29356
+ function checkEnglish(files, fm, body, add) {
29357
+ const agentDoc = (r) => r === "SKILL.md" || /^references\//.test(r) && /\.md$/i.test(r) && !/\.(zh|ja|ko|zh-cn|zh-tw)\.md$/i.test(r);
29358
+ const mostlyNonEnglish = (t) => {
29359
+ let fence = false, cjk = 0, latin = 0;
29360
+ for (const l of t.split("\n")) {
29361
+ if (/^\s*```/.test(l)) {
29362
+ fence = !fence;
29363
+ continue;
29364
+ }
29365
+ if (fence) continue;
29366
+ const x = l.replace(/^\s*>.*$/, "").replace(/\u201c[^\u201d]*\u201d|\u300c[^\u300d]*\u300d|"[^"]*"|`[^`]*`/g, "");
29367
+ cjk += (x.match(CJK_G) ?? []).length;
29368
+ latin += (x.match(/[A-Za-z]/g) ?? []).length;
29369
+ }
29370
+ return cjk > 40 && cjk > latin / 4;
29371
+ };
29372
+ const descHead = (fm.description ?? "").split(/\bTriggers?\s*[-:\u2013]/i)[0];
29373
+ const dc = (descHead.match(CJK_G) ?? []).length, dl = (descHead.match(/[A-Za-z]/g) ?? []).length;
29374
+ const bad = files.filter((f) => f.text && agentDoc(f.rel) && mostlyNonEnglish(f.rel === "SKILL.md" ? body : f.text)).map((f) => f.rel);
29375
+ if (dc > 8 && dl < 40 && !bad.includes("SKILL.md")) bad.unshift("SKILL.md (description)");
29376
+ if (!bad.length) return;
29377
+ add({ level: "warn", check: "english", file: bad[0].split(" ")[0], line: 0, message: `not written in English: ${bad.slice(0, 6).join(", ")}${bad.length > 6 ? ` (+${bad.length - 6} more)` : ""}. Writing what the agent reads in English is recommended: users speak many languages, and replies still follow each user's language.`, fix: "Translate to English (skill-creator can do it), keeping commands, paths, trigger words and quoted user text as they are. Trigger words in other languages can stay in the description." });
29378
+ }
29379
+ var LISTING_LOCALES = ["zh", "en"];
29380
+ var MAX_LISTING_NAME = 80;
29381
+ var MAX_LISTING_DESC = 300;
29382
+ var MAX_LISTING_WORKS = 6;
29383
+ var WORK_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
29384
+ var WORK_LINK = /^https:\/\/(?:www\.)?neta\.art\/w\/[^\s]+$/i;
29385
+ function checkListing(files, add) {
29386
+ const F = "listing.json";
29387
+ const w = (message, fix) => add({ level: "warn", check: "listing", file: F, line: 0, message, fix });
29388
+ const lf = files.find((f) => f.rel === F);
29389
+ if (!lf) {
29390
+ w("no listing.json (optional): if the Neta team features this skill in Studio, the name, description and showcase you write here are used first.", 'Add {"en": {"name", "description"}, "zh": {...}, "media": {"works": ["<id or https://neta.art/w/... link of a Work made with this skill and shared publicly>"]}}. skill-creator can write it.');
29391
+ return;
29392
+ }
29393
+ let j;
29394
+ try {
29395
+ j = JSON.parse(lf.text ?? "");
29396
+ } catch (e) {
29397
+ w(`not valid JSON (${e.message}).`);
29398
+ return;
29399
+ }
29400
+ if (!j || typeof j !== "object" || Array.isArray(j)) {
29401
+ w("must be a JSON object.");
29402
+ return;
29403
+ }
29404
+ for (const k of Object.keys(j)) if (![...LISTING_LOCALES, "media"].includes(k)) w(`unknown key "${k}"; only ${LISTING_LOCALES.join(", ")} and media are read.`);
29405
+ if (!LISTING_LOCALES.some((l) => j[l])) w(`no name and description: give at least one of ${LISTING_LOCALES.join(", ")}.`);
29406
+ const text = (v, field, max) => {
29407
+ if (v == null || v === "") {
29408
+ w(`${field} is empty.`);
29409
+ return;
29410
+ }
29411
+ if (typeof v !== "string") {
29412
+ w(`${field} must be text.`);
29413
+ return;
29414
+ }
29415
+ if ([...v].length > max) w(`${field} is ${[...v].length} characters; at most ${max}.`);
29416
+ if (/[\u0000-\u001f\u007f]/.test(v)) w(`${field} contains control characters.`);
29417
+ };
29418
+ for (const l of LISTING_LOCALES) {
29419
+ if (j[l] == null) continue;
29420
+ text(j[l]?.name, `${l}.name`, MAX_LISTING_NAME);
29421
+ text(j[l]?.description, `${l}.description`, MAX_LISTING_DESC);
29422
+ }
29423
+ if (j.media == null) return;
29424
+ const works = j.media?.works;
29425
+ if (!Array.isArray(works)) {
29426
+ w('media.works must be a list of Works: ["<id or https://neta.art/w/... link>"].');
29427
+ return;
29428
+ }
29429
+ if (works.length > MAX_LISTING_WORKS) w(`media.works has ${works.length} Works; at most ${MAX_LISTING_WORKS} (the Neta team picks one).`);
29430
+ for (const x of works) {
29431
+ if (typeof x === "string" && (WORK_ID.test(x) || WORK_LINK.test(x))) continue;
29432
+ w(`media.works: "${String(x).slice(0, 80)}" is not a Studio Work. Only Works made in Studio are shown (not links to other sites): they show the skill works in Studio, and sharing them publicly is how you agree to have them shown.`, "Use the id or the https://neta.art/w/... link of a Work made with this skill and shared publicly.");
29433
+ }
29434
+ }
29435
+ function lintSkill(dir) {
29436
+ const findings = [];
29437
+ const add = (f) => findings.push(f);
29438
+ const { files, symlinks } = readFiles(dir);
29439
+ const name = basename2(resolve2(dir));
29440
+ const skill = files.find((f) => f.rel === "SKILL.md");
29441
+ const mdPath = join6(dir, "SKILL.md");
29442
+ const mdIsLink = existsSync5(mdPath) && lstatSync2(mdPath).isSymbolicLink();
29443
+ const rawFile = skill?.text ?? (mdIsLink ? readFileSync5(mdPath, "utf8") : "");
29444
+ const bom = rawFile.startsWith("\uFEFF");
29445
+ const raw = bom ? rawFile.slice(1) : rawFile;
29446
+ const crlf = !bom && raw.replace(/\r\n/g, "\n").startsWith("---\n") && !(raw.startsWith("---\n") && raw.indexOf("\n---\n") >= 0);
29447
+ const parsed = frontmatter(raw);
29448
+ const fm = parsed?.fm ?? {};
29449
+ const body = parsed?.body ?? raw;
29450
+ const bodyStart = parsed?.bodyStart ?? 1;
29451
+ if (lstatSync2(dir).isSymbolicLink()) add({ level: "error", check: "publish-limits", file: "", line: 0, message: "the skill folder itself is a symbolic link; the skills site rejects it.", fix: "Move the real folder here instead of linking to it." });
29452
+ if (mdIsLink) add({ level: "error", check: "publish-limits", file: "SKILL.md", line: 0, message: "SKILL.md is a symbolic link; the skills site rejects it.", fix: "Replace it with the real file." });
29453
+ if (bom) add({ level: "error", check: "structure", file: "SKILL.md", line: 1, message: "SKILL.md starts with a byte-order mark (BOM); the skills site needs it to start with --- and would not publish it.", fix: "Save SKILL.md as UTF-8 without BOM." });
29454
+ else if (crlf) add({ level: "error", check: "structure", file: "SKILL.md", line: 1, message: "SKILL.md has Windows line endings (CRLF) around the frontmatter; the skills site only reads it with Unix line endings (LF) and would not publish it.", fix: "Save SKILL.md with LF line endings." });
29455
+ if (!parsed) add({ level: "error", check: "structure", file: "SKILL.md", line: 1, message: "SKILL.md has no frontmatter (it must start with --- and have name and description).", fix: "Add a --- block at the top with name and description." });
29456
+ else if (!fm.description?.trim()) add({ level: "error", check: "structure", file: "SKILL.md", line: 2, message: "the frontmatter has no description; agents decide when to use a skill by it.", fix: "Add description: what it makes and when to use it." });
29457
+ if (parsed) {
29458
+ if (fm.name && fm.name !== name) add({ level: "warn", check: "structure", file: "SKILL.md", line: 2, message: `name "${fm.name}" differs from the folder name "${name}"; the folder name is what gets published.`, fix: "Make them the same." });
29459
+ if (fm.description && fm.description.length > 1024) add({ level: "warn", check: "structure", file: "SKILL.md", line: 3, message: `description is ${fm.description.length} characters; keep it under 1024.` });
29460
+ if (fm.description && !/\b(use (it |this )?(when|for)|when the user)\b/i.test(fm.description) && !new RegExp("\\u5f53\\u7528\\u6237|\\u7528\\u4e8e|\\u89e6\\u53d1").test(fm.description))
29461
+ add({ level: "warn", check: "structure", file: "SKILL.md", line: 3, message: "description does not say when to use the skill.", fix: 'Add "Use when the user wants ..." in the words users type.' });
29462
+ }
29463
+ if (!NAME_RE.test(name) || name.length > 60) add({ level: "warn", check: "structure", file: "", line: 0, message: `folder name "${name}" is not lowercase kebab-case of at most 60 characters; the picker skips it and submit refuses it.`, fix: "Rename the folder, e.g. my-skill." });
29464
+ const lines = raw.split("\n").length;
29465
+ if (lines > 500) add({ level: "warn", check: "structure", file: "SKILL.md", line: 0, message: `SKILL.md is ${lines} lines; long files are read less carefully.`, fix: "Keep it under about 500 lines; move detail into references/ and say when to read each file." });
29466
+ const links = symlinks.filter((p) => p !== "SKILL.md");
29467
+ if (links.length) add({ level: "warn", check: "publish-limits", file: links[0], line: 0, message: `symbolic links are skipped when publishing, so installed copies will not have: ${links.slice(0, 5).join(", ")}${links.length > 5 ? ` (+${links.length - 5} more)` : ""}.`, fix: "Replace them with real files if the skill needs them." });
29468
+ if (files.length > MAX_FILES) add({ level: "error", check: "publish-limits", file: "", line: 0, message: `too many files: ${files.length} (at most ${MAX_FILES}).` });
29469
+ let total = 0;
29470
+ for (const f of files) {
29471
+ total += f.size;
29472
+ if (f.size > MAX_FILE_BYTES) add({ level: "error", check: "publish-limits", file: f.rel, line: 0, message: `${mb(f.size)}; at most ${mb(MAX_FILE_BYTES)} per file.` });
29473
+ }
29474
+ if (total > MAX_TOTAL_BYTES) add({ level: "error", check: "publish-limits", file: "", line: 0, message: `${mb(total)} in total; at most ${mb(MAX_TOTAL_BYTES)}.` });
29475
+ if (existsSync5(join6(dir, SOURCE_FILE))) {
29476
+ let h = "";
29477
+ try {
29478
+ const s = JSON.parse(readFileSync5(join6(dir, SOURCE_FILE), "utf8"));
29479
+ h = s.handle || s.id || "";
29480
+ } catch {
29481
+ }
29482
+ add({ level: "error", check: "installed-copy", file: SOURCE_FILE, line: 0, message: `this folder was installed from ${h || "another skill"}; only skills written in this space can be submitted.`, fix: "To publish your own version, copy it to a new name and make it yours; do not just delete the marker." });
29483
+ }
29484
+ for (const f of files) {
29485
+ if (!f.text) continue;
29486
+ const hit = scanSecret(f.text);
29487
+ if (hit) add({ level: "error", check: "secrets", file: f.rel, line: lineOf(f.text, hit.index), message: `looks like a secret (${hit.kind}); one such file blocks publishing the whole skill.`, fix: "Remove the value (not just the file) and rotate it if it was real." });
29488
+ }
29489
+ if (skill?.text) {
29490
+ checkPaths(dir, files, add);
29491
+ checkWriting(body, bodyStart, add);
29492
+ checkUserPhrases(files, add);
29493
+ checkEnglish(files, fm, body, add);
29494
+ }
29495
+ checkHardcodedUi(files, add);
29496
+ checkMatrix(files, add);
29497
+ checkListing(files, add);
29498
+ const errors = findings.filter((f) => f.level === "error").length;
29499
+ return { dir, ok: errors === 0, errors, warnings: findings.length - errors, findings };
29500
+ }
29501
+ function formatFindings(r) {
29502
+ const order = (f) => f.level === "error" ? 0 : 1;
29503
+ return [...r.findings].sort((a, b) => order(a) - order(b)).map((f) => {
29504
+ const where = f.file ? `${f.file}${f.line ? `:${f.line}` : ""}` : posix.basename(r.dir);
29505
+ return `${f.level === "error" ? "error" : "warn "} [${f.check}] ${where}: ${f.message}${f.fix ? `
29506
+ fix: ${f.fix}` : ""}`;
29507
+ });
29508
+ }
29509
+ async function cmdLint(arg, opts) {
29510
+ const dir = resolveSkillDir(arg);
29511
+ if (!dir) {
29512
+ console.error(arg ? `No skill found at ${arg}, ${SKILLS_DIR}/${arg} or ${DRAFTS_DIR}/${arg} (a skill folder has a SKILL.md).` : "There is no SKILL.md here. Give a skill name or folder: neta-search skills lint <name-or-dir>.");
29513
+ return 2;
29514
+ }
29515
+ const r = lintSkill(dir);
29516
+ if (opts.json) {
29517
+ console.log(JSON.stringify(r));
29518
+ return r.ok ? 0 : 1;
29519
+ }
29520
+ for (const l of formatFindings(r)) console.log(l);
29521
+ console.log(`${r.findings.length ? "\n" : ""}Static check of ${dir}: ${r.errors} error(s), ${r.warnings} warning(s).${r.errors ? " Errors are what the skills site would reject; fix them before submitting." : ""}${r.warnings ? " Warnings are suggestions; they do not block submitting." : ""}`);
29522
+ return r.ok ? 0 : 1;
29523
+ }
29524
+
29525
+ // cli/src/skills/submit.ts
29526
+ var SUBMISSION_PATH = "/v1/skills/submission";
29527
+ function precheck(dir) {
29528
+ const problems = [];
29529
+ const md = join7(dir, "SKILL.md");
29530
+ if (!existsSync6(md)) return ["there is no SKILL.md"];
29531
+ if (lstatSync3(md).isSymbolicLink()) problems.push("SKILL.md is a symbolic link; replace it with the real file");
29532
+ const raw = readFileSync6(md, "utf8");
29533
+ const fm = /^---\n([\s\S]*?)\n---\n/.exec(raw);
29534
+ if (raw.startsWith("\uFEFF")) problems.push("SKILL.md starts with a byte-order mark (BOM); save it as UTF-8 without BOM");
29535
+ else if (!fm && /^---\r?\n[\s\S]*?\r?\n---\r?\n/.test(raw)) problems.push("SKILL.md has Windows line endings (CRLF) around the frontmatter; save it with LF line endings");
29536
+ else if (!fm) problems.push("SKILL.md has no frontmatter (it must start with --- and have name / description)");
29537
+ 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)");
29028
29538
  const files = readSkillFiles(dir);
29029
29539
  if (files.size > MAX_FILES) problems.push(`too many files: ${files.size} (at most ${MAX_FILES})`);
29030
29540
  let total = 0;
29031
29541
  for (const [p, b] of files) {
29032
29542
  total += b.length;
29033
- if (b.length > MAX_FILE_BYTES) problems.push(`${p} is ${mb(b.length)} (at most ${mb(MAX_FILE_BYTES)} per file)`);
29543
+ if (b.length > MAX_FILE_BYTES) problems.push(`${p} is ${mb2(b.length)} (at most ${mb2(MAX_FILE_BYTES)} per file)`);
29034
29544
  }
29035
- if (total > MAX_TOTAL_BYTES) problems.push(`the skill is ${mb(total)} in total (at most ${mb(MAX_TOTAL_BYTES)})`);
29545
+ if (total > MAX_TOTAL_BYTES) problems.push(`the skill is ${mb2(total)} in total (at most ${mb2(MAX_TOTAL_BYTES)})`);
29036
29546
  return problems;
29037
29547
  }
29038
- var mb = (n) => `${(n / (1 << 20)).toFixed(1)} MB`;
29548
+ var mb2 = (n) => `${(n / (1 << 20)).toFixed(1)} MB`;
29039
29549
  function blockLabel(reason) {
29040
29550
  const head = reason.split(":")[0];
29041
29551
  switch (head) {
@@ -29089,9 +29599,9 @@ async function cmdSubmit(name, opts) {
29089
29599
  return 2;
29090
29600
  }
29091
29601
  if (opts.withdraw) return withdraw(env, name, opts);
29092
- const dir = join6(SKILLS_DIR, name);
29093
- if (!existsSync5(join6(dir, "SKILL.md"))) {
29094
- if (existsSync5(join6(DRAFTS_DIR, name, "SKILL.md"))) {
29602
+ const dir = join7(SKILLS_DIR, name);
29603
+ if (!existsSync6(join7(dir, "SKILL.md"))) {
29604
+ if (existsSync6(join7(DRAFTS_DIR, name, "SKILL.md"))) {
29095
29605
  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.`);
29096
29606
  } else {
29097
29607
  console.error(`There is no skill at ${dir}/ (it needs a SKILL.md).`);
@@ -29113,11 +29623,20 @@ async function cmdSubmit(name, opts) {
29113
29623
  for (const p of problems) console.error(` - ${p}`);
29114
29624
  return 1;
29115
29625
  }
29626
+ const lint = lintSkill(dir);
29627
+ if (!lint.ok) {
29628
+ console.error(`Not submitted: the static check found ${lint.errors} problem(s) the skills site would reject:`);
29629
+ for (const l of formatFindings({ ...lint, findings: lint.findings.filter((f) => f.level === "error") })) console.error(` ${l}`);
29630
+ return 1;
29631
+ }
29632
+ if (lint.warnings && !opts.json) {
29633
+ console.error(`Static check: ${lint.warnings} suggestion(s) (not blocking). Run neta-search skills lint ${name} to see them.`);
29634
+ }
29116
29635
  const files = readSkillFiles(dir);
29117
29636
  const digest = digestOf(files);
29118
29637
  const id = ownId(env, name);
29119
29638
  const total = [...files.values()].reduce((a, b) => a + b.length, 0);
29120
- if (!opts.json) console.error(`Checked: ${files.size} files, ${mb(total)}. Submitting ${digest} ...`);
29639
+ if (!opts.json) console.error(`Checked: ${files.size} files, ${mb2(total)}. Submitting ${digest} ...`);
29121
29640
  const sent = await reportSkillEventDetail(env, "use", id, digest, name, { via: "submit", where: "local" });
29122
29641
  if (sent === "unreachable") {
29123
29642
  console.error("Not submitted: could not reach the server. Try again in a moment.");
@@ -29254,15 +29773,168 @@ async function withdraw(env, name, opts) {
29254
29773
  }
29255
29774
  }
29256
29775
 
29776
+ // cli/src/skills/review-feedback.ts
29777
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync3 } from "node:fs";
29778
+ import { join as join8 } from "node:path";
29779
+ var REVIEW_FEEDBACK_PATH = "/v1/skills/review-feedback";
29780
+ var FEEDBACK_DIR = ".neta-search/skill-feedback";
29781
+ var FEEDBACK_NOTE = "README.txt";
29782
+ var RENDERED = ["feedback.md", "fix-pack.md"];
29783
+ function refused(g) {
29784
+ if (g === "unreachable" || g.status < 400 || g.status === 404) return false;
29785
+ console.error(`The server refused the request (HTTP ${g.status}): ${g.body.reason || "no reason given"}.`);
29786
+ return true;
29787
+ }
29788
+ var DIGEST_RE2 = /^sha256:[0-9a-f]{64}$/;
29789
+ async function call(env, method, params) {
29790
+ const q = new URLSearchParams({ space: env.spaceId, turn: env.turnId, ...params });
29791
+ try {
29792
+ const r = await fetch(`${env.apiBase}${REVIEW_FEEDBACK_PATH}?${q}`, { method, signal: AbortSignal.timeout(1e4) });
29793
+ if (r.status >= 500 || r.status !== 200 && r.status !== 404) return r.status >= 500 ? "unreachable" : { status: r.status, body: { state: "", reason: (await r.text()).trim() } };
29794
+ return { status: r.status, body: await r.json() };
29795
+ } catch {
29796
+ return "unreachable";
29797
+ }
29798
+ }
29799
+ var short = (d) => d.replace(/^sha256:/, "").slice(0, 7);
29800
+ async function cmdFeedback(name, opts) {
29801
+ const env = detectEnv();
29802
+ const say = (s) => console.log(s);
29803
+ if (env.anonymous || !env.turnId) {
29804
+ console.error("skills feedback only works inside a Cohub sandbox, in a chat in the space where you wrote the skill: it checks that you are its author.");
29805
+ return 2;
29806
+ }
29807
+ if (name !== void 0 && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
29808
+ console.error(`Skill names are kebab-case directory names (e.g. brawl-creator), got "${name}".`);
29809
+ return 2;
29810
+ }
29811
+ if (opts.digest && !DIGEST_RE2.test(opts.digest)) {
29812
+ console.error("--digest must look like sha256:<64 lowercase hex>.");
29813
+ return 2;
29814
+ }
29815
+ let got = await call(env, "POST", { actor: env.actorId });
29816
+ if (got === "unreachable") {
29817
+ console.error("Could not reach the server. Try again in a moment.");
29818
+ return 4;
29819
+ }
29820
+ if (refused(got)) return 2;
29821
+ const deadline = Date.now() + (opts.wait ?? 30) * 1e3;
29822
+ while (got !== "unreachable" && got.body.state === "queued" && Date.now() < deadline) {
29823
+ await new Promise((r) => setTimeout(r, opts.pollMs ?? 1e3));
29824
+ got = await call(env, "GET", {});
29825
+ }
29826
+ if (got === "unreachable") {
29827
+ console.error("Could not reach the server. Try again in a moment.");
29828
+ return 4;
29829
+ }
29830
+ if (refused(got)) return 2;
29831
+ const st = got.body.state;
29832
+ if (st === "queued" || st === "") {
29833
+ console.error("Still checking that this chat is yours. Run the same command again in a moment.");
29834
+ return 3;
29835
+ }
29836
+ if (st === "not_applicable") {
29837
+ console.error(`Review feedback is not available here: ${got.body.reason || "skills in this space are reviewed and fixed by the Neta team"}.`);
29838
+ return 1;
29839
+ }
29840
+ if (st === "rejected") {
29841
+ console.error(`Could not confirm you are the author: ${got.body.reason || "rejected"}. Only the owner of this space can read its skills' review feedback, from a chat they started here.`);
29842
+ return 1;
29843
+ }
29844
+ if (name === void 0) {
29845
+ const list = await call(env, "GET", {});
29846
+ if (list === "unreachable") {
29847
+ console.error("Could not reach the server. Try again in a moment.");
29848
+ return 4;
29849
+ }
29850
+ if (refused(list)) return 2;
29851
+ const skills2 = list.body.skills ?? [];
29852
+ if (opts.json) {
29853
+ say(JSON.stringify({ skills: skills2.map(({ feedback: _f, ...s }) => s) }));
29854
+ return 0;
29855
+ }
29856
+ if (!skills2.length) {
29857
+ say("None of the skills in this space has review feedback from Neta. Neta reviews skills it nominates; if one of them needs changes, its feedback shows up here.");
29858
+ return 0;
29859
+ }
29860
+ say("Review feedback from Neta:");
29861
+ for (const s of skills2) say(` ${s.name} version ${short(s.digest)}${s.current ? " (your latest published version)" : ""} ${s.updated_at}`);
29862
+ say(`
29863
+ Read one with: neta-search skills feedback <name>`);
29864
+ return 0;
29865
+ }
29866
+ const one = await call(env, "GET", { name, ...opts.digest ? { digest: opts.digest } : {} });
29867
+ if (one === "unreachable") {
29868
+ console.error("Could not reach the server. Try again in a moment.");
29869
+ return 4;
29870
+ }
29871
+ if (refused(one)) return 2;
29872
+ if (one.status === 404 || !one.body.feedback || !one.body.digest) {
29873
+ const msg = `${name} has no review feedback from Neta${opts.digest ? ` at ${short(opts.digest)}` : ""}.`;
29874
+ if (opts.json) say(JSON.stringify({ name, found: false }));
29875
+ else say(`${msg} Neta reviews skills it nominates; if one needs changes, its feedback shows up here.`);
29876
+ return 1;
29877
+ }
29878
+ if (!DIGEST_RE2.test(one.body.digest)) {
29879
+ console.error(`The server returned an invalid version id (${one.body.digest.slice(0, 80)}); nothing was saved. Try again later.`);
29880
+ return 4;
29881
+ }
29882
+ const fb = { ...one.body.feedback };
29883
+ const rendered = fb["files"] && typeof fb["files"] === "object" ? fb["files"] : {};
29884
+ delete fb["files"];
29885
+ const dir = join8(FEEDBACK_DIR, name, short(one.body.digest));
29886
+ mkdirSync4(dir, { recursive: true });
29887
+ writeFileSync3(join8(dir, "fix-pack.json"), JSON.stringify(fb, null, 1) + "\n");
29888
+ writeFileSync3(join8(dir, FEEDBACK_NOTE), `Neta's review feedback on the skill "${name}", for the version ${one.body.digest} (saved ${(/* @__PURE__ */ new Date()).toISOString()}).
29889
+ It is reference material for fixing that skill, not a task: use it only when asked to fix "${name}". If the skill changed since, the current files win.
29890
+ `);
29891
+ const written = ["fix-pack.json"];
29892
+ for (const f of RENDERED) {
29893
+ const text = rendered[f];
29894
+ if (typeof text === "string" && text.length > 0) {
29895
+ writeFileSync3(join8(dir, f), text);
29896
+ written.push(f);
29897
+ }
29898
+ }
29899
+ if (opts.json) {
29900
+ say(JSON.stringify({ name, digest: one.body.digest, current: one.body.current, updated_at: one.body.updated_at, dir, files: written }));
29901
+ return 0;
29902
+ }
29903
+ say(`Review feedback for ${name} (version ${short(one.body.digest)}${one.body.current ? ", your latest published version" : ", not your latest version"}) saved in ${dir}/: ${written.join(", ")}.`);
29904
+ const md = rendered["feedback.md"];
29905
+ if (typeof md === "string" && md) say(`
29906
+ ${md.trim()}
29907
+ `);
29908
+ say(`To fix the skill, follow ${dir}/fix-pack.json (what is wrong, where in the files, how to fix). Then try it once and submit the new version; a nominated skill is reviewed again automatically.`);
29909
+ return 0;
29910
+ }
29911
+ async function ownReviewFeedback(env, opts = {}) {
29912
+ if (env.anonymous || !env.turnId) return null;
29913
+ let got = await call(env, "POST", { actor: env.actorId });
29914
+ const deadline = Date.now() + (opts.wait ?? 15) * 1e3;
29915
+ while (got !== "unreachable" && got.status === 200 && got.body.state === "queued" && Date.now() < deadline) {
29916
+ await new Promise((r) => setTimeout(r, opts.pollMs ?? 1e3));
29917
+ got = await call(env, "GET", {});
29918
+ }
29919
+ if (got === "unreachable" || got.status !== 200 || got.body.state !== "granted") return null;
29920
+ const list = got.body.skills ? got : await call(env, "GET", {});
29921
+ if (list === "unreachable" || list.status !== 200 || !list.body.skills) return null;
29922
+ const out = /* @__PURE__ */ new Map();
29923
+ for (const s of list.body.skills) {
29924
+ if (!out.has(s.name) && DIGEST_RE2.test(s.digest)) out.set(s.name, { digest: s.digest, current: s.current, updated_at: s.updated_at });
29925
+ }
29926
+ return out;
29927
+ }
29928
+
29257
29929
  // cli/src/skills/manage.ts
29258
- import { existsSync as existsSync6, readdirSync as readdirSync4, rmSync as rmSync2 } from "node:fs";
29259
- import { join as join7 } from "node:path";
29930
+ import { existsSync as existsSync7, readdirSync as readdirSync4, rmSync as rmSync2 } from "node:fs";
29931
+ import { join as join9 } from "node:path";
29260
29932
  function scanLocal(dir) {
29261
- if (!existsSync6(dir)) return [];
29933
+ if (!existsSync7(dir)) return [];
29262
29934
  const out = [];
29263
29935
  for (const e of readdirSync4(dir, { withFileTypes: true })) {
29264
29936
  if (!e.isDirectory() || e.name.startsWith(".")) continue;
29265
- const path = join7(dir, e.name);
29937
+ const path = join9(dir, e.name);
29266
29938
  if (!skillDirExists(path)) continue;
29267
29939
  const src = readSource(path);
29268
29940
  if (src) {
@@ -29298,14 +29970,17 @@ var STATUS_LABEL = { latest: "latest", outdated: "update available", unavailable
29298
29970
  async function cmdList(opts) {
29299
29971
  const env = detectEnv();
29300
29972
  const local = scanLocal(opts.dir);
29973
+ const feedback = !opts.offline && local.some((s) => s.kind === "own") ? ownReviewFeedback(env, { pollMs: opts.pollMs }) : Promise.resolve(null);
29301
29974
  const rows = await Promise.all(
29302
29975
  local.map(async (s) => {
29303
29976
  const st = opts.offline ? { status: "unknown", handle: void 0 } : await statusOf(env, s);
29304
29977
  return { ...s, ...st, handle: s.handle ?? st.handle };
29305
29978
  })
29306
29979
  );
29980
+ const fb = await feedback;
29981
+ const withFeedback = rows.map((r) => r.kind === "own" && fb?.has(r.name) ? { ...r, review_feedback: fb.get(r.name) } : r);
29307
29982
  if (opts.json) {
29308
- console.log(JSON.stringify({ dir: opts.dir, count: rows.length, skills: rows }));
29983
+ console.log(JSON.stringify({ dir: opts.dir, count: rows.length, skills: withFeedback }));
29309
29984
  return 0;
29310
29985
  }
29311
29986
  if (rows.length === 0) {
@@ -29313,14 +29988,21 @@ async function cmdList(opts) {
29313
29988
  return 0;
29314
29989
  }
29315
29990
  console.log(`${opts.dir}/ (${rows.length}):`);
29316
- for (const r of rows) {
29991
+ for (const r of withFeedback) {
29317
29992
  const st = STATUS_LABEL[r.status] ? ` [${STATUS_LABEL[r.status]}]` : "";
29318
29993
  const from = r.id ? ` ${displayRef(r)}` : "";
29319
- console.log(` ${r.name.padEnd(28)} ${KIND_LABEL[r.kind]}${st}${from}`);
29994
+ const rf = "review_feedback" in r ? " [Neta review feedback]" : "";
29995
+ console.log(` ${r.name.padEnd(28)} ${KIND_LABEL[r.kind]}${st}${from}${rf}`);
29320
29996
  }
29321
29997
  const outdated = rows.filter((r) => r.status === "outdated").length;
29322
29998
  if (outdated > 0) console.log(`
29323
29999
  ${outdated} with updates available: neta-search skills update`);
30000
+ const reviewed = withFeedback.filter((r) => "review_feedback" in r).map((r) => r.name);
30001
+ if (reviewed.length > 0) {
30002
+ console.log(`
30003
+ Neta reviewed ${reviewed.length === 1 ? "this skill" : "these skills"} and left feedback on what to fix: ${reviewed.join(", ")}.`);
30004
+ console.log(`Read it with: neta-search skills feedback ${reviewed.length === 1 ? reviewed[0] : "<name>"}`);
30005
+ }
29324
30006
  return 0;
29325
30007
  }
29326
30008
  async function cmdUpdate(names, opts) {
@@ -29378,7 +30060,7 @@ async function cmdUpdate(names, opts) {
29378
30060
  }
29379
30061
  if (results.length === 0) console.log(`No installed skills in ${opts.dir}/; nothing to update.`);
29380
30062
  const say = {
29381
- updated: (r) => `Updated ${r.name} (${short(r.from)} \u2192 ${short(r.to)})`,
30063
+ updated: (r) => `Updated ${r.name} (${short2(r.from)} \u2192 ${short2(r.to)})`,
29382
30064
  latest: (r) => `${r.name} is up to date`,
29383
30065
  unavailable: (r) => `${r.name} is no longer available from its source; keeping the local copy`,
29384
30066
  own_skipped: (r) => `${r.name} is your own skill; nothing to update`,
@@ -29389,7 +30071,7 @@ async function cmdUpdate(names, opts) {
29389
30071
  for (const r of results) console.log(say[r.result](r));
29390
30072
  return failed > 0 ? 1 : 0;
29391
30073
  }
29392
- function short(d) {
30074
+ function short2(d) {
29393
30075
  return (d ?? "").replace(/^sha256:/, "").slice(0, 12) || "?";
29394
30076
  }
29395
30077
  async function cmdRemove(names, opts) {
@@ -29397,7 +30079,7 @@ async function cmdRemove(names, opts) {
29397
30079
  const local = scanLocal(opts.dir);
29398
30080
  const results = [];
29399
30081
  for (const n of names) {
29400
- const found = (await findLocal(env, local, n)).filter((s) => existsSync6(s.path));
30082
+ const found = (await findLocal(env, local, n)).filter((s) => existsSync7(s.path));
29401
30083
  if (found.length === 0) results.push({ name: n, result: "not_found" });
29402
30084
  for (const s of found) {
29403
30085
  if (s.kind === "own" && !opts.force) {
@@ -29412,8 +30094,8 @@ async function cmdRemove(names, opts) {
29412
30094
  if (opts.json) console.log(JSON.stringify({ results }));
29413
30095
  else
29414
30096
  for (const r of results) {
29415
- if (r.result === "removed") console.log(`Removed ${join7(opts.dir, r.name)}`);
29416
- else if (r.result === "not_found") console.error(`${join7(opts.dir, r.name)} does not exist`);
30097
+ if (r.result === "removed") console.log(`Removed ${join9(opts.dir, r.name)}`);
30098
+ else if (r.result === "not_found") console.error(`${join9(opts.dir, r.name)} does not exist`);
29417
30099
  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.`);
29418
30100
  }
29419
30101
  return failed > 0 ? 1 : 0;
@@ -29422,7 +30104,7 @@ async function cmdRemove(names, opts) {
29422
30104
  // cli/src/cli.ts
29423
30105
  var program2 = new Command();
29424
30106
  program2.name("neta-search").description("neta-search: search the Cohub skill ecosystem").version(cliVersion());
29425
- var skills = program2.command("skills").description("skills: find / add / use / list / update / remove / init / submit");
30107
+ var skills = program2.command("skills").description("skills: find / add / use / list / update / remove / init / lint / submit / feedback");
29426
30108
  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) => {
29427
30109
  await run(cmdFind(query, { limit: Number(opts.limit) || 20, json: opts.json }));
29428
30110
  });
@@ -29432,10 +30114,16 @@ skills.command("add").description("install a skill: an id or handle from find do
29432
30114
  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()).option("--verified", "use the version that passed Neta's review; fails if there is none (never falls back to the latest); needs a handle or id").action(async (idOrName, opts) => {
29433
30115
  await run(cmdUse(idOrName, opts));
29434
30116
  });
30117
+ skills.command("lint").description("static check of a skill folder before you submit it: errors are what the skills site would reject; warnings are suggestions (paths, test declarations, common writing problems). Runs locally, sends nothing").argument("[name-or-dir]", "skill name (.agents/skills/<name> or your drafts) or a folder; default: the current folder").option("--json", "compact JSON output (for agents)").action(async (arg, opts) => {
30118
+ await run(cmdLint(arg, { json: !!opts.json }));
30119
+ });
29435
30120
  skills.command("submit").description("publish the current version of one of your skills (.agents/skills/<name>/ in this space) on the public skills site and wait for the result; only the space owner can publish. --withdraw takes it down").argument("<name>", "skill directory name under .agents/skills/").option("--withdraw", "take a published skill down (deleting the directory does not)").option("--wait <seconds>", "how long to wait for the server's result", "60").option("--json", "compact JSON output (for agents)").action(async (name, opts) => {
29436
30121
  const wait = Number(opts.wait);
29437
30122
  await run(cmdSubmit(name, { json: !!opts.json, withdraw: !!opts.withdraw, wait: Number.isFinite(wait) && wait >= 0 ? wait : 60 }));
29438
30123
  });
30124
+ skills.command("feedback").description("read Neta's review feedback on your own skills (only the space owner, from a chat in this space). No name: list the skills that have feedback; a name: save the latest feedback to .neta-search/skill-feedback/<name>/ and print it").argument("[name]", "skill directory name").option("--digest <sha256>", "a specific version (sha256:<hex>) instead of the latest one with feedback").option("--json", "compact JSON output (for agents)").action(async (name, opts) => {
30125
+ await run(cmdFeedback(name, { json: !!opts.json, digest: opts.digest }));
30126
+ });
29439
30127
  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) => {
29440
30128
  await run(cmdList({ dir: opts.dir, offline: !!opts.offline, json: !!opts.json }));
29441
30129
  });