@arp-protocol/cli 0.0.3 → 0.0.4

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
@@ -16,7 +16,40 @@ function appendOwn(path, text) {
16
16
  // ../hook/src/hook.ts
17
17
  import { existsSync as existsSync3, lstatSync as lstatSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } 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 dirname3, join as join5 } 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")
@@ -4027,7 +4624,7 @@ async function postToolUseFailure(payload) {
4027
4624
 
4028
4625
  // ../hook/src/worked.ts
4029
4626
  import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3 } from "node:fs";
4030
- import { dirname as dirname3, join as join4 } from "node:path";
4627
+ import { join as join4 } from "node:path";
4031
4628
 
4032
4629
  // ../../packages/protocol/src/local-outcomes.ts
4033
4630
  var OUTCOME_ANSWERS2 = ["worked", "failed", "unsure"];
@@ -4050,7 +4647,6 @@ function answersIn2(text, asked) {
4050
4647
  }
4051
4648
 
4052
4649
  // ../hook/src/worked.ts
4053
- var MAX_ASKED = 5;
4054
4650
  function isRecord5(value) {
4055
4651
  return typeof value === "object" && value !== null && !Array.isArray(value);
4056
4652
  }
@@ -4065,13 +4661,15 @@ function stopInputOf(raw) {
4065
4661
  return {
4066
4662
  session,
4067
4663
  cwd: text(raw["cwd"]) ?? process.cwd(),
4068
- active: field(raw, "stop_hook_active", "stopHookActive") === true,
4069
4664
  last: text(field(raw, "last_assistant_message", "lastAssistantMessage")) ?? "",
4070
4665
  transcript: text(field(raw, "transcript_path", "transcriptPath"))
4071
4666
  };
4072
4667
  }
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(", ")}`;
4668
+ function requestFor(skill) {
4669
+ 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.";
4670
+ }
4671
+ function requestOutput(context) {
4672
+ return JSON.stringify({ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: context } });
4075
4673
  }
4076
4674
  var isArpsOwn = (name) => name === "arp" || name.endsWith(":arp");
4077
4675
  function rows(path) {
@@ -4132,19 +4730,33 @@ function record(input, asked, outcome, at) {
4132
4730
  appendOwn(join4(arpHome(), "outcomes.jsonl"), `${JSON.stringify(line)}
4133
4731
  `);
4134
4732
  }
4733
+ function askOnSkill(session, skill, source, now = new Date) {
4734
+ if (isArpsOwn(skill))
4735
+ return null;
4736
+ try {
4737
+ const home = arpHome();
4738
+ mkdirSync3(home, { recursive: true });
4739
+ const askedPath = join4(home, "asked.jsonl");
4740
+ if (rows(askedPath).some((row2) => row2["session"] === session && row2["skill"] === skill))
4741
+ return null;
4742
+ const row = { at: now.toISOString(), session, skill, source };
4743
+ appendOwn(askedPath, `${JSON.stringify(row)}
4744
+ `);
4745
+ return requestFor(skill);
4746
+ } catch {
4747
+ return null;
4748
+ }
4749
+ }
4135
4750
  async function stop(payload) {
4136
4751
  const input = stopInputOf(payload);
4137
- if (input === null)
4138
- return null;
4139
- const ask = onStop(input);
4140
- return ask === null ? null : JSON.stringify(ask);
4752
+ if (input !== null)
4753
+ onStop(input);
4754
+ return null;
4141
4755
  }
4142
4756
  function onStop(input, now = new Date) {
4143
4757
  try {
4144
4758
  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" ? [{
4759
+ const asked = rows(join4(home, "asked.jsonl")).flatMap((row) => row["session"] === input.session && typeof row["skill"] === "string" ? [{
4148
4760
  at: typeof row["at"] === "string" ? row["at"] : "",
4149
4761
  session: input.session,
4150
4762
  skill: row["skill"],
@@ -4158,27 +4770,7 @@ function onStop(input, now = new Date) {
4158
4770
  if (row !== undefined)
4159
4771
  record(input, row, answer, at);
4160
4772
  }
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
- }
4773
+ } catch {}
4182
4774
  }
4183
4775
 
4184
4776
  // ../hook/src/hook.ts
@@ -4245,7 +4837,7 @@ function responseOf(call, outcome) {
4245
4837
  } catch {
4246
4838
  bytes = 0;
4247
4839
  }
4248
- return { answered: raw !== null, bytes, failed: outcome !== "ok" };
4840
+ return { answered: raw !== null, bytes, failed: isFailure(outcome) };
4249
4841
  }
4250
4842
  function readString2(from, key) {
4251
4843
  const value = from?.[key];
@@ -4283,19 +4875,19 @@ function whatOf(call, cwd, fetched) {
4283
4875
  var raw = "";
4284
4876
  for await (const chunk of process.stdin)
4285
4877
  raw += chunk;
4286
- var parsed;
4878
+ var parsed2;
4287
4879
  try {
4288
- parsed = JSON.parse(raw);
4880
+ parsed2 = JSON.parse(raw);
4289
4881
  } catch {
4290
4882
  process.exit(0);
4291
4883
  }
4292
4884
  if (process.argv.includes("--capture"))
4293
- capture(parsed, join5(homedir3(), ".arp", "captures"));
4294
- var event = eventOf(parsed);
4885
+ capture(parsed2, join5(homedir3(), ".arp", "captures"));
4886
+ var event = eventOf(parsed2);
4295
4887
  var handler = event === null ? undefined : ROUTES[event];
4296
4888
  if (handler !== undefined)
4297
- await route(handler, parsed);
4298
- var call = toolCallOf(parsed);
4889
+ await route(handler, parsed2);
4890
+ var call = toolCallOf(parsed2);
4299
4891
  if (call === null)
4300
4892
  process.exit(0);
4301
4893
  var cwd = call.cwd ?? process.cwd();
@@ -4325,13 +4917,19 @@ var line = {
4325
4917
  response: responseOf(call, classified.outcome)
4326
4918
  };
4327
4919
  try {
4328
- mkdirSync4(dirname4(path), { recursive: true });
4329
- if (dirname4(path) === own && lstatSync2(own).isSymbolicLink())
4920
+ mkdirSync4(dirname3(path), { recursive: true });
4921
+ if (dirname3(path) === own && lstatSync2(own).isSymbolicLink())
4330
4922
  throw new Error(`${own} is a symbolic link`);
4331
4923
  const ignore = join5(own, ".gitignore");
4332
- if (dirname4(path) === own && !existsSync3(ignore))
4924
+ if (dirname3(path) === own && !existsSync3(ignore))
4333
4925
  writeFileSync2(ignore, `*
4334
4926
  `, { flag: "wx" });
4335
4927
  appendOwn(path, `${JSON.stringify(line)}
4336
4928
  `);
4337
4929
  } catch {}
4930
+ if (what.kind === "skill" && call.event === "PostToolUse" && classified.outcome === "ok" && call.session !== null) {
4931
+ const request = askOnSkill(call.session, what.name, what.source);
4932
+ if (request !== null)
4933
+ process.stdout.write(`${requestOutput(request)}
4934
+ `);
4935
+ }