@arp-protocol/cli 0.0.3 → 0.0.5

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/dist/hook.js CHANGED
@@ -14,9 +14,42 @@ function appendOwn(path, text) {
14
14
  }
15
15
 
16
16
  // ../hook/src/hook.ts
17
- import { existsSync as existsSync3, lstatSync as lstatSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
17
+ import { existsSync as existsSync4, lstatSync as lstatSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
18
18
  import { homedir as homedir3 } from "node:os";
19
- import { dirname as dirname4, join as join5 } from "node:path";
19
+ import { dirname as dirname4, join as join6 } from "node:path";
20
+ // ../../packages/protocol/dist/declare/tool-thing.js
21
+ class InvalidToolUrlError extends Error {
22
+ input;
23
+ constructor(input, why) {
24
+ super(`${JSON.stringify(input)} is not a usable tool URL: ${why}`);
25
+ this.name = "InvalidToolUrlError";
26
+ this.input = input;
27
+ }
28
+ }
29
+ function normaliseToolUrl(input) {
30
+ const trimmed = input.trim();
31
+ if (trimmed.length === 0)
32
+ throw new InvalidToolUrlError(input, "empty");
33
+ const withScheme = /^[a-z][a-z0-9+.-]*:/i.test(trimmed) ? trimmed : `https://${trimmed}`;
34
+ let url;
35
+ try {
36
+ url = new URL(withScheme);
37
+ } catch {
38
+ throw new InvalidToolUrlError(input, "not parseable as a URL");
39
+ }
40
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
41
+ throw new InvalidToolUrlError(input, `scheme ${url.protocol} is not http or https`);
42
+ }
43
+ if (url.hostname.length === 0)
44
+ throw new InvalidToolUrlError(input, "no host");
45
+ const host = url.hostname.replace(/\.+$/, "").replace(/^(?:www\.)+/, "");
46
+ const path = url.pathname.replace(/(?:\.git|\/)+$/, "");
47
+ const params = [...new URLSearchParams(url.search).entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
48
+ const query = params.length === 0 ? "" : `?${new URLSearchParams(params).toString()}`;
49
+ const port = url.port === "" || url.port === "80" || url.port === "443" ? "" : `:${url.port}`;
50
+ return `https://${host}${port}${path}${query}`;
51
+ }
52
+
20
53
  // ../../packages/graphql/dist/http.js
21
54
  var DEFAULT_TIMEOUT_MS = 1e4;
22
55
  var DEFAULT_MAX_BODY_BYTES = 8 * 1024 * 1024;
@@ -504,38 +537,6 @@ var X402_REASONS_QUERY = `query X402Reasons($ids: [String!]!, $because: String!,
504
537
  ${LEVEL}
505
538
  }
506
539
  }`;
507
- // ../../packages/protocol/dist/declare/tool-thing.js
508
- class InvalidToolUrlError extends Error {
509
- input;
510
- constructor(input, why) {
511
- super(`${JSON.stringify(input)} is not a usable tool URL: ${why}`);
512
- this.name = "InvalidToolUrlError";
513
- this.input = input;
514
- }
515
- }
516
- function normaliseToolUrl(input) {
517
- const trimmed = input.trim();
518
- if (trimmed.length === 0)
519
- throw new InvalidToolUrlError(input, "empty");
520
- const withScheme = /^[a-z][a-z0-9+.-]*:/i.test(trimmed) ? trimmed : `https://${trimmed}`;
521
- let url;
522
- try {
523
- url = new URL(withScheme);
524
- } catch {
525
- throw new InvalidToolUrlError(input, "not parseable as a URL");
526
- }
527
- if (url.protocol !== "http:" && url.protocol !== "https:") {
528
- throw new InvalidToolUrlError(input, `scheme ${url.protocol} is not http or https`);
529
- }
530
- if (url.hostname.length === 0)
531
- throw new InvalidToolUrlError(input, "no host");
532
- const host = url.hostname.replace(/\.+$/, "").replace(/^(?:www\.)+/, "");
533
- const path = url.pathname.replace(/(?:\.git|\/)+$/, "");
534
- const params = [...new URLSearchParams(url.search).entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
535
- const query = params.length === 0 ? "" : `?${new URLSearchParams(params).toString()}`;
536
- const port = url.port === "" || url.port === "80" || url.port === "443" ? "" : `:${url.port}`;
537
- return `https://${host}${port}${path}${query}`;
538
- }
539
540
  // ../../packages/protocol/dist/session.js
540
541
  function refForInvocation(name, source, fetched) {
541
542
  const bare = name.includes(":") ? name.split(":").pop() ?? name : name;
@@ -609,9 +610,45 @@ var COMMON = new Set(("awk bash break bun case cat cd chmod command continue cp
609
610
  var INSTALL = /\b(?:npm\s+(?:i|install)\s+(?:-g|--global)|pnpm\s+add\s+-g|yarn\s+global\s+add|brew\s+install|pipx\s+install|pip3?\s+install|cargo\s+install)\s+([@a-z0-9][@\w./-]*)/i;
610
611
  var PROBE = /\b(?:command\s+-v|which)\s+([a-z][\w-]*)/i;
611
612
  var COMMAND = /^[a-z][a-z0-9_-]*$/;
613
+ function shellSegments(line) {
614
+ const segments = [];
615
+ let current = "";
616
+ let quote = null;
617
+ for (let index = 0;index < line.length; index += 1) {
618
+ const char = line[index] ?? "";
619
+ if (char === "\\" && quote !== "'") {
620
+ current += char + (line[index + 1] ?? "");
621
+ index += 1;
622
+ continue;
623
+ }
624
+ if (quote !== null) {
625
+ if (char === quote)
626
+ quote = null;
627
+ current += char;
628
+ continue;
629
+ }
630
+ if (char === '"' || char === "'") {
631
+ quote = char;
632
+ current += char;
633
+ continue;
634
+ }
635
+ if (char === "#" && (index === 0 || /\s/.test(line[index - 1] ?? "")))
636
+ break;
637
+ if (char === "|" || char === ";" || char === "&" && line[index + 1] === "&") {
638
+ segments.push(current);
639
+ current = "";
640
+ if (line[index + 1] === char)
641
+ index += 1;
642
+ continue;
643
+ }
644
+ current += char;
645
+ }
646
+ segments.push(current);
647
+ return segments;
648
+ }
612
649
  function commandsIn(shell, explicitOnly) {
613
650
  const found = [];
614
- for (const segment of shell.split(/&&|\|\||[|;]/)) {
651
+ for (const segment of shellSegments(shell)) {
615
652
  const line = segment.trim().replace(/^\$\s+/, "");
616
653
  if (line === "" || line.startsWith("#"))
617
654
  continue;
@@ -646,7 +683,7 @@ function shellLinesRun(lines) {
646
683
  run.push(line);
647
684
  continued = line.trimEnd().endsWith("\\");
648
685
  heredocEnd = /<<-?\s*['"]?([A-Za-z_]+)/.exec(line)?.[1] ?? null;
649
- for (const char of line) {
686
+ for (const char of quote === null ? shellSegments(line).join(";") : line) {
650
687
  if (quote === null && (char === '"' || char === "'"))
651
688
  quote = char;
652
689
  else if (char === quote)
@@ -749,12 +786,21 @@ function installedFromReceipts(text) {
749
786
  return found;
750
787
  }
751
788
  // ../hook/src/classify.ts
789
+ function isFailure(outcome) {
790
+ return outcome !== "ok" && outcome !== "payment_required";
791
+ }
752
792
  var CANCELLED = /\b(interrupted|cancel(l)?ed|aborted) by (the )?user\b|\buser (rejected|denied|declined)\b|\bpermission denied by user\b|\buser doesn't want to (proceed|take this action)\b/i;
753
793
  var AUTH = /\b(unauthori[sz]ed|forbidden|invalid (api )?(key|token)|authentication|not authenticated|expired token)\b/i;
754
794
  var TIMEOUT = /\b(timed? ?out|timeout|deadline exceeded|ETIMEDOUT)\b/i;
755
795
  var UNREACHABLE = /\b(ENOTFOUND|EAI_AGAIN|ECONNREFUSED|ECONNRESET|EHOSTUNREACH|ENETUNREACH|getaddrinfo|certificate|TLS|SSL|socket hang up|connection refused|fetch failed)\b/i;
756
- var PAYMENT = /\b(payment required|insufficient (funds|balance)|payment (failed|refused|rejected)|x402)\b/i;
757
- var STATUS = /\b(40[123]|429|5\d\d)\b/;
796
+ var CHALLENGE = /\bpayment required\b|"x402Version"[\s\S]{0,2000}"accepts"|"accepts"[\s\S]{0,2000}"x402Version"/i;
797
+ var REFUSED = /\binsufficient[ _](funds|balance)\b|\bpayment (failed|refused|rejected|declined|invalid)\b|\bsettlement failed\b/i;
798
+ var REFUSED_IN_X402 = /"success"\s*:\s*false|"errorReason"\s*:\s*"[^"]+"|\binvalid_[a-z_]*(signature|payload|payment)[a-z_]*\b/i;
799
+ var ABOUT_PAYMENT = /\bx402|\bpayment\b/i;
800
+ var TRANSACTION = /^(0x[0-9a-fA-F]{64}|[1-9A-HJ-NP-Za-km-z]{64,90})$/;
801
+ var FLAT_OBJECT = /\{[^{}]{0,2000}\}/g;
802
+ var RESPONSE_HEADER = /\b(?:x-)?payment-response\s*[:=]\s*"?([A-Za-z0-9+/=_-]{16,4000})/gi;
803
+ var STATUS = /\b(40[123]|429|5\d\d)\b/g;
758
804
  function isRecord2(value) {
759
805
  return typeof value === "object" && value !== null && !Array.isArray(value);
760
806
  }
@@ -795,21 +841,96 @@ function statusOf(call) {
795
841
  function flaggedError(response) {
796
842
  return isRecord2(response) && (response.isError === true || response.is_error === true);
797
843
  }
844
+ function isSettled(value) {
845
+ return isRecord2(value) && value.success === true && typeof value.network === "string" && typeof value.transaction === "string" && TRANSACTION.test(value.transaction);
846
+ }
847
+ function stringsOf(value, depth = 0, out = []) {
848
+ if (typeof value === "string")
849
+ out.push(value);
850
+ else if (depth < 4 && Array.isArray(value))
851
+ for (const item of value)
852
+ stringsOf(item, depth + 1, out);
853
+ else if (depth < 4 && isRecord2(value))
854
+ for (const item of Object.values(value))
855
+ stringsOf(item, depth + 1, out);
856
+ return out;
857
+ }
858
+ function isUnsettled(value) {
859
+ return isRecord2(value) && value.success === false && typeof value.network === "string";
860
+ }
861
+ function anyIn(value, test, depth = 0) {
862
+ if (test(value))
863
+ return true;
864
+ if (depth >= 4)
865
+ return false;
866
+ if (Array.isArray(value))
867
+ return value.some((item) => anyIn(item, test, depth + 1));
868
+ if (isRecord2(value))
869
+ return Object.values(value).some((item) => anyIn(item, test, depth + 1));
870
+ return false;
871
+ }
872
+ function parsed(text) {
873
+ try {
874
+ return JSON.parse(text);
875
+ } catch {
876
+ return null;
877
+ }
878
+ }
879
+ function decoded(base64) {
880
+ try {
881
+ return Buffer.from(base64.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8");
882
+ } catch {
883
+ return "";
884
+ }
885
+ }
886
+ function settledInText(text) {
887
+ for (const [object] of text.matchAll(FLAT_OBJECT))
888
+ if (isSettled(parsed(object)))
889
+ return true;
890
+ for (const match of text.matchAll(RESPONSE_HEADER))
891
+ if (isSettled(parsed(decoded(match[1] ?? ""))))
892
+ return true;
893
+ return false;
894
+ }
895
+ function settlementIn(call) {
896
+ if (anyIn(call.response, isSettled))
897
+ return true;
898
+ const texts = [...call.error === null ? [] : [call.error], ...stringsOf(call.response)];
899
+ return texts.some(settledInText);
900
+ }
901
+ function emptyContent(response) {
902
+ const blocks = Array.isArray(response) ? response : isRecord2(response) && Array.isArray(response.content) ? response.content : null;
903
+ if (typeof response === "string")
904
+ return response.trim().length === 0;
905
+ if (blocks === null)
906
+ return false;
907
+ return blocks.every((block) => !isRecord2(block) || typeof block.text !== "string" || block.text.trim().length === 0 || isSettled(parsed(block.text)));
908
+ }
798
909
  function classifyCall(call) {
799
910
  if (call.interrupted)
800
911
  return null;
801
912
  const failed = call.event === "PostToolUseFailure" || call.error !== null || flaggedError(call.response);
802
913
  const status = statusOf(call);
803
- if (!failed && (status === null || status < 400))
804
- return { outcome: "ok", code: null };
914
+ if (!failed && (status === null || status < 400)) {
915
+ return settlementIn(call) && emptyContent(call.response) ? { outcome: "paid_no_delivery", code: null } : { outcome: "ok", code: null };
916
+ }
805
917
  const text = failureText(call);
806
918
  if (CANCELLED.test(text))
807
919
  return null;
808
- const found = STATUS.exec(text);
809
- const match = status ?? (found === null ? null : Number(found[0]));
920
+ const found = [...text.matchAll(STATUS)].map((one) => Number(one[0]));
921
+ const after = status !== null && status !== 402 ? status : found.find((one) => one !== 402) ?? null;
922
+ const challenge = status === 402 || status === null && (found.includes(402) || CHALLENGE.test(text));
923
+ const match = after ?? (challenge ? 402 : null);
810
924
  const code = match === null ? null : String(match);
811
- if (match === 402 || PAYMENT.test(text))
812
- return { outcome: "payment", code };
925
+ if (settlementIn(call))
926
+ return { outcome: "paid_no_delivery", code: after === null ? null : String(after) };
927
+ const refused = anyIn(call.response, isUnsettled) || REFUSED.test(text) || (challenge || ABOUT_PAYMENT.test(text)) && REFUSED_IN_X402.test(text);
928
+ if (refused) {
929
+ return { outcome: "payment_refused", code };
930
+ }
931
+ if (challenge && after === null && !TIMEOUT.test(text) && !UNREACHABLE.test(text)) {
932
+ return { outcome: "payment_required", code };
933
+ }
813
934
  if (match === 401 || match === 403 || AUTH.test(text))
814
935
  return { outcome: "auth", code };
815
936
  if (TIMEOUT.test(text))
@@ -1041,18 +1162,18 @@ function toolCallOf(raw) {
1041
1162
 
1042
1163
  // ../../packages/protocol/src/content-path.ts
1043
1164
  function skillRefFromUrl3(url) {
1044
- let parsed;
1165
+ let parsed2;
1045
1166
  try {
1046
- parsed = new URL(url.includes("://") ? url : `https://${url}`);
1167
+ parsed2 = new URL(url.includes("://") ? url : `https://${url}`);
1047
1168
  } catch {
1048
1169
  return null;
1049
1170
  }
1050
- if (parsed.hostname.replace(/^www\./, "") !== "github.com")
1171
+ if (parsed2.hostname.replace(/^www\./, "") !== "github.com")
1051
1172
  return null;
1052
- const skill = parsed.searchParams.get("skill");
1173
+ const skill = parsed2.searchParams.get("skill");
1053
1174
  if (skill === null || skill.length === 0)
1054
1175
  return null;
1055
- const [owner, repo] = parsed.pathname.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/, "").split("/");
1176
+ const [owner, repo] = parsed2.pathname.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/, "").split("/");
1056
1177
  if (owner === undefined || repo === undefined)
1057
1178
  return null;
1058
1179
  if (owner.length === 0 || repo.length === 0)
@@ -1122,9 +1243,45 @@ var COMMON2 = new Set(("awk bash break bun case cat cd chmod command continue cp
1122
1243
  var INSTALL2 = /\b(?:npm\s+(?:i|install)\s+(?:-g|--global)|pnpm\s+add\s+-g|yarn\s+global\s+add|brew\s+install|pipx\s+install|pip3?\s+install|cargo\s+install)\s+([@a-z0-9][@\w./-]*)/i;
1123
1244
  var PROBE2 = /\b(?:command\s+-v|which)\s+([a-z][\w-]*)/i;
1124
1245
  var COMMAND2 = /^[a-z][a-z0-9_-]*$/;
1246
+ function shellSegments2(line) {
1247
+ const segments = [];
1248
+ let current = "";
1249
+ let quote = null;
1250
+ for (let index = 0;index < line.length; index += 1) {
1251
+ const char = line[index] ?? "";
1252
+ if (char === "\\" && quote !== "'") {
1253
+ current += char + (line[index + 1] ?? "");
1254
+ index += 1;
1255
+ continue;
1256
+ }
1257
+ if (quote !== null) {
1258
+ if (char === quote)
1259
+ quote = null;
1260
+ current += char;
1261
+ continue;
1262
+ }
1263
+ if (char === '"' || char === "'") {
1264
+ quote = char;
1265
+ current += char;
1266
+ continue;
1267
+ }
1268
+ if (char === "#" && (index === 0 || /\s/.test(line[index - 1] ?? "")))
1269
+ break;
1270
+ if (char === "|" || char === ";" || char === "&" && line[index + 1] === "&") {
1271
+ segments.push(current);
1272
+ current = "";
1273
+ if (line[index + 1] === char)
1274
+ index += 1;
1275
+ continue;
1276
+ }
1277
+ current += char;
1278
+ }
1279
+ segments.push(current);
1280
+ return segments;
1281
+ }
1125
1282
  function commandsIn2(shell, explicitOnly) {
1126
1283
  const found = [];
1127
- for (const segment of shell.split(/&&|\|\||[|;]/)) {
1284
+ for (const segment of shellSegments2(shell)) {
1128
1285
  const line = segment.trim().replace(/^\$\s+/, "");
1129
1286
  if (line === "" || line.startsWith("#"))
1130
1287
  continue;
@@ -1159,7 +1316,7 @@ function shellLinesRun2(lines) {
1159
1316
  run.push(line);
1160
1317
  continued = line.trimEnd().endsWith("\\");
1161
1318
  heredocEnd = /<<-?\s*['"]?([A-Za-z_]+)/.exec(line)?.[1] ?? null;
1162
- for (const char of line) {
1319
+ for (const char of quote === null ? shellSegments2(line).join(";") : line) {
1163
1320
  if (quote === null && (char === '"' || char === "'"))
1164
1321
  quote = char;
1165
1322
  else if (char === quote)
@@ -1212,6 +1369,223 @@ function readRequires2(markdown) {
1212
1369
  return { cli, runs, login: null, description };
1213
1370
  }
1214
1371
 
1372
+ // ../../packages/protocol/src/skill-needs.ts
1373
+ var MARKDOWN = /\.(md|mdx|markdown)$/i;
1374
+ var SCRIPT = /\.(sh|bash|zsh|py|js|mjs|cjs|ts|rb|pl|ps1|php|lua)$/i;
1375
+ var COMMENT_LINE = /^\s*(?:#|\/\/|\*|--|;|rem\s)/i;
1376
+ function isTestFile2(path) {
1377
+ return /(^|\/)(tests?|__tests__|spec|fixtures?)\/|\.(test|spec)\.[a-z]+$|(^|\/)test_[^/]+\.py$|_test\.(py|go|rb)$/i.test(path);
1378
+ }
1379
+ function isSecretName2(name) {
1380
+ return /(^|_)(API_?KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|PASS|CREDENTIALS|CREDS)$/.test(name);
1381
+ }
1382
+ var SET_IN_SKILL_MD = /(?:^|\s|`)(?:export\s+|set\s+|\$env:)?([A-Z][A-Z0-9_]{2,})=/g;
1383
+ function lineAt(text, index) {
1384
+ let line = 1;
1385
+ for (let at = text.indexOf(`
1386
+ `);at !== -1 && at < index; at = text.indexOf(`
1387
+ `, at + 1))
1388
+ line += 1;
1389
+ return line;
1390
+ }
1391
+ function keysIn2(files, settings) {
1392
+ const skillMd = files.find((file) => file.path === "SKILL.md");
1393
+ const toSet = new Set;
1394
+ if (skillMd !== undefined)
1395
+ for (const match of skillMd.text.matchAll(SET_IN_SKILL_MD))
1396
+ toSet.add(match[1] ?? "");
1397
+ const wanted = new Set(settings.filter((name) => isSecretName2(name) || toSet.has(name)));
1398
+ const ordered = [
1399
+ ...skillMd === undefined ? [] : [skillMd],
1400
+ ...files.filter((file) => file.path !== "SKILL.md" && !isTestFile2(file.path))
1401
+ ];
1402
+ const found = new Map;
1403
+ for (const file of ordered) {
1404
+ for (const [index, line] of file.text.split(`
1405
+ `).entries()) {
1406
+ for (const match of line.matchAll(/\b[A-Z][A-Z0-9_]{2,}\b/g)) {
1407
+ const name = match[0];
1408
+ if (!wanted.has(name) || found.has(name))
1409
+ continue;
1410
+ found.set(name, { name, why: isSecretName2(name) ? "secret" : "set in SKILL.md", file: file.path, line: index + 1 });
1411
+ }
1412
+ }
1413
+ }
1414
+ return [...found.values()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
1415
+ }
1416
+ var EVERYWHERE = new Set("sh bash zsh dash env cat ls echo printf mkdir rm cp mv ln grep sed awk tr cut head tail sort uniq wc find xargs test true false base64 date sleep chmod touch tee kill ps uname which open xdg-open cmd powershell pwsh taskkill taskkill.exe where where.exe".split(" "));
1417
+ var ALIASES = {
1418
+ soffice: "LibreOffice",
1419
+ libreoffice: "LibreOffice",
1420
+ "google-chrome": "Chrome",
1421
+ chromium: "Chrome",
1422
+ "chromium-browser": "Chrome",
1423
+ magick: "ImageMagick",
1424
+ convert: "ImageMagick",
1425
+ pdftoppm: "poppler",
1426
+ pdftotext: "poppler",
1427
+ python: "python3",
1428
+ docker: "Docker"
1429
+ };
1430
+ var SPAWNS = [
1431
+ /\b(?:execFileSync|execFile|spawnSync|spawn|execaSync|execa)\(\s*["'`]([a-z][\w.+-]*)["'`]/g,
1432
+ /\b(?:execSync|exec)\(\s*["'`]([a-z][\w.+-]*)[\s"'`]/g,
1433
+ /\bBun\.spawn(?:Sync)?\(\s*\[\s*["'`]([a-z][\w.+-]*)["'`]/g,
1434
+ /\bnew\s+Deno\.Command\(\s*["'`]([a-z][\w.+-]*)["'`]/g,
1435
+ /\bsubprocess\.(?:run|call|check_call|check_output|Popen)\(\s*\[\s*["']([a-z][\w.+-]*)["']/g,
1436
+ /\bsubprocess\.(?:run|call|check_call|check_output|Popen)\(\s*["']([a-z][\w.+-]*)[\s"']/g,
1437
+ /\bos\.(?:system|popen)\(\s*["']([a-z][\w.+-]*)[\s"']/g
1438
+ ];
1439
+ var DRIVES = {
1440
+ puppeteer: "Chrome",
1441
+ "puppeteer-core": "Chrome",
1442
+ "chrome-launcher": "Chrome",
1443
+ "chrome-remote-interface": "Chrome",
1444
+ pyppeteer: "Chrome",
1445
+ playwright: "a Playwright browser",
1446
+ "playwright-core": "a Playwright browser",
1447
+ "@playwright/test": "a Playwright browser",
1448
+ "selenium-webdriver": "a browser and its driver",
1449
+ selenium: "a browser and its driver",
1450
+ "fluent-ffmpeg": "ffmpeg",
1451
+ "ffmpeg-python": "ffmpeg",
1452
+ ffmpeg: "ffmpeg",
1453
+ pydub: "ffmpeg",
1454
+ moviepy: "ffmpeg",
1455
+ "openai-whisper": "ffmpeg",
1456
+ whisper: "ffmpeg",
1457
+ pytesseract: "tesseract",
1458
+ "node-tesseract-ocr": "tesseract",
1459
+ pdf2image: "poppler",
1460
+ "pdf-poppler": "poppler",
1461
+ wand: "ImageMagick",
1462
+ gm: "ImageMagick",
1463
+ pypandoc: "pandoc",
1464
+ "node-pandoc": "pandoc",
1465
+ docker: "Docker",
1466
+ dockerode: "Docker"
1467
+ };
1468
+ var IMPORTS = [
1469
+ /\bfrom\s+["']([@\w./-]+)["']/g,
1470
+ /\bimport\s*\(\s*["']([@\w./-]+)["']\s*\)/g,
1471
+ /\bimport\s+["']([@\w./-]+)["']/g,
1472
+ /\brequire\(\s*["']([@\w./-]+)["']\s*\)/g,
1473
+ /^\s*(?:from\s+([\w.]+)\s+import|import\s+([\w.]+))/gm
1474
+ ];
1475
+ function packageRoot(specifier) {
1476
+ if (specifier.startsWith("@"))
1477
+ return specifier.split("/").slice(0, 2).join("/");
1478
+ return specifier.split("/")[0]?.split(".")[0] ?? specifier;
1479
+ }
1480
+ var NAMED_PROGRAMS = [
1481
+ { name: "ffmpeg", pattern: /\bffmpeg\b/i },
1482
+ { name: "ffprobe", pattern: /\bffprobe\b/i },
1483
+ { name: "Chrome", pattern: /\b(?:Google Chrome|Chrome|Chromium|chrome-headless-shell|google-chrome|chromium-browser)\b/ },
1484
+ { name: "Docker", pattern: /\b[Dd]ocker\b/ },
1485
+ { name: "git", pattern: /\bgit\b(?!\s*(?:hub|lab))/ },
1486
+ { name: "jq", pattern: /\bjq\b/ },
1487
+ { name: "pandoc", pattern: /\bpandoc\b/i },
1488
+ { name: "ImageMagick", pattern: /\b(?:ImageMagick|imagemagick|magick)\b/ },
1489
+ { name: "yt-dlp", pattern: /\byt-dlp\b/ },
1490
+ { name: "tesseract", pattern: /\btesseract\b/i },
1491
+ { name: "poppler", pattern: /\b(?:poppler|pdftoppm|pdftotext)\b/i },
1492
+ { name: "LibreOffice", pattern: /\b(?:LibreOffice|libreoffice|soffice)\b/ },
1493
+ { name: "sox", pattern: /\bsox\b/ },
1494
+ { name: "exiftool", pattern: /\bexiftool\b/i },
1495
+ { name: "ollama", pattern: /\b[Oo]llama\b/ },
1496
+ { name: "gh", pattern: /\bgh\s+(?:CLI|cli)\b|\bGitHub CLI\b/ },
1497
+ { name: "python3", pattern: /\b[Pp]ython\s?3?\b(?![-.\w])/ },
1498
+ { name: "node", pattern: /\b(?:Node\.js|node)\b(?![-.\w])/ },
1499
+ { name: "Java", pattern: /\b(?:Java|JDK|JRE)\b/ }
1500
+ ];
1501
+ var NEEDED = /\b(?:requires?|required|requirements?|needs?|needed|prerequisites?|depends on|must (?:be )?install(?:ed)?|must have|install(?:ed)? first|has to be installed|be installed)\b/i;
1502
+ var NEEDED_GLOBAL = new RegExp(NEEDED.source, "gi");
1503
+ var NOT_NEEDED = /\b(?:no|not|never|without|doesn't|don't|isn't|aren't|zero)\b[^.;]{0,30}\b(?:requires?|required|needs?|needed|install)/i;
1504
+ var NOT_AFTER = /\b(?:requires?|needs?)\s+(?:no|nothing|neither)\b/i;
1505
+ function saysWhatItNeeds(path, isScript) {
1506
+ if (isScript)
1507
+ return true;
1508
+ if (!MARKDOWN.test(path))
1509
+ return false;
1510
+ const base = path.slice(path.lastIndexOf("/") + 1).toLowerCase();
1511
+ return base === "skill.md" || /^(readme|requirements?|prerequisites?|setup|install(ation)?|dependencies|getting-started)\b/.test(base);
1512
+ }
1513
+ var COMMENT_OR_STRING = /^\s*(?:#|\/\/|\*|\/\*|"""|''')|["'`][^"'`]*\b(?:requires?|needs?|install)/i;
1514
+ function namedAfterVerb(line, program) {
1515
+ for (const verb of line.matchAll(NEEDED_GLOBAL)) {
1516
+ const at = verb.index;
1517
+ const after = line.slice(at + verb[0].length).slice(0, 40).split(/[.;:!?](?:\s|$)/)[0] ?? "";
1518
+ const before = line.slice(Math.max(0, at - 30), at).split(/[.;:!?](?:\s|$)/).at(-1) ?? "";
1519
+ if (program.test(after) || program.test(before))
1520
+ return true;
1521
+ }
1522
+ return false;
1523
+ }
1524
+ function programsNeededIn2(files) {
1525
+ const out = new Map;
1526
+ const add = (program) => {
1527
+ if (!out.has(program.name))
1528
+ out.set(program.name, program);
1529
+ };
1530
+ for (const file of files) {
1531
+ if (isTestFile2(file.path))
1532
+ continue;
1533
+ const lines = file.text.split(`
1534
+ `);
1535
+ const isScript = SCRIPT.test(file.path) || lines[0]?.startsWith("#!") === true;
1536
+ if (isScript && !/\.(sh|bash|zsh)$/i.test(file.path)) {
1537
+ for (const pattern of SPAWNS) {
1538
+ for (const match of file.text.matchAll(pattern)) {
1539
+ const name = match[1] ?? "";
1540
+ const line = lineAt(file.text, match.index);
1541
+ if (name === "" || EVERYWHERE.has(name) || COMMENT_LINE.test(lines[line - 1] ?? ""))
1542
+ continue;
1543
+ add({ name: ALIASES[name] ?? name, from: "spawned", file: file.path, line });
1544
+ }
1545
+ }
1546
+ for (const pattern of IMPORTS) {
1547
+ for (const match of file.text.matchAll(pattern)) {
1548
+ const root = packageRoot(match[1] ?? match[2] ?? "");
1549
+ const driven = DRIVES[root];
1550
+ const line = lineAt(file.text, match.index);
1551
+ if (driven === undefined || COMMENT_LINE.test(lines[line - 1] ?? ""))
1552
+ continue;
1553
+ add({ name: driven, from: "package", file: file.path, line, via: root });
1554
+ }
1555
+ }
1556
+ }
1557
+ if (/(^|\/)package\.json$/.test(file.path)) {
1558
+ for (const [index, line] of lines.entries()) {
1559
+ const name = /^\s*"(@?[\w./-]+)"\s*:\s*"/.exec(line)?.[1];
1560
+ const driven = name === undefined ? undefined : DRIVES[name];
1561
+ if (driven !== undefined)
1562
+ add({ name: driven, from: "package", file: file.path, line: index + 1, via: name ?? "" });
1563
+ }
1564
+ }
1565
+ if (/(^|\/)requirements[\w.-]*\.txt$/i.test(file.path)) {
1566
+ for (const [index, line] of lines.entries()) {
1567
+ const name = /^\s*([A-Za-z0-9][\w.-]*)/.exec(line.replace(/#.*/, ""))?.[1]?.toLowerCase();
1568
+ const driven = name === undefined ? undefined : DRIVES[name];
1569
+ if (driven !== undefined)
1570
+ add({ name: driven, from: "package", file: file.path, line: index + 1, via: name ?? "" });
1571
+ }
1572
+ }
1573
+ if (saysWhatItNeeds(file.path, isScript)) {
1574
+ for (const [index, line] of lines.entries()) {
1575
+ if (NOT_NEEDED.test(line) || NOT_AFTER.test(line))
1576
+ continue;
1577
+ if (isScript && !COMMENT_OR_STRING.test(line))
1578
+ continue;
1579
+ for (const { name, pattern } of NAMED_PROGRAMS) {
1580
+ if (namedAfterVerb(line, pattern))
1581
+ add({ name, from: "named as needed", file: file.path, line: index + 1 });
1582
+ }
1583
+ }
1584
+ }
1585
+ }
1586
+ return [...out.values()];
1587
+ }
1588
+
1215
1589
  // ../../packages/protocol/src/skill-contents.ts
1216
1590
  var CONTENTS_LIMITS2 = {
1217
1591
  files: 200,
@@ -1219,8 +1593,8 @@ var CONTENTS_LIMITS2 = {
1219
1593
  items: 40,
1220
1594
  lineChars: 160
1221
1595
  };
1222
- var SCRIPT = /\.(sh|bash|zsh|py|js|mjs|cjs|ts|rb|pl|ps1|php|lua)$/i;
1223
- var MARKDOWN = /\.(md|mdx|markdown)$/i;
1596
+ var SCRIPT2 = /\.(sh|bash|zsh|py|js|mjs|cjs|ts|rb|pl|ps1|php|lua)$/i;
1597
+ var MARKDOWN2 = /\.(md|mdx|markdown)$/i;
1224
1598
  var FENCE = /^\s*(?:```|~~~)/;
1225
1599
  function cap(text) {
1226
1600
  const flat = text.replace(/\s+/g, " ").trim();
@@ -1240,10 +1614,10 @@ function interpreterOf(firstLine) {
1240
1614
  }
1241
1615
  var SCRIPT_PROBE = /(?:^|[;&|(!{]|\bif|\bthen|\$\()\s*(?:!\s*)?(?:command\s+-v|which|type\s+-p|hash)\s+([a-z][\w.+-]*)/g;
1242
1616
  var SYSTEM_INSTALL = /\b(?:brew|apt-get|apt|dnf|yum|apk|pacman|choco|winget|scoop|port)\s+(?:install|add|-S)\s+(?:-\S+\s+)*([a-z][\w.+-]*)/g;
1243
- var COMMENT_LINE = /^\s*(?:#|\/\/|\*|--|;|rem\s)/i;
1617
+ var COMMENT_LINE2 = /^\s*(?:#|\/\/|\*|--|;|rem\s)/i;
1244
1618
  var PYTHON_PROBE = /\bshutil\.which\(\s*["']([a-z][\w.+-]*)["']/g;
1245
1619
  function programsInScriptLine(line, shell) {
1246
- if (COMMENT_LINE.test(line))
1620
+ if (COMMENT_LINE2.test(line))
1247
1621
  return [];
1248
1622
  const matches = shell ? [...line.matchAll(SCRIPT_PROBE), ...line.matchAll(SYSTEM_INSTALL)] : [...line.matchAll(PYTHON_PROBE)];
1249
1623
  return matches.map((match) => match[1] ?? "").filter((name) => name !== "");
@@ -1307,7 +1681,7 @@ function settingsIn(path, text, code) {
1307
1681
  for (const match of text.matchAll(pattern))
1308
1682
  found.add((match[1] ?? "").toUpperCase());
1309
1683
  }
1310
- if (MARKDOWN.test(path)) {
1684
+ if (MARKDOWN2.test(path)) {
1311
1685
  for (const match of text.matchAll(EXPORT_IN_PROSE))
1312
1686
  found.add(match[1] ?? "");
1313
1687
  for (const match of text.matchAll(KEY_NAMED))
@@ -1323,7 +1697,7 @@ function codeOf(path, text, isScript) {
1323
1697
  `);
1324
1698
  if (isScript)
1325
1699
  return lines;
1326
- if (!MARKDOWN.test(path))
1700
+ if (!MARKDOWN2.test(path))
1327
1701
  return [];
1328
1702
  const out = [];
1329
1703
  let fenced = false;
@@ -1496,7 +1870,7 @@ function readSkillContents2(input) {
1496
1870
  if (files.length >= CONTENTS_LIMITS2.files)
1497
1871
  skipped.push({ path: file.path, why: "over the file limit" });
1498
1872
  else if (file.text === null)
1499
- skipped.push({ path: file.path, why: file.notFetched === true ? "not fetched" : "binary" });
1873
+ skipped.push({ path: file.path, why: file.tooLarge === true ? "too large" : file.notFetched === true ? "not fetched" : "binary" });
1500
1874
  else if (file.text.length > CONTENTS_LIMITS2.fileChars)
1501
1875
  skipped.push({ path: file.path, why: "too large" });
1502
1876
  else
@@ -1507,7 +1881,7 @@ function readSkillContents2(input) {
1507
1881
  const programs = new Map;
1508
1882
  const add = (name, from) => {
1509
1883
  if (!programs.has(name))
1510
- programs.set(name, from);
1884
+ programs.set(name, { name, from });
1511
1885
  };
1512
1886
  const requires = readRequires2(skillMd);
1513
1887
  for (const name of requires.cli)
@@ -1519,11 +1893,11 @@ function readSkillContents2(input) {
1519
1893
  const domains = new Set;
1520
1894
  const fetchAndRun = [];
1521
1895
  const hidden = [];
1522
- const scripts = skipped.filter((file) => file.why === "not fetched" && SCRIPT.test(file.path)).map((file) => ({ path: file.path, namedInSkillMd: namesFile(skillMd, file.path) }));
1896
+ const scripts = skipped.filter((file) => file.why === "not fetched" && SCRIPT2.test(file.path)).map((file) => ({ path: file.path, namedInSkillMd: namesFile(skillMd, file.path) }));
1523
1897
  for (const file of files) {
1524
1898
  const lines = file.text.split(`
1525
1899
  `);
1526
- const isScript = SCRIPT.test(file.path) || lines[0]?.startsWith("#!") === true;
1900
+ const isScript = SCRIPT2.test(file.path) || lines[0]?.startsWith("#!") === true;
1527
1901
  const code = codeOf(file.path, file.text, isScript);
1528
1902
  const shell = isShell(file.path, file.text);
1529
1903
  for (const line of code) {
@@ -1556,18 +1930,22 @@ function readSkillContents2(input) {
1556
1930
  if (run !== undefined)
1557
1931
  fetchAndRun.push({ kind: run.kind, file: file.path, line: index + 1, text: cap(line) });
1558
1932
  }
1559
- if (MARKDOWN.test(file.path))
1933
+ if (MARKDOWN2.test(file.path))
1560
1934
  hidden.push(...hiddenInMarkdown(file.path, file.text));
1561
1935
  }
1562
1936
  const packageNames = new Set([...packages.values()].map((pkg) => pkg.name));
1563
- for (const [name, from] of programs)
1564
- if (from !== "declared" && packageNames.has(name))
1937
+ for (const [name, program] of programs)
1938
+ if (program.from !== "declared" && packageNames.has(name))
1565
1939
  programs.delete(name);
1940
+ for (const needed of programsNeededIn2(files))
1941
+ if (!programs.has(needed.name))
1942
+ programs.set(needed.name, needed);
1566
1943
  const cut = [];
1567
1944
  return {
1568
- programs: bounded("programs", [...programs.entries()].map(([name, from]) => ({ name, from })).sort((a, b) => compare(a.name, b.name)), cut),
1945
+ programs: bounded("programs", [...programs.values()].sort((a, b) => compare(a.name, b.name)), cut),
1569
1946
  packages: bounded("packages", [...packages.values()].sort((a, b) => compare(`${a.manager} ${a.name}`, `${b.manager} ${b.name}`)), cut),
1570
1947
  settings: bounded("settings", [...settings].sort(compare), cut),
1948
+ keys: bounded("keys", keysIn2(files, [...settings]), cut),
1571
1949
  scripts: bounded("scripts", scripts.sort((a, b) => compare(a.path, b.path)), cut),
1572
1950
  fetchAndRun: bounded("fetchAndRun", fetchAndRun, cut),
1573
1951
  domains: bounded("domains", [...domains].sort(compare), cut),
@@ -1580,24 +1958,12 @@ function readSkillContents2(input) {
1580
1958
  cut
1581
1959
  };
1582
1960
  }
1583
- function contentsLines2(contents, perList = 6) {
1584
- const list = (items, cut) => items.length === 0 ? "none found" : `${items.slice(0, perList).join(", ")}${items.length > perList || cut ? `, and ${Math.max(0, items.length - perList)}${cut ? "+" : ""} more` : ""}`;
1585
- const at = (rows, name) => list(rows.map((row) => `${row.kind} at ${row.file}:${row.line} "${row.text}"`), contents.cut.includes(name));
1586
- return [
1587
- `Programs: ${list(contents.programs.map((p) => `${p.name} (${p.from})`), contents.cut.includes("programs"))}`,
1588
- ...contents.packages.length === 0 ? [] : [`Packages: ${list(contents.packages.map((p) => `${p.manager} ${p.name}`), contents.cut.includes("packages"))}`],
1589
- `Keys and settings it reads: ${list(contents.settings, contents.cut.includes("settings"))}`,
1590
- `Scripts: ${list(contents.scripts.map((s) => s.namedInSkillMd ? `${s.path} (named in SKILL.md)` : s.path), contents.cut.includes("scripts"))}`,
1591
- `Fetches and runs code: ${at(contents.fetchAndRun, "fetchAndRun")}`,
1592
- `Domains named: ${list(contents.domains, contents.cut.includes("domains"))}`,
1593
- `Hidden or encoded text: ${at(contents.hidden, "hidden")}`,
1594
- ...contents.read.skipped.length === 0 ? [] : [`Not read: ${list(contents.read.skipped.map((s) => `${s.path} (${s.why})`), false)}`]
1595
- ];
1596
- }
1597
1961
 
1598
1962
  // ../../packages/protocol/src/pulse.ts
1599
1963
  var MAX_SKILL_FILES_READ2 = 200;
1600
1964
  var MAX_SCRIPTS_READ = 5;
1965
+ var MAX_DIRECTORY_FILES_READ = 80;
1966
+ var NOT_TEXT = /\.(png|jpe?g|gif|webp|ico|bmp|tiff?|avif|svgz|pdf|zip|gz|tgz|bz2|xz|7z|rar|jar|woff2?|ttf|otf|eot|mp[34]|m4a|wav|ogg|flac|aac|mov|webm|avi|mkv|pptx|docx|xlsx|psd|ai|sketch|fig|db|sqlite|bin|exe|dll|so|dylib|wasm|pyc|class|hdr|exr|glb|ktx2?)$/i;
1601
1967
  var GITHUB_READ_TIMEOUT_MS = 1e4;
1602
1968
  var INSTALLER_ORDER = [
1603
1969
  "",
@@ -1669,7 +2035,7 @@ var PAID_WORD = /\b(pricing|paid|subscription|credits?|billing|free tier|per min
1669
2035
  var HOST = /https?:\/\/([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g;
1670
2036
  var NOT_A_SERVICE = /(^|\.)(github\.com|githubusercontent\.com|example\.(com|org|net))$/;
1671
2037
  var LOOKS_LIKE_A_FILE = /\.(jpe?g|png|gif|webp|mp[34]|wav|mov|webm|pdf|md|json|txt)$/;
1672
- var SCRIPT2 = /\.(sh|bash|py|js|mjs|cjs|ts|rb|pl)$/i;
2038
+ var SCRIPT3 = /\.(sh|bash|py|js|mjs|cjs|ts|rb|pl)$/i;
1673
2039
  var RUNS_FILE = /\b(?:node|bun|deno|tsx|python3?|bash|sh)\s+((?:~|\.{1,2})?\/?[\w.@-]+(?:\/[\w.@-]+)*\.(?:mjs|cjs|js|ts|py|sh))\b/g;
1674
2040
  var PLACEHOLDER = /(^|\/)(path|your|my|foo|example|script|file|input|output)(\/|\.|$)/i;
1675
2041
  function filesItRuns(markdown, repositoryFiles) {
@@ -1699,7 +2065,7 @@ function readSkillFacts2(markdown, filesInDirectory, repositoryFiles = filesInDi
1699
2065
  return {
1700
2066
  files: filesInDirectory.length,
1701
2067
  bytes: filesInDirectory.reduce((total, file) => total + file.size, 0),
1702
- scripts: filesInDirectory.filter((file) => SCRIPT2.test(file.path)).length,
2068
+ scripts: filesInDirectory.filter((file) => SCRIPT3.test(file.path)).length,
1703
2069
  keys: [...new Set(markdown.match(KEY_NAME3) ?? [])],
1704
2070
  paidWords: [...new Set((markdown.match(PAID_WORD) ?? []).map((word) => word.toLowerCase()))],
1705
2071
  hosts: [...new Set(hosts)],
@@ -1713,6 +2079,14 @@ function parentOf(path) {
1713
2079
  const at = path.lastIndexOf("/");
1714
2080
  return at === -1 ? "" : path.slice(0, at);
1715
2081
  }
2082
+ var MAX_NAME_READS2 = 12;
2083
+ var NAME_BATCH = 6;
2084
+ var wordsOf = (text) => text.toLowerCase().split(/[^a-z0-9]+/).filter((word) => word.length > 1);
2085
+ function byNearness(files, wanted) {
2086
+ const words = new Set(wordsOf(wanted));
2087
+ const shared = (file) => wordsOf(parentOf(file.path)).filter((word) => words.has(word)).length;
2088
+ return files.map((file) => ({ file, shared: shared(file) })).sort((a, b) => b.shared - a.shared || (a.file.path < b.file.path ? -1 : a.file.path > b.file.path ? 1 : 0));
2089
+ }
1716
2090
  async function locate(source, repo, skill, listing) {
1717
2091
  const wanted = normaliseName(skill);
1718
2092
  const candidates = listing.files.filter((file) => skillFilesIn2([file]).length === 1);
@@ -1724,12 +2098,18 @@ async function locate(source, repo, skill, listing) {
1724
2098
  if (others.length > MAX_SKILL_FILES_READ2) {
1725
2099
  return { status: "incomplete", reason: "too_many_skill_files" };
1726
2100
  }
1727
- for (let at = 0;at < others.length && named.length === 0; at += 20) {
1728
- named = await read(others.slice(at, at + 20));
2101
+ const nearest = byNearness(others, wanted).slice(0, MAX_NAME_READS2);
2102
+ const sharing = nearest.filter((entry) => entry.shared > 0).length;
2103
+ let readTo = 0;
2104
+ while (readTo < nearest.length && named.length === 0) {
2105
+ const size = readTo < sharing ? sharing - readTo : NAME_BATCH;
2106
+ named = await read(nearest.slice(readTo, readTo + size).map((entry) => entry.file));
2107
+ readTo += size;
1729
2108
  }
1730
2109
  if (named.length > 0) {
1731
- const last = others.findIndex((file) => file === named.at(-1)?.file);
1732
- named = [...named, ...await read(others.slice(last + 1))];
2110
+ named = [...named, ...await read(nearest.slice(readTo, sharing).map((entry) => entry.file))];
2111
+ } else if (nearest.length < others.length) {
2112
+ return { status: "incomplete", reason: "too_many_skill_files" };
1733
2113
  }
1734
2114
  }
1735
2115
  if (named.length === 0) {
@@ -1769,7 +2149,24 @@ async function readFileOnly(result, source) {
1769
2149
  }
1770
2150
  return null;
1771
2151
  }
1772
- async function readPulse2(result, source) {
2152
+ var SCRIPT_READ_CAP2 = { files: MAX_SCRIPTS_READ, bytesEach: 256 * 1024, bytes: 512 * 1024 };
2153
+ var NOT_CODE_DIRECTORY = /(^|\/)(examples?|assets?|fonts?|images?|media|samples?|fixtures?)\//i;
2154
+ function withinReadCap(paths, listed) {
2155
+ const sizeOf = new Map(listed.map((file) => [file.path, file.size]));
2156
+ const kept = [];
2157
+ let bytes = 0;
2158
+ for (const path of paths) {
2159
+ const size = sizeOf.get(path) ?? 0;
2160
+ if (kept.length >= SCRIPT_READ_CAP2.files || NOT_CODE_DIRECTORY.test(path))
2161
+ continue;
2162
+ if (size > SCRIPT_READ_CAP2.bytesEach || bytes + size > SCRIPT_READ_CAP2.bytes)
2163
+ continue;
2164
+ kept.push(path);
2165
+ bytes += size;
2166
+ }
2167
+ return kept;
2168
+ }
2169
+ async function readPulse2(result, source, depth) {
1773
2170
  if (!/^(?!\.{1,2}\/)[\w.-]+\/(?!\.{1,2}$)[\w.-]+$/.test(result.source))
1774
2171
  return { ...result, status: "not_github" };
1775
2172
  const listing = await source.listing(result.source);
@@ -1787,15 +2184,20 @@ async function readPulse2(result, source) {
1787
2184
  const directory = parentOf(located.file.path);
1788
2185
  const prefix = directory === "" ? "" : `${directory}/`;
1789
2186
  const inDirectory = listing.files.filter((file) => file.path.startsWith(prefix));
1790
- const scripts = filesItRuns(located.markdown, listing.files).slice(0, MAX_SCRIPTS_READ);
2187
+ const scripts = withinReadCap(filesItRuns(located.markdown, listing.files), listing.files);
1791
2188
  const scriptTexts = await Promise.all(scripts.map(async (file) => ({ file, text: await source.file(result.source, file) })));
1792
2189
  const keysInScriptsItRuns = scriptTexts.map(({ file, text }) => ({ file, keys: [...new Set(text?.match(KEY_NAME3) ?? [])] })).filter((entry) => entry.keys.length > 0);
1793
2190
  const relative = (path) => path.startsWith(prefix) ? path.slice(prefix.length) : path;
1794
2191
  const fetched = new Set(scriptTexts.filter((entry) => entry.text !== null).map((entry) => entry.file));
2192
+ const others = depth === undefined ? [] : await readDirectory(source, result.source, inDirectory, new Set([located.file.path, ...fetched]), depth.directory.by);
2193
+ for (const file of others)
2194
+ if (file.text !== null)
2195
+ fetched.add(file.path);
1795
2196
  const contains = readSkillContents2([
1796
2197
  { path: "SKILL.md", text: located.markdown },
1797
2198
  ...scriptTexts.flatMap(({ file, text }) => text === null ? [] : [{ path: relative(file), text }]),
1798
- ...inDirectory.filter((file) => SCRIPT2.test(file.path) && !fetched.has(file.path)).map((file) => ({ path: relative(file.path), text: null, notFetched: true }))
2199
+ ...others.flatMap(({ path, text }) => text === null ? [] : [{ path: relative(path), text }]),
2200
+ ...inDirectory.filter((file) => !fetched.has(file.path) && file.path !== located.file.path && (depth !== undefined || SCRIPT3.test(file.path))).map((file) => NOT_TEXT.test(file.path) ? { path: relative(file.path), text: null } : file.size > CONTENTS_LIMITS2.fileChars ? { path: relative(file.path), text: null, notFetched: true, tooLarge: true } : { path: relative(file.path), text: null, notFetched: true })
1799
2201
  ]);
1800
2202
  return {
1801
2203
  ...result,
@@ -1812,6 +2214,22 @@ async function readPulse2(result, source) {
1812
2214
  contains
1813
2215
  };
1814
2216
  }
2217
+ async function readDirectory(source, repo, inDirectory, done, by) {
2218
+ let late = false;
2219
+ by.then(() => {
2220
+ late = true;
2221
+ });
2222
+ const plan = inDirectory.filter((file) => !done.has(file.path) && !NOT_TEXT.test(file.path) && file.size <= CONTENTS_LIMITS2.fileChars).slice(0, MAX_DIRECTORY_FILES_READ);
2223
+ const out = [];
2224
+ for (let at = 0;at < plan.length && !late; at += 8) {
2225
+ const batch = Promise.all(plan.slice(at, at + 8).map(async (file) => ({ path: file.path, text: await source.file(repo, file.path).catch(() => null) })));
2226
+ const settled = await Promise.race([batch, by.then(() => null)]);
2227
+ if (settled === null)
2228
+ break;
2229
+ out.push(...settled);
2230
+ }
2231
+ return out;
2232
+ }
1815
2233
  function githubPulseSource2(options = {}) {
1816
2234
  const get = options.fetch ?? fetch;
1817
2235
  const timeoutMs = options.timeoutMs ?? GITHUB_READ_TIMEOUT_MS;
@@ -1826,6 +2244,7 @@ function githubPulseSource2(options = {}) {
1826
2244
  };
1827
2245
  const listings = new Map;
1828
2246
  const repositories = new Map;
2247
+ const fileReads = new Map;
1829
2248
  const quota = { apiRequests: 0, limited: false, resetsAt: null };
1830
2249
  const noteLimit = (response) => {
1831
2250
  const hit = response.status === 429 || response.status === 403 && (response.headers.get("x-ratelimit-remaining") === "0" || response.headers.has("retry-after"));
@@ -1898,15 +2317,191 @@ function githubPulseSource2(options = {}) {
1898
2317
  return {
1899
2318
  listing: (repo) => once(listings, repo, readListing),
1900
2319
  repository: (repo) => once(repositories, repo, readRepository),
1901
- file: async (repo, path) => {
1902
- const encoded = path.split("/").map(encodeURIComponent).join("/");
1903
- const response = await get(`https://raw.githubusercontent.com/${repo}/HEAD/${encoded}`, { signal: bounded2() });
1904
- return response.ok ? response.text() : null;
2320
+ file: (repo, path) => {
2321
+ const key = `${repo}
2322
+ ${path}`;
2323
+ const kept = options.files?.get(key);
2324
+ if (kept !== undefined)
2325
+ return Promise.resolve(kept);
2326
+ return once(fileReads, key, async () => {
2327
+ const encoded = path.split("/").map(encodeURIComponent).join("/");
2328
+ const response = await get(`https://raw.githubusercontent.com/${repo}/HEAD/${encoded}`, { signal: bounded2() });
2329
+ const text = response.ok ? await response.text() : null;
2330
+ if (response.ok || response.status === 404)
2331
+ options.files?.set(key, text);
2332
+ return text;
2333
+ });
1905
2334
  },
1906
2335
  quota: () => ({ ...quota })
1907
2336
  };
1908
2337
  }
1909
2338
 
2339
+ // ../../packages/protocol/src/before-install.ts
2340
+ var PAID_SERVICES2 = [
2341
+ { name: "Anthropic", hosts: ["api.anthropic.com"], keyPrefixes: ["ANTHROPIC_"] },
2342
+ { name: "AssemblyAI", hosts: ["api.assemblyai.com"], keyPrefixes: ["ASSEMBLYAI_"] },
2343
+ { name: "Azure OpenAI", hosts: ["openai.azure.com"], keyPrefixes: ["AZURE_OPENAI_"] },
2344
+ { name: "Brave Search", hosts: ["api.search.brave.com"], keyPrefixes: ["BRAVE_"] },
2345
+ { name: "Browserbase", hosts: ["api.browserbase.com"], keyPrefixes: ["BROWSERBASE_"] },
2346
+ { name: "Cohere", hosts: ["api.cohere.ai", "api.cohere.com"], keyPrefixes: ["COHERE_", "CO_API_"] },
2347
+ { name: "Deepgram", hosts: ["api.deepgram.com"], keyPrefixes: ["DEEPGRAM_"] },
2348
+ { name: "DeepSeek", hosts: ["api.deepseek.com"], keyPrefixes: ["DEEPSEEK_"] },
2349
+ { name: "ElevenLabs", hosts: ["api.elevenlabs.io"], keyPrefixes: ["ELEVENLABS_", "ELEVEN_"] },
2350
+ { name: "Exa", hosts: ["api.exa.ai"], keyPrefixes: ["EXA_"] },
2351
+ { name: "fal", hosts: ["fal.run", "queue.fal.run"], keyPrefixes: ["FAL_"] },
2352
+ { name: "Firecrawl", hosts: ["api.firecrawl.dev"], keyPrefixes: ["FIRECRAWL_"] },
2353
+ { name: "Fireworks AI", hosts: ["api.fireworks.ai"], keyPrefixes: ["FIREWORKS_"] },
2354
+ { name: "Google Gemini", hosts: ["generativelanguage.googleapis.com"], keyPrefixes: ["GEMINI_"] },
2355
+ { name: "Google Vertex AI", hosts: ["aiplatform.googleapis.com"], keyPrefixes: ["VERTEX_"] },
2356
+ { name: "Groq", hosts: ["api.groq.com"], keyPrefixes: ["GROQ_"] },
2357
+ { name: "HeyGen", hosts: ["api.heygen.com"], keyPrefixes: ["HEYGEN_"] },
2358
+ { name: "Mistral", hosts: ["api.mistral.ai"], keyPrefixes: ["MISTRAL_"] },
2359
+ { name: "OpenAI", hosts: ["api.openai.com"], keyPrefixes: ["OPENAI_"] },
2360
+ { name: "OpenRouter", hosts: ["openrouter.ai"], keyPrefixes: ["OPENROUTER_"] },
2361
+ { name: "Perplexity", hosts: ["api.perplexity.ai"], keyPrefixes: ["PERPLEXITY_", "PPLX_"] },
2362
+ { name: "Replicate", hosts: ["api.replicate.com"], keyPrefixes: ["REPLICATE_"] },
2363
+ { name: "Resend", hosts: ["api.resend.com"], keyPrefixes: ["RESEND_"] },
2364
+ { name: "Runway", hosts: ["api.dev.runwayml.com", "api.runwayml.com"], keyPrefixes: ["RUNWAY_", "RUNWAYML_"] },
2365
+ { name: "SerpApi", hosts: ["serpapi.com"], keyPrefixes: ["SERPAPI_", "SERP_API_"] },
2366
+ { name: "Stability AI", hosts: ["api.stability.ai"], keyPrefixes: ["STABILITY_"] },
2367
+ { name: "Tavily", hosts: ["api.tavily.com"], keyPrefixes: ["TAVILY_"] },
2368
+ { name: "Together AI", hosts: ["api.together.xyz", "api.together.ai"], keyPrefixes: ["TOGETHER_"] },
2369
+ { name: "Twilio", hosts: ["api.twilio.com"], keyPrefixes: ["TWILIO_"] },
2370
+ { name: "xAI", hosts: ["api.x.ai"], keyPrefixes: ["XAI_"] }
2371
+ ];
2372
+ var STATIC_HOSTS = [
2373
+ "fonts.googleapis.com",
2374
+ "gstatic.com",
2375
+ "cdn.jsdelivr.net",
2376
+ "unpkg.com",
2377
+ "cdnjs.cloudflare.com",
2378
+ "esm.sh",
2379
+ "cdn.skypack.dev",
2380
+ "ga.jspm.io",
2381
+ "githubusercontent.com",
2382
+ "cdn.tailwindcss.com",
2383
+ "code.jquery.com",
2384
+ "use.typekit.net",
2385
+ "fonts.bunny.net",
2386
+ "rsms.me",
2387
+ "cartocdn.com",
2388
+ "tile.openstreetmap.org",
2389
+ "server.arcgisonline.com",
2390
+ "images.unsplash.com"
2391
+ ];
2392
+ var STATIC_SHAPE = /^(?:cdn|static|fonts?|tiles?|assets|img|images|media)\d*[.-]|(?:^|[.-])cdn(?:[.-]|$)|\.cdn\./;
2393
+ var API_SHAPE = /^api\d*[.-]|[.-]api\.|^.*\.googleapis\.com$/;
2394
+ function under(host, base) {
2395
+ return host === base || host.endsWith(`.${base}`);
2396
+ }
2397
+ var ASSET = /\.(woff2?|ttf|otf|eot|png|jpe?g|gif|webp|svgz?|ico|bmp|tiff?|avif|heic|hdr|exr|mp[34]|m4a|wav|ogg|flac|aac|mov|webm|avi|mkv|psd|ai|sketch|fig|glb|gltf|fbx|blend|ktx2?|basis|pdf|zip|gz|tgz)$|(^|\/)\.(gitkeep|keep|DS_Store)$/i;
2398
+ function beforeInstall2(input) {
2399
+ const services = new Map;
2400
+ const service = (name) => {
2401
+ const found = services.get(name) ?? { name, hosts: [], keys: [] };
2402
+ services.set(name, found);
2403
+ return found;
2404
+ };
2405
+ const apiHosts = [];
2406
+ const staticHosts = [];
2407
+ const otherHosts = [];
2408
+ for (const host of input.domains) {
2409
+ const known = PAID_SERVICES2.find((entry) => entry.hosts.some((base) => under(host, base)));
2410
+ if (known !== undefined) {
2411
+ service(known.name).hosts.push(host);
2412
+ apiHosts.push(host);
2413
+ } else if (STATIC_HOSTS.some((base) => under(host, base)) || STATIC_SHAPE.test(host))
2414
+ staticHosts.push(host);
2415
+ else if (API_SHAPE.test(host))
2416
+ apiHosts.push(host);
2417
+ else
2418
+ otherHosts.push(host);
2419
+ }
2420
+ for (const key of input.keys) {
2421
+ if (key.why !== "secret")
2422
+ continue;
2423
+ const known = PAID_SERVICES2.find((entry) => entry.keyPrefixes.some((prefix) => key.name.startsWith(prefix)));
2424
+ if (known !== undefined)
2425
+ service(known.name).keys.push(key.name);
2426
+ }
2427
+ const asked = new Set(input.keys.map((key) => key.name));
2428
+ return {
2429
+ fetchAndRun: [...input.fetchAndRun],
2430
+ keys: [...input.keys],
2431
+ otherSettings: input.settings.filter((name) => !asked.has(name)).length,
2432
+ services: [...services.values()].sort((a, b) => a.name < b.name ? -1 : 1),
2433
+ programs: [...input.programs],
2434
+ scripts: [...input.scripts],
2435
+ apiHosts,
2436
+ staticHosts,
2437
+ otherHosts,
2438
+ hidden: [...input.hidden],
2439
+ notRead: input.skipped.filter((file) => file.why !== "binary" && !ASSET.test(file.path)),
2440
+ cut: [...input.cut]
2441
+ };
2442
+ }
2443
+ var plural = (count, one, many = `${one}s`) => `${count} ${count === 1 ? one : many}`;
2444
+ function programNames(programs, shown = 4) {
2445
+ const names = programs.map((program) => program.name);
2446
+ return names.length <= shown ? names.join(", ") : `${names.slice(0, shown).join(", ")} +${names.length - shown}`;
2447
+ }
2448
+ function beforeInstallSummary2(view) {
2449
+ const secrets = view.keys.filter((key) => key.why === "secret").length;
2450
+ const toSet = view.keys.length - secrets;
2451
+ const asks = secrets + toSet === 0 ? "no keys" : [secrets === 0 ? null : plural(secrets, "key"), toSet === 0 ? null : plural(toSet, "setting")].filter((part) => part !== null).join(", ");
2452
+ return [
2453
+ ...view.fetchAndRun.length === 0 ? [] : ["fetches and runs code"],
2454
+ asks,
2455
+ view.services.length === 0 ? "no paid services" : plural(view.services.length, "paid service"),
2456
+ view.programs.length === 0 ? "nothing to install named" : `needs ${programNames(view.programs)}`,
2457
+ view.scripts.length === 0 ? "no scripts" : plural(view.scripts.length, "script"),
2458
+ ...view.hidden.length === 0 ? [] : [plural(view.hidden.length, "hidden text", "hidden texts")],
2459
+ ...view.notRead.length === 0 ? [] : [`${plural(view.notRead.length, "file")} not read`]
2460
+ ].join(" · ");
2461
+ }
2462
+ function programSourceText2(program) {
2463
+ const at = program.file === undefined ? "" : `${program.file}${program.line === undefined ? "" : `:${program.line}`}`;
2464
+ switch (program.from) {
2465
+ case "declared":
2466
+ return "declared in SKILL.md";
2467
+ case "runs":
2468
+ return "run in SKILL.md";
2469
+ case "interpreter":
2470
+ return "script interpreter";
2471
+ case "script":
2472
+ return "a script checks for it or installs it";
2473
+ case "spawned":
2474
+ return `run by ${at}`;
2475
+ case "package":
2476
+ return `via ${program.via ?? "a package"}, ${at}`;
2477
+ case "named as needed":
2478
+ return `said to be needed, ${at}`;
2479
+ }
2480
+ }
2481
+ function contentsLines2(contents, perList = 6) {
2482
+ const view = beforeInstall2({ ...contents, skipped: contents.read.skipped });
2483
+ const list = (items, cut2) => `${items.slice(0, perList).join(", ")}${items.length > perList || cut2 ? `, and ${Math.max(0, items.length - perList)}${cut2 ? "+" : ""} more` : ""}`;
2484
+ const cut = (name) => view.cut.includes(name);
2485
+ const at = (rows, name) => list(rows.map((row) => `${row.kind} at ${row.file}:${row.line} "${row.text}"`), cut(name));
2486
+ const others = view.otherSettings === 0 ? "" : `; ${plural(view.otherSettings, "other setting")} it reads`;
2487
+ const hostParts = [
2488
+ view.apiHosts.length === 0 ? "no API host" : list(view.apiHosts, false),
2489
+ ...view.staticHosts.length === 0 ? [] : [`fonts, CDNs and map tiles: ${view.staticHosts.length}`],
2490
+ ...view.otherHosts.length === 0 ? [] : [`other sites named: ${view.otherHosts.length}`]
2491
+ ];
2492
+ return [
2493
+ `Before you install: ${beforeInstallSummary2(view)}`,
2494
+ ...view.fetchAndRun.length === 0 ? [] : [`Fetches and runs code: ${at(view.fetchAndRun, "fetchAndRun")}`],
2495
+ `Asks for: ${view.keys.length === 0 ? `no keys${others}` : `${list(view.keys.map((key) => `${key.name} (${key.why === "secret" ? "" : "set in SKILL.md, "}${key.file}:${key.line})`), cut("keys"))}${others}`}`,
2496
+ `Paid services: ${view.services.length === 0 ? "none named" : list(view.services.map((entry) => `${entry.name} (${[...entry.hosts, ...entry.keys].join(", ")})`), false)}`,
2497
+ `Needs installed: ${view.programs.length === 0 ? "none named" : list(view.programs.map((program) => `${program.name} (${programSourceText2(program)})`), cut("programs"))}`,
2498
+ `Runs on your machine: ${view.scripts.length === 0 ? "no scripts" : `${plural(view.scripts.length, "script")}: ${list(view.scripts.map((script) => script.namedInSkillMd ? `${script.path} (named in SKILL.md)` : script.path), cut("scripts"))}`}${view.fetchAndRun.length === 0 ? "; fetches and runs no code" : ""}`,
2499
+ `Sends data to (hosts its files name, not shown to be contacted): ${hostParts.join("; ")}`,
2500
+ ...view.hidden.length === 0 ? [] : [`Hidden or encoded text: ${at(view.hidden, "hidden")}`],
2501
+ ...view.notRead.length === 0 ? [] : [`Not read: ${list(view.notRead.map((file) => `${file.path} (${file.why})`), false)}`]
2502
+ ];
2503
+ }
2504
+
1910
2505
  // ../../packages/protocol/src/skill-install.ts
1911
2506
  var MAX_SKILLS_READ2 = 3;
1912
2507
  var ADD = new Set(["add", "a", "install", "i"]);
@@ -2062,7 +2657,7 @@ async function namesUnder(install, source) {
2062
2657
  }));
2063
2658
  return { names, total: files.length };
2064
2659
  }
2065
- async function readSkillInstall2(install, source, deadline) {
2660
+ async function readSkillInstall2(install, source, deadline, directoryBy = deadline) {
2066
2661
  if (install.kind === "elsewhere")
2067
2662
  return install;
2068
2663
  const late = deadline.then(() => "late");
@@ -2090,7 +2685,7 @@ async function readSkillInstall2(install, source, deadline) {
2090
2685
  total = install.skills.length;
2091
2686
  }
2092
2687
  const readings = await Promise.all(names.map(async (skill) => {
2093
- const pulse = readPulse2({ source: install.repo, skill, installs: null, installsText: null, url: null }, source).then((read) => ({ skill, pulse: read })).catch(() => ({ skill, pulse: null, why: "read failed" }));
2688
+ const pulse = readPulse2({ source: install.repo, skill, installs: null, installsText: null, url: null }, source, { directory: { by: directoryBy } }).then((read) => ({ skill, pulse: read })).catch(() => ({ skill, pulse: null, why: "read failed" }));
2094
2689
  const settled = await Promise.race([pulse, late]);
2095
2690
  return settled === "late" ? { skill, pulse: null, why: "no answer in time" } : settled;
2096
2691
  }));
@@ -2102,7 +2697,7 @@ async function readSkillInstall2(install, source, deadline) {
2102
2697
  listing: null
2103
2698
  };
2104
2699
  }
2105
- var plural = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
2700
+ var plural2 = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
2106
2701
  function readingLines(repo, reading) {
2107
2702
  const head = `${repo} · ${reading.skill}`;
2108
2703
  const { pulse } = reading;
@@ -2114,9 +2709,9 @@ function readingLines(repo, reading) {
2114
2709
  case "read": {
2115
2710
  const facts = [
2116
2711
  `${pulse.path}`,
2117
- plural(pulse.files, "file"),
2712
+ plural2(pulse.files, "file"),
2118
2713
  ...pulse.repository === null ? [] : [
2119
- plural(pulse.repository.stars, "star"),
2714
+ plural2(pulse.repository.stars, "star"),
2120
2715
  ...pulse.repository.pushedAt === null ? [] : [`last push ${pulse.repository.pushedAt.slice(0, 10)}`],
2121
2716
  ...pulse.repository.archived ? ["archived"] : []
2122
2717
  ]
@@ -2134,7 +2729,7 @@ function readingLines(repo, reading) {
2134
2729
  ];
2135
2730
  case "absent":
2136
2731
  return [
2137
- `${head}: no SKILL.md in the repository names it (${plural(pulse.skillFiles, "skill file")} read)`
2732
+ `${head}: no SKILL.md in the repository names it (${plural2(pulse.skillFiles, "skill file")} read)`
2138
2733
  ];
2139
2734
  case "ambiguous":
2140
2735
  return [
@@ -2169,7 +2764,7 @@ ${read.source}: not read (ARP reads GitHub sources only)`;
2169
2764
  for (const reading of read.readings)
2170
2765
  lines.push(...readingLines(install.repo, reading));
2171
2766
  if (read.notRead > 0)
2172
- lines.push(`${plural(read.notRead, "more skill")} in this install not read (ARP reads ${MAX_SKILLS_READ2})`);
2767
+ lines.push(`${plural2(read.notRead, "more skill")} in this install not read (ARP reads ${MAX_SKILLS_READ2})`);
2173
2768
  if (install.ref !== null)
2174
2769
  lines.push(`Read at the default branch's HEAD; the command names ${install.ref}.`);
2175
2770
  return lines.join(`
@@ -2178,6 +2773,7 @@ ${read.source}: not read (ARP reads GitHub sources only)`;
2178
2773
 
2179
2774
  // ../hook/src/pre-install.ts
2180
2775
  var GITHUB_BUDGET_MS = 6000;
2776
+ var DIRECTORY_MARGIN_MS = 1500;
2181
2777
  var OUTPUT_CAP = 9500;
2182
2778
  var FOR_THE_AGENT = "This is what the skill's own files contain, read by ARP's hook before the install ran. " + "They are facts, not a verdict, and nothing was blocked. The person was shown the same lines.";
2183
2779
  function capped(text) {
@@ -2198,13 +2794,14 @@ async function preToolUse(raw, options = {}) {
2198
2794
  const token = (env["GITHUB_TOKEN"] ?? env["GH_TOKEN"])?.trim() || undefined;
2199
2795
  const budgetMs = options.budgetMs ?? GITHUB_BUDGET_MS;
2200
2796
  const deadline = new Promise((resolve) => setTimeout(resolve, budgetMs));
2797
+ const directoryBy = new Promise((resolve) => setTimeout(resolve, Math.max(0, budgetMs - DIRECTORY_MARGIN_MS)));
2201
2798
  const source = githubPulseSource2({
2202
2799
  ...options.fetch === undefined ? {} : { fetch: options.fetch },
2203
2800
  ...token === undefined ? {} : { token },
2204
2801
  timeoutMs: budgetMs + 500,
2205
2802
  signal: AbortSignal.timeout(budgetMs + 500)
2206
2803
  });
2207
- const report = capped(skillInstallReport2(await readSkillInstall2(install, source, deadline)));
2804
+ const report = capped(skillInstallReport2(await readSkillInstall2(install, source, deadline, directoryBy)));
2208
2805
  return JSON.stringify({
2209
2806
  systemMessage: report,
2210
2807
  hookSpecificOutput: {
@@ -2633,7 +3230,7 @@ async function readSkillLines(atomUri, config) {
2633
3230
  function counted(counts) {
2634
3231
  return [...counts.entries()].sort(([a, x], [b, y]) => y.size - x.size || a.localeCompare(b)).map(([name, wallets]) => `${name} (${wallets.size})`).join(", ");
2635
3232
  }
2636
- var plural2 = (n) => `${n} wallet${n === 1 ? "" : "s"}`;
3233
+ var plural3 = (n) => `${n} wallet${n === 1 ? "" : "s"}`;
2637
3234
  function linesSentence(usage, label, where) {
2638
3235
  if (usage === null)
2639
3236
  return null;
@@ -2661,7 +3258,7 @@ function linesSentence(usage, label, where) {
2661
3258
  ...because.size === 0 ? [] : [`because ${counted(because)}`],
2662
3259
  ...named.size === 0 ? [] : [`${outcome === "failed" ? "needs" : "with"} ${counted(named)}`]
2663
3260
  ];
2664
- parts.push([`${usage.truncated ? "at least " : ""}${plural2(wallets.size)} wrote ${outcome}`, ...detail].join(" · "));
3261
+ parts.push([`${usage.truncated ? "at least " : ""}${plural3(wallets.size)} wrote ${outcome}`, ...detail].join(" · "));
2665
3262
  }
2666
3263
  if (parts.length === 0)
2667
3264
  return null;
@@ -3843,9 +4440,9 @@ function answerOf(response) {
3843
4440
  if (typeof text !== "string")
3844
4441
  continue;
3845
4442
  try {
3846
- const parsed = JSON.parse(text);
3847
- if (typeof parsed === "object" && parsed !== null)
3848
- return parsed;
4443
+ const parsed2 = JSON.parse(text);
4444
+ if (typeof parsed2 === "object" && parsed2 !== null)
4445
+ return parsed2;
3849
4446
  } catch {
3850
4447
  continue;
3851
4448
  }
@@ -3945,7 +4542,7 @@ function appendFailure(line) {
3945
4542
  }
3946
4543
  function recordSkillFailure(call, cwd) {
3947
4544
  const classified = classifyCall(call);
3948
- if (classified === null || classified.outcome === "ok" || call.session === null)
4545
+ if (classified === null || !isFailure(classified.outcome) || call.session === null)
3949
4546
  return null;
3950
4547
  const command = call.input?.["command"];
3951
4548
  if (typeof command !== "string")
@@ -4025,19 +4622,200 @@ async function postToolUseFailure(payload) {
4025
4622
  return context === null ? null : failureOutput(context);
4026
4623
  }
4027
4624
 
4028
- // ../hook/src/worked.ts
4029
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3 } from "node:fs";
4625
+ // ../hook/src/share-ask.ts
4626
+ import { spawn } from "node:child_process";
4627
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
4030
4628
  import { dirname as dirname3, join as join4 } from "node:path";
4629
+ import { fileURLToPath } from "node:url";
4630
+
4631
+ // ../../packages/protocol/src/sharing-state.ts
4632
+ var SHARING_HOWS2 = ["init", "command", "desktop", "asked-once", "asked-always", "asked-no"];
4633
+ function sharingChoiceOf2(text) {
4634
+ if (text === null)
4635
+ return null;
4636
+ try {
4637
+ const parsed2 = JSON.parse(text);
4638
+ if (typeof parsed2 !== "object" || parsed2 === null)
4639
+ return { on: false, at: null, how: null };
4640
+ const { failures, at, how } = parsed2;
4641
+ return {
4642
+ on: failures === true,
4643
+ at: typeof at === "string" ? at : null,
4644
+ how: typeof how === "string" && SHARING_HOWS2.includes(how) ? how : null
4645
+ };
4646
+ } catch {
4647
+ return { on: false, at: null, how: null };
4648
+ }
4649
+ }
4650
+ function sharingFileText2(on, how, at) {
4651
+ return `${JSON.stringify({ failures: on, at: at.toISOString(), how })}
4652
+ `;
4653
+ }
4654
+ function choiceForAnswer2(answer) {
4655
+ return { on: answer === "always", how: `asked-${answer}` };
4656
+ }
4657
+ var ANSWER = /^[\s>*`-]*arp-share:\s*(once|always|no)[\s.`*]*$/gim;
4658
+ function shareAnswerIn2(text) {
4659
+ const found = new Set;
4660
+ for (const match of text.matchAll(ANSWER)) {
4661
+ const word = match[1]?.toLowerCase();
4662
+ if (word === "once" || word === "always" || word === "no")
4663
+ found.add(word);
4664
+ }
4665
+ return found.size === 1 ? [...found][0] ?? null : null;
4666
+ }
4667
+
4668
+ // ../hook/src/share-ask.ts
4669
+ var isOff = () => process.env["ARP_SHARE_AUTO"]?.trim().toLowerCase() === "off";
4670
+ var GITHUB_REPO = /^[\w.-]+\/[\w.-]+$/;
4671
+ var readIfThere2 = (path) => existsSync2(path) ? readFileSync3(path, "utf8") : null;
4672
+ function shareQuestionFor() {
4673
+ return " Sharing is not set on this machine yet. If that line says worked or failed, ask the person once, right after it, " + "in their language: share this result as a public line on Intuition testnet? Three choices: yes, this once; " + "always (this result and every one from now on, sent at the end of each session; earlier ones stay here); no (not asked again). Say what would leave: " + "the skill's public address, its category, worked or failed with a closed reason, signed by this machine's own " + "ARP address. Never their prompts, files, paths, code or keys. Do not choose for them. At the end of your reply to " + "their answer, add one line: `arp-share: once`, `arp-share: always` or `arp-share: no`, the one they chose; if " + "they did not answer it, add none.";
4674
+ }
4675
+ function stateOf(home) {
4676
+ const text = readIfThere2(join4(home, "share-question.json"));
4677
+ if (text === null)
4678
+ return null;
4679
+ try {
4680
+ const row = JSON.parse(text);
4681
+ if (typeof row.session !== "string" || typeof row.skill !== "string")
4682
+ return null;
4683
+ if (row.state !== "offered" && row.state !== "put" && row.state !== "answered")
4684
+ return null;
4685
+ return { at: typeof row.at === "string" ? row.at : "", session: row.session, skill: row.skill, state: row.state };
4686
+ } catch {
4687
+ return { at: "", session: "", skill: "", state: "put" };
4688
+ }
4689
+ }
4690
+ var writeState = (home, state) => writeFileSync2(join4(home, "share-question.json"), `${JSON.stringify(state)}
4691
+ `);
4692
+ function offerShare(session, skill, source, cwd, now = new Date) {
4693
+ if (isOff())
4694
+ return false;
4695
+ try {
4696
+ const home = arpHome();
4697
+ if (existsSync2(join4(home, "sharing.json")))
4698
+ return false;
4699
+ const state = stateOf(home);
4700
+ if (state !== null && (state.state !== "offered" || state.session === session))
4701
+ return false;
4702
+ const name = skill.split(":").pop() ?? skill;
4703
+ const known = source ?? installedSource(name, cwd);
4704
+ if (known === null || !GITHUB_REPO.test(known))
4705
+ return false;
4706
+ mkdirSync3(home, { recursive: true });
4707
+ writeState(home, { at: now.toISOString(), session, skill, state: "offered" });
4708
+ return true;
4709
+ } catch {
4710
+ return false;
4711
+ }
4712
+ }
4713
+ function onStopShare(session, last, recorded, now = new Date) {
4714
+ if (isOff())
4715
+ return;
4716
+ try {
4717
+ const home = arpHome();
4718
+ const state = stateOf(home);
4719
+ if (state === null || state.session !== session)
4720
+ return;
4721
+ if (state.state === "put") {
4722
+ if (existsSync2(join4(home, "sharing.json")))
4723
+ return;
4724
+ const answer = shareAnswerIn2(last);
4725
+ if (answer === null)
4726
+ return;
4727
+ const choice = choiceForAnswer2(answer);
4728
+ writeFileSync2(join4(home, "sharing.json"), sharingFileText2(choice.on, choice.how, now));
4729
+ if (answer !== "no") {
4730
+ appendOwn(join4(home, "share-once.jsonl"), `${JSON.stringify({ at: now.toISOString(), session, skill: state.skill })}
4731
+ `);
4732
+ }
4733
+ writeState(home, { ...state, state: "answered", answer, at: now.toISOString() });
4734
+ return;
4735
+ }
4736
+ if (state.state !== "offered")
4737
+ return;
4738
+ const outcome = recorded.get(state.skill);
4739
+ if (outcome === "worked" || outcome === "failed")
4740
+ writeState(home, { ...state, state: "put", at: now.toISOString() });
4741
+ } catch {}
4742
+ }
4743
+ function senderOf(here = dirname3(fileURLToPath(import.meta.url))) {
4744
+ const send = ["share", "send", "--auto"];
4745
+ const given = process.env["ARP_CLI"]?.trim();
4746
+ if (given !== undefined && given !== "")
4747
+ return { command: process.execPath, args: [given, ...send] };
4748
+ for (const name of ["arp.js", "cli.js"]) {
4749
+ if (existsSync2(join4(here, name)))
4750
+ return { command: process.execPath, args: [join4(here, name), ...send] };
4751
+ }
4752
+ const written = readIfThere2(join4(here, "send.json"));
4753
+ if (written === null)
4754
+ return null;
4755
+ try {
4756
+ const parsed2 = JSON.parse(written);
4757
+ if (typeof parsed2.command !== "string" || !Array.isArray(parsed2.args))
4758
+ return null;
4759
+ const args = parsed2.args.filter((arg) => typeof arg === "string");
4760
+ return { command: parsed2.command, args };
4761
+ } catch {
4762
+ return null;
4763
+ }
4764
+ }
4765
+ function onceFor(home, session) {
4766
+ for (const line of (readIfThere2(join4(home, "share-once.jsonl")) ?? "").split(`
4767
+ `)) {
4768
+ try {
4769
+ if (JSON.parse(line).session === session)
4770
+ return true;
4771
+ } catch {
4772
+ continue;
4773
+ }
4774
+ }
4775
+ return false;
4776
+ }
4777
+ async function sessionEnd(payload) {
4778
+ if (isOff() || typeof payload !== "object" || payload === null)
4779
+ return null;
4780
+ const raw = payload;
4781
+ const session = raw["session_id"] ?? raw["sessionId"];
4782
+ if (typeof session !== "string")
4783
+ return null;
4784
+ const cwd = typeof raw["cwd"] === "string" ? raw["cwd"] : process.cwd();
4785
+ try {
4786
+ const home = arpHome();
4787
+ const choice = sharingChoiceOf2(readIfThere2(join4(home, "sharing.json")));
4788
+ if (choice?.on !== true && !onceFor(home, session))
4789
+ return null;
4790
+ const sender = senderOf();
4791
+ if (sender === null) {
4792
+ mkdirSync3(home, { recursive: true });
4793
+ appendOwn(join4(home, "share-log.jsonl"), `${JSON.stringify({ at: new Date().toISOString(), sent: 0, failed: 0, error: "no arp program beside the hook; run arp share send" })}
4794
+ `);
4795
+ return null;
4796
+ }
4797
+ const child = spawn(sender.command, sender.args, { cwd, detached: true, stdio: "ignore", env: process.env });
4798
+ child.on("error", () => {
4799
+ return;
4800
+ });
4801
+ child.unref();
4802
+ } catch {}
4803
+ return null;
4804
+ }
4805
+
4806
+ // ../hook/src/worked.ts
4807
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync4 } from "node:fs";
4808
+ import { join as join5 } from "node:path";
4031
4809
 
4032
4810
  // ../../packages/protocol/src/local-outcomes.ts
4033
4811
  var OUTCOME_ANSWERS2 = ["worked", "failed", "unsure"];
4034
4812
  function isOutcomeAnswer2(word) {
4035
4813
  return OUTCOME_ANSWERS2.includes(word);
4036
4814
  }
4037
- var ANSWER = /^[\s>*`-]*arp:\s*([A-Za-z0-9._:@/-]+)\s+(worked|failed|unsure)[\s.`*]*$/gim;
4815
+ var ANSWER2 = /^[\s>*`-]*arp:\s*([A-Za-z0-9._:@/-]+)\s+(worked|failed|unsure)[\s.`*]*$/gim;
4038
4816
  function answersIn2(text, asked) {
4039
4817
  const found = new Map;
4040
- for (const match of text.matchAll(ANSWER)) {
4818
+ for (const match of text.matchAll(ANSWER2)) {
4041
4819
  const [, skill, word] = match;
4042
4820
  if (skill === undefined || word === undefined)
4043
4821
  continue;
@@ -4050,7 +4828,6 @@ function answersIn2(text, asked) {
4050
4828
  }
4051
4829
 
4052
4830
  // ../hook/src/worked.ts
4053
- var MAX_ASKED = 5;
4054
4831
  function isRecord5(value) {
4055
4832
  return typeof value === "object" && value !== null && !Array.isArray(value);
4056
4833
  }
@@ -4065,20 +4842,22 @@ function stopInputOf(raw) {
4065
4842
  return {
4066
4843
  session,
4067
4844
  cwd: text(raw["cwd"]) ?? process.cwd(),
4068
- active: field(raw, "stop_hook_active", "stopHookActive") === true,
4069
4845
  last: text(field(raw, "last_assistant_message", "lastAssistantMessage")) ?? "",
4070
4846
  transcript: text(field(raw, "transcript_path", "transcriptPath"))
4071
4847
  };
4072
4848
  }
4073
- function questionFor(skills) {
4074
- return "ARP asks once per skill: did each skill you used here do its job, and did the task finish with it? " + "Reply with one line per skill and nothing else: `arp: <skill> worked`, `arp: <skill> failed` or `arp: <skill> unsure`. " + `Kept on this machine unless \`arp share\` is on. Skills: ${skills.join(", ")}`;
4849
+ function requestFor(skill, share = false) {
4850
+ return `ARP: at the end of your final reply, add one line saying whether the skill ${skill} did its job: ` + `\`arp: ${skill} worked\`, \`arp: ${skill} failed\` or \`arp: ${skill} unsure\`. ` + "Kept on this machine unless `arp share` is on." + (share ? shareQuestionFor() : "");
4851
+ }
4852
+ function requestOutput(context) {
4853
+ return JSON.stringify({ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: context } });
4075
4854
  }
4076
4855
  var isArpsOwn = (name) => name === "arp" || name.endsWith(":arp");
4077
4856
  function rows(path) {
4078
- if (!existsSync2(path))
4857
+ if (!existsSync3(path))
4079
4858
  return [];
4080
4859
  const found = [];
4081
- for (const line of readFileSync3(path, "utf8").split(`
4860
+ for (const line of readFileSync4(path, "utf8").split(`
4082
4861
  `)) {
4083
4862
  if (line.trim() === "")
4084
4863
  continue;
@@ -4094,7 +4873,7 @@ function withOf(skill, source, cwd, before) {
4094
4873
  const name = skill.split(":").pop() ?? skill;
4095
4874
  let need = null;
4096
4875
  let latest = "";
4097
- for (const row of rows(join4(arpHome(), "failures.jsonl"))) {
4876
+ for (const row of rows(join5(arpHome(), "failures.jsonl"))) {
4098
4877
  const { skill: rowSkill, source: rowSource, need: rowNeed, at } = row;
4099
4878
  if (typeof rowSkill !== "string" || typeof rowNeed !== "string" || typeof at !== "string")
4100
4879
  continue;
@@ -4112,11 +4891,11 @@ function withOf(skill, source, cwd, before) {
4112
4891
  const directory = skillDirectory(name, cwd);
4113
4892
  if (directory === null)
4114
4893
  return null;
4115
- return publishableNeed3(need, readFileSync3(join4(directory, "SKILL.md"), "utf8"));
4894
+ return publishableNeed3(need, readFileSync4(join5(directory, "SKILL.md"), "utf8"));
4116
4895
  }
4117
4896
  function record(input, asked, outcome, at) {
4118
4897
  const search = foundBySearch(input.cwd, input.session, asked.skill);
4119
- const words = input.transcript !== null && existsSync2(input.transcript) ? lastUserWords(readFileSync3(input.transcript, "utf8")) : "";
4898
+ const words = input.transcript !== null && existsSync3(input.transcript) ? lastUserWords(readFileSync4(input.transcript, "utf8")) : "";
4120
4899
  const domain = search?.domain ?? contextOf(words, asked.skill);
4121
4900
  const source = asked.source ?? search?.source ?? installedSource(asked.skill.split(":").pop() ?? asked.skill, input.cwd);
4122
4901
  const named = outcome === "worked" ? withOf(asked.skill, source, input.cwd, at) : null;
@@ -4129,63 +4908,60 @@ function record(input, asked, outcome, at) {
4129
4908
  outcome,
4130
4909
  ...named === null ? {} : { with: named }
4131
4910
  };
4132
- appendOwn(join4(arpHome(), "outcomes.jsonl"), `${JSON.stringify(line)}
4911
+ appendOwn(join5(arpHome(), "outcomes.jsonl"), `${JSON.stringify(line)}
4912
+ `);
4913
+ }
4914
+ function askOnSkill(session, skill, source, cwd = process.cwd(), now = new Date) {
4915
+ if (isArpsOwn(skill))
4916
+ return null;
4917
+ try {
4918
+ const home = arpHome();
4919
+ mkdirSync4(home, { recursive: true });
4920
+ const askedPath = join5(home, "asked.jsonl");
4921
+ if (rows(askedPath).some((row2) => row2["session"] === session && row2["skill"] === skill))
4922
+ return null;
4923
+ const row = { at: now.toISOString(), session, skill, source };
4924
+ appendOwn(askedPath, `${JSON.stringify(row)}
4133
4925
  `);
4926
+ return requestFor(skill, offerShare(session, skill, source, cwd, now));
4927
+ } catch {
4928
+ return null;
4929
+ }
4134
4930
  }
4135
4931
  async function stop(payload) {
4136
4932
  const input = stopInputOf(payload);
4137
- if (input === null)
4138
- return null;
4139
- const ask = onStop(input);
4140
- return ask === null ? null : JSON.stringify(ask);
4933
+ if (input !== null)
4934
+ onStop(input);
4935
+ return null;
4141
4936
  }
4142
4937
  function onStop(input, now = new Date) {
4143
4938
  try {
4144
4939
  const home = arpHome();
4145
- mkdirSync3(home, { recursive: true });
4146
- const askedPath = join4(home, "asked.jsonl");
4147
- const asked = rows(askedPath).flatMap((row) => row["session"] === input.session && typeof row["skill"] === "string" ? [{
4940
+ const asked = rows(join5(home, "asked.jsonl")).flatMap((row) => row["session"] === input.session && typeof row["skill"] === "string" ? [{
4148
4941
  at: typeof row["at"] === "string" ? row["at"] : "",
4149
4942
  session: input.session,
4150
4943
  skill: row["skill"],
4151
4944
  source: typeof row["source"] === "string" ? row["source"] : null
4152
4945
  }] : []);
4153
- const answered = new Set(rows(join4(home, "outcomes.jsonl")).flatMap((row) => row["session"] === input.session && typeof row["skill"] === "string" ? [row["skill"]] : []));
4946
+ const answered = new Set(rows(join5(home, "outcomes.jsonl")).flatMap((row) => row["session"] === input.session && typeof row["skill"] === "string" ? [row["skill"]] : []));
4154
4947
  const pending = asked.filter((row) => !answered.has(row.skill));
4155
4948
  const at = now.toISOString();
4156
- for (const [skill, answer] of answersIn2(input.last, pending.map((row) => row.skill))) {
4949
+ const answers = answersIn2(input.last, pending.map((row) => row.skill));
4950
+ for (const [skill, answer] of answers) {
4157
4951
  const row = pending.find((entry) => entry.skill === skill);
4158
4952
  if (row !== undefined)
4159
4953
  record(input, row, answer, at);
4160
4954
  }
4161
- if (input.active)
4162
- return null;
4163
- const known = new Set(asked.map((row) => row.skill));
4164
- const log = process.env["ARP_SESSION_LOG"] ?? join4(input.cwd, ".arp", "session.jsonl");
4165
- const fresh = [];
4166
- for (const skill of skillsOfSession(log, input.session)) {
4167
- if (known.has(skill.name) || isArpsOwn(skill.name))
4168
- continue;
4169
- known.add(skill.name);
4170
- fresh.push({ at, session: input.session, skill: skill.name, source: skill.source });
4171
- }
4172
- const ask = fresh.slice(0, MAX_ASKED);
4173
- if (ask.length === 0)
4174
- return null;
4175
- mkdirSync3(dirname3(askedPath), { recursive: true });
4176
- appendOwn(askedPath, ask.map((row) => `${JSON.stringify(row)}
4177
- `).join(""));
4178
- return { decision: "block", reason: questionFor(ask.map((row) => row.skill)) };
4179
- } catch {
4180
- return null;
4181
- }
4955
+ onStopShare(input.session, input.last, answers, now);
4956
+ } catch {}
4182
4957
  }
4183
4958
 
4184
4959
  // ../hook/src/hook.ts
4185
4960
  var ROUTES = {
4186
4961
  PreToolUse: (payload) => preToolUse(payload),
4187
4962
  PostToolUseFailure: (payload) => postToolUseFailure(payload),
4188
- Stop: (payload) => stop(payload)
4963
+ Stop: (payload) => stop(payload),
4964
+ SessionEnd: (payload) => sessionEnd(payload)
4189
4965
  };
4190
4966
  async function route(handler, payload) {
4191
4967
  let out = null;
@@ -4205,12 +4981,12 @@ async function route(handler, payload) {
4205
4981
  });
4206
4982
  }
4207
4983
  function fetchedRefs(cwd) {
4208
- const path = process.env["ARP_RECEIPTS"] ?? join5(cwd, ".arp", "receipts.jsonl");
4209
- if (!existsSync3(path))
4984
+ const path = process.env["ARP_RECEIPTS"] ?? join6(cwd, ".arp", "receipts.jsonl");
4985
+ if (!existsSync4(path))
4210
4986
  return [];
4211
4987
  let text2;
4212
4988
  try {
4213
- text2 = readFileSync4(path, "utf8");
4989
+ text2 = readFileSync5(path, "utf8");
4214
4990
  } catch {
4215
4991
  return [];
4216
4992
  }
@@ -4245,7 +5021,7 @@ function responseOf(call, outcome) {
4245
5021
  } catch {
4246
5022
  bytes = 0;
4247
5023
  }
4248
- return { answered: raw !== null, bytes, failed: outcome !== "ok" };
5024
+ return { answered: raw !== null, bytes, failed: isFailure(outcome) };
4249
5025
  }
4250
5026
  function readString2(from, key) {
4251
5027
  const value = from?.[key];
@@ -4283,19 +5059,19 @@ function whatOf(call, cwd, fetched) {
4283
5059
  var raw = "";
4284
5060
  for await (const chunk of process.stdin)
4285
5061
  raw += chunk;
4286
- var parsed;
5062
+ var parsed2;
4287
5063
  try {
4288
- parsed = JSON.parse(raw);
5064
+ parsed2 = JSON.parse(raw);
4289
5065
  } catch {
4290
5066
  process.exit(0);
4291
5067
  }
4292
5068
  if (process.argv.includes("--capture"))
4293
- capture(parsed, join5(homedir3(), ".arp", "captures"));
4294
- var event = eventOf(parsed);
5069
+ capture(parsed2, join6(homedir3(), ".arp", "captures"));
5070
+ var event = eventOf(parsed2);
4295
5071
  var handler = event === null ? undefined : ROUTES[event];
4296
5072
  if (handler !== undefined)
4297
- await route(handler, parsed);
4298
- var call = toolCallOf(parsed);
5073
+ await route(handler, parsed2);
5074
+ var call = toolCallOf(parsed2);
4299
5075
  if (call === null)
4300
5076
  process.exit(0);
4301
5077
  var cwd = call.cwd ?? process.cwd();
@@ -4311,8 +5087,8 @@ if (what === null)
4311
5087
  var classified = classifyCall(call);
4312
5088
  if (classified === null)
4313
5089
  process.exit(0);
4314
- var own = join5(cwd, ".arp");
4315
- var path = process.env["ARP_SESSION_LOG"] ?? join5(own, "session.jsonl");
5090
+ var own = join6(cwd, ".arp");
5091
+ var path = process.env["ARP_SESSION_LOG"] ?? join6(own, "session.jsonl");
4316
5092
  var line = {
4317
5093
  at: new Date().toISOString(),
4318
5094
  session: call.session,
@@ -4325,13 +5101,19 @@ var line = {
4325
5101
  response: responseOf(call, classified.outcome)
4326
5102
  };
4327
5103
  try {
4328
- mkdirSync4(dirname4(path), { recursive: true });
5104
+ mkdirSync5(dirname4(path), { recursive: true });
4329
5105
  if (dirname4(path) === own && lstatSync2(own).isSymbolicLink())
4330
5106
  throw new Error(`${own} is a symbolic link`);
4331
- const ignore = join5(own, ".gitignore");
4332
- if (dirname4(path) === own && !existsSync3(ignore))
4333
- writeFileSync2(ignore, `*
5107
+ const ignore = join6(own, ".gitignore");
5108
+ if (dirname4(path) === own && !existsSync4(ignore))
5109
+ writeFileSync3(ignore, `*
4334
5110
  `, { flag: "wx" });
4335
5111
  appendOwn(path, `${JSON.stringify(line)}
4336
5112
  `);
4337
5113
  } catch {}
5114
+ if (what.kind === "skill" && call.event === "PostToolUse" && classified.outcome === "ok" && call.session !== null) {
5115
+ const request = askOnSkill(call.session, what.name, what.source, cwd);
5116
+ if (request !== null)
5117
+ process.stdout.write(`${requestOutput(request)}
5118
+ `);
5119
+ }