@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/CHANGELOG.md +25 -1
- package/README.md +3 -0
- package/dist/chunks/{backfill-awengrsd.js → backfill-5zmh0t43.js} +3 -1
- package/dist/chunks/check-k4qnc9ee.js +2011 -0
- package/dist/chunks/cli-7c393v47.js +173 -0
- package/dist/chunks/{cli-10wacn76.js → cli-8m56jsfx.js} +15 -173
- package/dist/chunks/cli-kda5hz4w.js +23 -0
- package/dist/chunks/{cli-wev0jgf7.js → cli-wer26exb.js} +905 -974
- package/dist/chunks/cli-zehr6kdx.js +719 -0
- package/dist/chunks/{commands-157bms70.js → commands-t5a0w785.js} +12 -2
- package/dist/chunks/{line-691gbaha.js → line-kvm1ew55.js} +54 -5
- package/dist/chunks/{node-init-rcepfyeh.js → node-init-amedveja.js} +2 -1
- package/dist/chunks/{node-m0mx4y5k.js → node-x35ncwhq.js} +2 -1
- package/dist/chunks/{server-59zry2jp.js → server-ek7fet5m.js} +190 -90
- package/dist/chunks/{share-vtbkzs5f.js → share-ax2k8805.js} +3 -1
- package/dist/cli.js +34 -11
- package/dist/hook.js +746 -148
- package/package.json +2 -2
|
@@ -0,0 +1,2011 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
KEY_NAME,
|
|
4
|
+
NEEDS_LABEL,
|
|
5
|
+
PROGRAM_NAME,
|
|
6
|
+
WITH_LABEL,
|
|
7
|
+
needKindOf,
|
|
8
|
+
needKindOfReason
|
|
9
|
+
} from "./cli-kda5hz4w.js";
|
|
10
|
+
import {
|
|
11
|
+
"13579_default" as _13579_default
|
|
12
|
+
} from "./cli-7c393v47.js";
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_TIMEOUT_MS,
|
|
15
|
+
PULSE_ATOMS_QUERY,
|
|
16
|
+
PULSE_QUALIFIERS_QUERY,
|
|
17
|
+
PULSE_USES_QUERY,
|
|
18
|
+
createGraphqlTransport,
|
|
19
|
+
defaultFetch,
|
|
20
|
+
isRecord,
|
|
21
|
+
readRecord,
|
|
22
|
+
readString
|
|
23
|
+
} from "./cli-zehr6kdx.js";
|
|
24
|
+
import"./cli-g5zvr10v.js";
|
|
25
|
+
|
|
26
|
+
// src/check.ts
|
|
27
|
+
import { createHash } from "node:crypto";
|
|
28
|
+
import { existsSync, statSync } from "node:fs";
|
|
29
|
+
import { delimiter, join } from "node:path";
|
|
30
|
+
|
|
31
|
+
// ../../packages/protocol/src/content-path.ts
|
|
32
|
+
function directoryOf(path) {
|
|
33
|
+
const parts = path.split("/");
|
|
34
|
+
return parts.length < 2 ? "" : parts[parts.length - 2] ?? "";
|
|
35
|
+
}
|
|
36
|
+
function skillFilesIn(listing) {
|
|
37
|
+
return listing.filter((file) => (file.path.split("/").pop() ?? "").toLowerCase() === "skill.md");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ../../packages/protocol/src/declare/thing-cid.ts
|
|
41
|
+
function thingDocument(thing) {
|
|
42
|
+
return JSON.stringify({
|
|
43
|
+
"@context": "https://schema.org",
|
|
44
|
+
"@type": "Thing",
|
|
45
|
+
description: thing.description,
|
|
46
|
+
image: thing.image,
|
|
47
|
+
name: thing.name,
|
|
48
|
+
url: thing.url
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function base32(bytes) {
|
|
52
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyz234567";
|
|
53
|
+
let bits = 0;
|
|
54
|
+
let value = 0;
|
|
55
|
+
let out = "";
|
|
56
|
+
for (const byte of bytes) {
|
|
57
|
+
value = value << 8 | byte;
|
|
58
|
+
bits += 8;
|
|
59
|
+
while (bits >= 5) {
|
|
60
|
+
out += alphabet[value >>> bits - 5 & 31];
|
|
61
|
+
bits -= 5;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (bits > 0)
|
|
65
|
+
out += alphabet[value << 5 - bits & 31];
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
function thingUriOfDigest(digest) {
|
|
69
|
+
const cid = new Uint8Array(4 + digest.length);
|
|
70
|
+
cid.set([1, 85, 18, 32]);
|
|
71
|
+
cid.set(digest, 4);
|
|
72
|
+
return `ipfs://b${base32(cid)}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ../../packages/protocol/src/declare/tool-thing.ts
|
|
76
|
+
class InvalidToolUrlError extends Error {
|
|
77
|
+
input;
|
|
78
|
+
constructor(input, why) {
|
|
79
|
+
super(`${JSON.stringify(input)} is not a usable tool URL: ${why}`);
|
|
80
|
+
this.name = "InvalidToolUrlError";
|
|
81
|
+
this.input = input;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
var TOOL_THING_DESCRIPTION = "A tool identified by its URL, so every declaration of its use lands on one atom.";
|
|
85
|
+
function normaliseToolUrl(input) {
|
|
86
|
+
const trimmed = input.trim();
|
|
87
|
+
if (trimmed.length === 0)
|
|
88
|
+
throw new InvalidToolUrlError(input, "empty");
|
|
89
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:/i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
90
|
+
let url;
|
|
91
|
+
try {
|
|
92
|
+
url = new URL(withScheme);
|
|
93
|
+
} catch {
|
|
94
|
+
throw new InvalidToolUrlError(input, "not parseable as a URL");
|
|
95
|
+
}
|
|
96
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
97
|
+
throw new InvalidToolUrlError(input, `scheme ${url.protocol} is not http or https`);
|
|
98
|
+
}
|
|
99
|
+
if (url.hostname.length === 0)
|
|
100
|
+
throw new InvalidToolUrlError(input, "no host");
|
|
101
|
+
const host = url.hostname.replace(/\.+$/, "").replace(/^(?:www\.)+/, "");
|
|
102
|
+
const path = url.pathname.replace(/(?:\.git|\/)+$/, "");
|
|
103
|
+
const params = [...new URLSearchParams(url.search).entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
104
|
+
const query = params.length === 0 ? "" : `?${new URLSearchParams(params).toString()}`;
|
|
105
|
+
const port = url.port === "" || url.port === "80" || url.port === "443" ? "" : `:${url.port}`;
|
|
106
|
+
return `https://${host}${port}${path}${query}`;
|
|
107
|
+
}
|
|
108
|
+
function toolThing(url) {
|
|
109
|
+
const normalised = normaliseToolUrl(url);
|
|
110
|
+
return {
|
|
111
|
+
name: normalised.slice("https://".length),
|
|
112
|
+
description: TOOL_THING_DESCRIPTION,
|
|
113
|
+
image: "",
|
|
114
|
+
url: normalised
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ../../packages/protocol/src/declare/usage-words.ts
|
|
119
|
+
var USAGE_OUTCOMES = ["worked", "failed"];
|
|
120
|
+
var HAS_OUTCOME_LABEL = "has outcome";
|
|
121
|
+
var USAGE_REASONS = ["missing-key", "missing-program", "missing-file", "not-the-right-tool", "no-delivery"];
|
|
122
|
+
var BECAUSE_LABEL = "because";
|
|
123
|
+
|
|
124
|
+
// ../../packages/protocol/src/sources/intuition/terms.ts
|
|
125
|
+
var USES = "0x5c0bde1cc696456c0268248c4656acdf9621fdb39e605bc99b0a83dc8ff6e800";
|
|
126
|
+
|
|
127
|
+
// ../../packages/protocol/src/pulse-usage.ts
|
|
128
|
+
var DEFAULT_LIMIT = 250;
|
|
129
|
+
var rowsOf = (data) => Array.isArray(data["triples"]) ? data["triples"].filter(isRecord) : [];
|
|
130
|
+
async function findToolAtoms(uris, config) {
|
|
131
|
+
const out = new Map;
|
|
132
|
+
if (uris.length === 0)
|
|
133
|
+
return out;
|
|
134
|
+
const transport = config.transport ?? createGraphqlTransport({
|
|
135
|
+
url: config.graphqlUrl,
|
|
136
|
+
fetch: config.fetch ?? defaultFetch(),
|
|
137
|
+
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
138
|
+
});
|
|
139
|
+
const data = await transport.request(PULSE_ATOMS_QUERY, { uris: [...uris], limit: uris.length });
|
|
140
|
+
const rows = Array.isArray(data["atoms"]) ? data["atoms"].filter(isRecord) : [];
|
|
141
|
+
for (const row of rows) {
|
|
142
|
+
const id = readString(row, "term_id");
|
|
143
|
+
const uri = readString(row, "data");
|
|
144
|
+
if (id !== null && uri !== null)
|
|
145
|
+
out.set(uri, id);
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
async function getPulseUsage(toolAtomIds, config) {
|
|
150
|
+
const transport = config.transport ?? createGraphqlTransport({
|
|
151
|
+
url: config.graphqlUrl,
|
|
152
|
+
fetch: config.fetch ?? defaultFetch(),
|
|
153
|
+
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
154
|
+
});
|
|
155
|
+
const limit = Math.min(config.limit ?? DEFAULT_LIMIT, DEFAULT_LIMIT);
|
|
156
|
+
const key = (id) => id.toLowerCase();
|
|
157
|
+
const empty = () => ({
|
|
158
|
+
declarations: [],
|
|
159
|
+
agents: 0,
|
|
160
|
+
wallets: 0,
|
|
161
|
+
byDomain: {},
|
|
162
|
+
byOutcome: {},
|
|
163
|
+
byReason: {},
|
|
164
|
+
byNeed: {},
|
|
165
|
+
byWith: {},
|
|
166
|
+
byOthers: [],
|
|
167
|
+
truncated: false
|
|
168
|
+
});
|
|
169
|
+
const out = new Map(toolAtomIds.map((id) => [key(id), empty()]));
|
|
170
|
+
if (toolAtomIds.length === 0)
|
|
171
|
+
return out;
|
|
172
|
+
const edgeRows = rowsOf(await transport.request(PULSE_USES_QUERY, { tools: [...toolAtomIds], uses: USES, limit }));
|
|
173
|
+
const edges = new Map;
|
|
174
|
+
const NESTED_CAP = 100;
|
|
175
|
+
let truncated = edgeRows.length >= limit;
|
|
176
|
+
for (const row of edgeRows) {
|
|
177
|
+
const tripleId = readString(row, "term_id");
|
|
178
|
+
const tool = readString(row, "object_id");
|
|
179
|
+
const writer = readString(row, "creator_id");
|
|
180
|
+
const subject = readRecord(row, "subject");
|
|
181
|
+
const atomId = subject === null ? null : readString(subject, "term_id");
|
|
182
|
+
if (tripleId === null || tool === null || writer === null || subject === null || atomId === null)
|
|
183
|
+
continue;
|
|
184
|
+
const edgeHolders = (Array.isArray(row["positions"]) ? row["positions"].filter(isRecord) : []).map((holder) => readString(holder, "account_id")).filter((account) => account !== null);
|
|
185
|
+
const declaration = (wallet, declaredAt) => ({
|
|
186
|
+
tripleId,
|
|
187
|
+
agent: { atomId, label: readString(subject, "label") },
|
|
188
|
+
writer: wallet,
|
|
189
|
+
holders: edgeHolders,
|
|
190
|
+
declaredAt,
|
|
191
|
+
domains: [],
|
|
192
|
+
outcomes: [],
|
|
193
|
+
lines: []
|
|
194
|
+
});
|
|
195
|
+
const declarers = new Map([[key(writer), declaration(writer, readString(row, "created_at"))]]);
|
|
196
|
+
const holders = Array.isArray(row["positions"]) ? row["positions"].filter(isRecord) : [];
|
|
197
|
+
if (holders.length >= NESTED_CAP)
|
|
198
|
+
truncated = true;
|
|
199
|
+
for (const holder of holders) {
|
|
200
|
+
const wallet = readString(holder, "account_id");
|
|
201
|
+
if (wallet !== null && !declarers.has(key(wallet)))
|
|
202
|
+
declarers.set(key(wallet), declaration(wallet, null));
|
|
203
|
+
}
|
|
204
|
+
edges.set(key(tripleId), { tool: key(tool), declarers });
|
|
205
|
+
}
|
|
206
|
+
const payers = new Set(config.payers.map(key));
|
|
207
|
+
const writers = [...new Set([...edges.values()].flatMap((edge) => [...edge.declarers.values()].map((row) => row.writer)))];
|
|
208
|
+
const qualifiers = async (subjects, predicates) => subjects.length === 0 ? [] : rowsOf(await transport.request(PULSE_QUALIFIERS_QUERY, { edges: subjects, predicates, writers, limit }));
|
|
209
|
+
const pass = (rows) => {
|
|
210
|
+
truncated = truncated || rows.length >= limit;
|
|
211
|
+
return rows;
|
|
212
|
+
};
|
|
213
|
+
const standing = (row, edge, below) => {
|
|
214
|
+
const holders = Array.isArray(row["positions"]) ? row["positions"].filter(isRecord) : [];
|
|
215
|
+
if (holders.length >= NESTED_CAP)
|
|
216
|
+
truncated = true;
|
|
217
|
+
const wallets = [readString(row, "creator_id"), ...holders.map((holder) => readString(holder, "account_id"))];
|
|
218
|
+
return new Set(wallets.filter((wallet) => wallet !== null).map(key).filter((wallet) => edge.declarers.has(wallet) && !payers.has(wallet) && (below === undefined || below.has(wallet))));
|
|
219
|
+
};
|
|
220
|
+
const parts = (row) => {
|
|
221
|
+
const object = readRecord(row, "object");
|
|
222
|
+
return {
|
|
223
|
+
termId: readString(row, "term_id"),
|
|
224
|
+
subjectId: readString(row, "subject_id"),
|
|
225
|
+
predicateId: readString(row, "predicate_id"),
|
|
226
|
+
writer: readString(row, "creator_id"),
|
|
227
|
+
value: object === null ? null : readString(object, "label")
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
const isOutcome = (value) => USAGE_OUTCOMES.some((word) => word === value);
|
|
231
|
+
const isReason = (value) => USAGE_REASONS.some((word) => word === value);
|
|
232
|
+
const contexts = new Map;
|
|
233
|
+
const outcomes = new Map;
|
|
234
|
+
const others = (edge, row, predicate) => out.get(edge.tool)?.byOthers.push({
|
|
235
|
+
tripleId: row.termId ?? "",
|
|
236
|
+
writer: row.writer ?? "",
|
|
237
|
+
predicate,
|
|
238
|
+
value: row.value ?? ""
|
|
239
|
+
});
|
|
240
|
+
const onEdge = pass(await qualifiers([...edges.keys()], [config.inContextOf, config.hasOutcome]));
|
|
241
|
+
for (const row of onEdge) {
|
|
242
|
+
const line = parts(row);
|
|
243
|
+
const edge = line.subjectId === null ? undefined : edges.get(key(line.subjectId));
|
|
244
|
+
if (edge === undefined || line.predicateId === null || line.writer === null || line.value === null || line.termId === null)
|
|
245
|
+
continue;
|
|
246
|
+
const isOutcomeLine = key(line.predicateId) === key(config.hasOutcome);
|
|
247
|
+
if (isOutcomeLine && !isOutcome(line.value))
|
|
248
|
+
continue;
|
|
249
|
+
const wallets = standing(row, edge);
|
|
250
|
+
if (wallets.size === 0) {
|
|
251
|
+
others(edge, line, isOutcomeLine ? "outcome" : "context");
|
|
252
|
+
} else if (isOutcomeLine && isOutcome(line.value)) {
|
|
253
|
+
outcomes.set(key(line.termId), { edge, domain: null, form: "sibling", outcome: line.value, wallets, reasons: new Map, named: new Map });
|
|
254
|
+
} else {
|
|
255
|
+
contexts.set(key(line.termId), { edge, domain: line.value, wallets });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const onContext = pass(await qualifiers([...contexts.keys()], [config.hasOutcome]));
|
|
259
|
+
for (const row of onContext) {
|
|
260
|
+
const line = parts(row);
|
|
261
|
+
const context = line.subjectId === null ? undefined : contexts.get(key(line.subjectId));
|
|
262
|
+
if (context === undefined || line.value === null || line.termId === null || !isOutcome(line.value))
|
|
263
|
+
continue;
|
|
264
|
+
const wallets = standing(row, context.edge, context.wallets);
|
|
265
|
+
if (wallets.size === 0) {
|
|
266
|
+
others(context.edge, line, "outcome");
|
|
267
|
+
} else {
|
|
268
|
+
outcomes.set(key(line.termId), { edge: context.edge, domain: context.domain, form: "nested", outcome: line.value, wallets, reasons: new Map, named: new Map });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const reading = (predicate) => predicate !== undefined;
|
|
272
|
+
const failures = [...outcomes.entries()].filter(([, found]) => found.outcome === "failed").map(([id]) => id);
|
|
273
|
+
const worked = [...outcomes.entries()].filter(([, found]) => found.outcome === "worked").map(([id]) => id);
|
|
274
|
+
const withId = config.with;
|
|
275
|
+
const onOutcome = pass(await qualifiers(reading(withId) ? [...failures, ...worked] : failures, [config.because, ...reading(withId) ? [withId] : []]));
|
|
276
|
+
const reasonTerms = new Map;
|
|
277
|
+
const addName = (found, wallets, name) => {
|
|
278
|
+
for (const wallet of wallets) {
|
|
279
|
+
const given = found.named.get(wallet) ?? [];
|
|
280
|
+
if (!given.includes(name))
|
|
281
|
+
given.push(name);
|
|
282
|
+
found.named.set(wallet, given);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
for (const row of onOutcome) {
|
|
286
|
+
const line = parts(row);
|
|
287
|
+
const found = line.subjectId === null ? undefined : outcomes.get(key(line.subjectId));
|
|
288
|
+
if (found === undefined || line.value === null || line.predicateId === null)
|
|
289
|
+
continue;
|
|
290
|
+
if (withId !== undefined && key(line.predicateId) === key(withId)) {
|
|
291
|
+
if (found.outcome !== "worked" || needKindOf(line.value) === null)
|
|
292
|
+
continue;
|
|
293
|
+
const wallets2 = standing(row, found.edge, found.wallets);
|
|
294
|
+
if (wallets2.size === 0)
|
|
295
|
+
others(found.edge, line, "with");
|
|
296
|
+
addName(found, wallets2, line.value);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (found.outcome !== "failed" || !isReason(line.value))
|
|
300
|
+
continue;
|
|
301
|
+
const wallets = standing(row, found.edge, found.wallets);
|
|
302
|
+
if (wallets.size === 0)
|
|
303
|
+
others(found.edge, line, "reason");
|
|
304
|
+
for (const wallet of wallets) {
|
|
305
|
+
const given = found.reasons.get(wallet) ?? [];
|
|
306
|
+
if (!given.includes(line.value))
|
|
307
|
+
given.push(line.value);
|
|
308
|
+
found.reasons.set(wallet, given);
|
|
309
|
+
}
|
|
310
|
+
if (line.termId !== null && wallets.size > 0 && needKindOfReason(line.value) !== null) {
|
|
311
|
+
reasonTerms.set(key(line.termId), { found, reason: line.value, wallets });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const needsId = config.needs;
|
|
315
|
+
if (reading(needsId)) {
|
|
316
|
+
const onReason = pass(await qualifiers([...reasonTerms.keys()], [needsId]));
|
|
317
|
+
for (const row of onReason) {
|
|
318
|
+
const line = parts(row);
|
|
319
|
+
const reason = line.subjectId === null ? undefined : reasonTerms.get(key(line.subjectId));
|
|
320
|
+
if (reason === undefined || line.value === null)
|
|
321
|
+
continue;
|
|
322
|
+
const kind = needKindOf(line.value);
|
|
323
|
+
if (kind === null || kind !== needKindOfReason(reason.reason))
|
|
324
|
+
continue;
|
|
325
|
+
const wallets = standing(row, reason.found.edge, reason.wallets);
|
|
326
|
+
if (wallets.size === 0)
|
|
327
|
+
others(reason.found.edge, line, "needs");
|
|
328
|
+
addName(reason.found, wallets, line.value);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
for (const { edge, domain, wallets } of contexts.values()) {
|
|
332
|
+
for (const wallet of wallets)
|
|
333
|
+
edge.declarers.get(wallet)?.domains.push(domain);
|
|
334
|
+
}
|
|
335
|
+
const ordered = [...outcomes.values()].sort((a, b) => Number(b.form === "nested") - Number(a.form === "nested"));
|
|
336
|
+
for (const found of ordered) {
|
|
337
|
+
for (const wallet of found.wallets) {
|
|
338
|
+
const declaration = found.edge.declarers.get(wallet);
|
|
339
|
+
if (declaration === undefined)
|
|
340
|
+
continue;
|
|
341
|
+
const reasons = found.reasons.get(wallet) ?? [];
|
|
342
|
+
const named = found.named.get(wallet) ?? [];
|
|
343
|
+
const field = found.outcome === "failed" ? "needs" : "with";
|
|
344
|
+
if (!declaration.outcomes.includes(found.outcome))
|
|
345
|
+
declaration.outcomes.push(found.outcome);
|
|
346
|
+
const domains = found.form === "nested" ? [found.domain] : declaration.domains.length > 0 ? declaration.domains : [null];
|
|
347
|
+
for (const domain of domains) {
|
|
348
|
+
const same = declaration.lines.find((line) => line.domain === domain && line.outcome === found.outcome);
|
|
349
|
+
if (same === undefined) {
|
|
350
|
+
declaration.lines.push({ domain, outcome: found.outcome, reasons, form: found.form, ...named.length === 0 ? {} : { [field]: named } });
|
|
351
|
+
} else {
|
|
352
|
+
same.reasons = [...new Set([...same.reasons, ...reasons])];
|
|
353
|
+
if (named.length > 0)
|
|
354
|
+
same[field] = [...new Set([...same[field] ?? [], ...named])];
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
for (const { tool, declarers } of edges.values()) {
|
|
360
|
+
const rows = [...declarers.values()].filter((row, at) => at === 0 || row.domains.length > 0 || row.lines.length > 0);
|
|
361
|
+
out.get(tool)?.declarations.push(...rows);
|
|
362
|
+
}
|
|
363
|
+
for (const usage of out.values()) {
|
|
364
|
+
usage.truncated = truncated;
|
|
365
|
+
usage.agents = new Set(usage.declarations.map((declaration) => key(declaration.agent.atomId))).size;
|
|
366
|
+
usage.wallets = new Set(usage.declarations.map((declaration) => key(declaration.writer)).filter((wallet) => !payers.has(wallet))).size;
|
|
367
|
+
for (const declaration of usage.declarations) {
|
|
368
|
+
for (const domain of declaration.domains)
|
|
369
|
+
usage.byDomain[domain] = (usage.byDomain[domain] ?? 0) + 1;
|
|
370
|
+
for (const outcome of declaration.outcomes)
|
|
371
|
+
usage.byOutcome[outcome] = (usage.byOutcome[outcome] ?? 0) + 1;
|
|
372
|
+
for (const reason of new Set(declaration.lines.flatMap((line) => line.reasons))) {
|
|
373
|
+
usage.byReason[reason] = (usage.byReason[reason] ?? 0) + 1;
|
|
374
|
+
}
|
|
375
|
+
const byNeed = usage.byNeed ??= {};
|
|
376
|
+
for (const name of new Set(declaration.lines.flatMap((line) => line.needs ?? [])))
|
|
377
|
+
byNeed[name] = (byNeed[name] ?? 0) + 1;
|
|
378
|
+
const byWith = usage.byWith ??= {};
|
|
379
|
+
for (const name of new Set(declaration.lines.flatMap((line) => line.with ?? [])))
|
|
380
|
+
byWith[name] = (byWith[name] ?? 0) + 1;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return out;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ../../packages/protocol/src/failure-lines.ts
|
|
387
|
+
function skillAtom(url, sha256) {
|
|
388
|
+
const thing = toolThing(url);
|
|
389
|
+
return { uri: thingUriOfDigest(sha256(thingDocument(thing))), label: thing.name };
|
|
390
|
+
}
|
|
391
|
+
async function readSkillLines(atomUri, config) {
|
|
392
|
+
const words = [HAS_OUTCOME_LABEL, BECAUSE_LABEL, NEEDS_LABEL, WITH_LABEL];
|
|
393
|
+
const atoms = await findToolAtoms([atomUri, ...words], config);
|
|
394
|
+
const tool = atoms.get(atomUri);
|
|
395
|
+
const hasOutcome = atoms.get(HAS_OUTCOME_LABEL);
|
|
396
|
+
const because = atoms.get(BECAUSE_LABEL);
|
|
397
|
+
if (tool === undefined || hasOutcome === undefined || because === undefined)
|
|
398
|
+
return null;
|
|
399
|
+
const needs = atoms.get(NEEDS_LABEL);
|
|
400
|
+
const withId = atoms.get(WITH_LABEL);
|
|
401
|
+
const usage = await getPulseUsage([tool], {
|
|
402
|
+
...config,
|
|
403
|
+
hasOutcome,
|
|
404
|
+
because,
|
|
405
|
+
...needs === undefined ? {} : { needs },
|
|
406
|
+
...withId === undefined ? {} : { with: withId }
|
|
407
|
+
});
|
|
408
|
+
return usage.get(tool.toLowerCase()) ?? null;
|
|
409
|
+
}
|
|
410
|
+
function counted(counts) {
|
|
411
|
+
return [...counts.entries()].sort(([a, x], [b, y]) => y.size - x.size || a.localeCompare(b)).map(([name, wallets]) => `${name} (${wallets.size})`).join(", ");
|
|
412
|
+
}
|
|
413
|
+
var plural = (n) => `${n} wallet${n === 1 ? "" : "s"}`;
|
|
414
|
+
function linesSentence(usage, label, where) {
|
|
415
|
+
if (usage === null)
|
|
416
|
+
return null;
|
|
417
|
+
const parts = [];
|
|
418
|
+
for (const outcome of ["failed", "worked"]) {
|
|
419
|
+
const wallets = new Set;
|
|
420
|
+
const because = new Map;
|
|
421
|
+
const named = new Map;
|
|
422
|
+
const add = (to, name, wallet) => to.set(name, (to.get(name) ?? new Set).add(wallet));
|
|
423
|
+
for (const row of usage.declarations) {
|
|
424
|
+
const wallet = row.writer.toLowerCase();
|
|
425
|
+
for (const line of row.lines) {
|
|
426
|
+
if (line.outcome !== outcome)
|
|
427
|
+
continue;
|
|
428
|
+
wallets.add(wallet);
|
|
429
|
+
for (const reason of line.reasons)
|
|
430
|
+
add(because, reason, wallet);
|
|
431
|
+
for (const name of (outcome === "failed" ? line.needs : line.with) ?? [])
|
|
432
|
+
add(named, name, wallet);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (wallets.size === 0)
|
|
436
|
+
continue;
|
|
437
|
+
const detail = [
|
|
438
|
+
...because.size === 0 ? [] : [`because ${counted(because)}`],
|
|
439
|
+
...named.size === 0 ? [] : [`${outcome === "failed" ? "needs" : "with"} ${counted(named)}`]
|
|
440
|
+
];
|
|
441
|
+
parts.push([`${usage.truncated ? "at least " : ""}${plural(wallets.size)} wrote ${outcome}`, ...detail].join(" · "));
|
|
442
|
+
}
|
|
443
|
+
if (parts.length === 0)
|
|
444
|
+
return null;
|
|
445
|
+
return `ARP, lines agents wrote on ${label} (${where}): ${parts.join("; ")}.`;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ../../packages/protocol/src/skill-requires.ts
|
|
449
|
+
var LOGIN = /\b(auth login|log ?in|sign ?up|api[ _-]?key|access token)\b/i;
|
|
450
|
+
function frontmatter(markdown) {
|
|
451
|
+
const lines = markdown.split(`
|
|
452
|
+
`);
|
|
453
|
+
if (lines[0]?.trim() !== "---")
|
|
454
|
+
return { lines: [], bodyStart: 0 };
|
|
455
|
+
const end = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
|
|
456
|
+
return end === -1 ? { lines: [], bodyStart: 0 } : { lines: lines.slice(1, end), bodyStart: end + 1 };
|
|
457
|
+
}
|
|
458
|
+
function unquote(value) {
|
|
459
|
+
return value.trim().replace(/^["']|["']$/g, "");
|
|
460
|
+
}
|
|
461
|
+
function declaredBins(lines) {
|
|
462
|
+
const at = lines.findIndex((line) => /^\s*bins\s*:/.test(line));
|
|
463
|
+
if (at === -1)
|
|
464
|
+
return [];
|
|
465
|
+
const inline = lines[at]?.split(":").slice(1).join(":").trim() ?? "";
|
|
466
|
+
if (inline.startsWith("[")) {
|
|
467
|
+
return inline.replace(/^\[|\]$/g, "").split(",").map(unquote).filter((name) => name.length > 0);
|
|
468
|
+
}
|
|
469
|
+
const out = [];
|
|
470
|
+
for (const line of lines.slice(at + 1)) {
|
|
471
|
+
const item = /^\s*-\s*(.+)$/.exec(line);
|
|
472
|
+
if (item === null)
|
|
473
|
+
break;
|
|
474
|
+
out.push(unquote(item[1] ?? ""));
|
|
475
|
+
}
|
|
476
|
+
return out.filter((name) => name.length > 0);
|
|
477
|
+
}
|
|
478
|
+
function bashTools(lines) {
|
|
479
|
+
const line = lines.find((candidate) => /^\s*allowed-tools\s*:/.test(candidate));
|
|
480
|
+
if (line === undefined)
|
|
481
|
+
return [];
|
|
482
|
+
return [...line.matchAll(/Bash\(\s*([A-Za-z0-9._-]+)/g)].map((match) => match[1] ?? "").filter((name) => name.length > 0);
|
|
483
|
+
}
|
|
484
|
+
function declaredDescription(lines) {
|
|
485
|
+
const at = lines.findIndex((line) => /^description\s*:/.test(line));
|
|
486
|
+
if (at === -1)
|
|
487
|
+
return null;
|
|
488
|
+
const inline = lines[at]?.split(":").slice(1).join(":").trim() ?? "";
|
|
489
|
+
if (inline !== "" && !/^[>|][-+]?$/.test(inline))
|
|
490
|
+
return unquote(inline) || null;
|
|
491
|
+
const block = [];
|
|
492
|
+
for (const line of lines.slice(at + 1)) {
|
|
493
|
+
if (!/^\s+\S/.test(line))
|
|
494
|
+
break;
|
|
495
|
+
block.push(line.trim());
|
|
496
|
+
}
|
|
497
|
+
return block.length === 0 ? null : block.join(" ");
|
|
498
|
+
}
|
|
499
|
+
var COMMON = new Set(("awk bash break bun case cat cd chmod command continue cp curl cut date diff do done echo else end env " + "esac exit export false fi find for function git grep head if kill ln local ls mkdir mv node npm npx open pip pip3 pnpm printf pwd " + "python python3 read return rm sed set sh shift sleep sort source sudo tail tar tee test then time " + "touch tr true uniq unset unzip wait wc wget which while xargs yarn zip").split(" "));
|
|
500
|
+
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;
|
|
501
|
+
var PROBE = /\b(?:command\s+-v|which)\s+([a-z][\w-]*)/i;
|
|
502
|
+
var COMMAND = /^[a-z][a-z0-9_-]*$/;
|
|
503
|
+
function shellSegments(line) {
|
|
504
|
+
const segments = [];
|
|
505
|
+
let current = "";
|
|
506
|
+
let quote = null;
|
|
507
|
+
for (let index = 0;index < line.length; index += 1) {
|
|
508
|
+
const char = line[index] ?? "";
|
|
509
|
+
if (char === "\\" && quote !== "'") {
|
|
510
|
+
current += char + (line[index + 1] ?? "");
|
|
511
|
+
index += 1;
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
if (quote !== null) {
|
|
515
|
+
if (char === quote)
|
|
516
|
+
quote = null;
|
|
517
|
+
current += char;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
if (char === '"' || char === "'") {
|
|
521
|
+
quote = char;
|
|
522
|
+
current += char;
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (char === "#" && (index === 0 || /\s/.test(line[index - 1] ?? "")))
|
|
526
|
+
break;
|
|
527
|
+
if (char === "|" || char === ";" || char === "&" && line[index + 1] === "&") {
|
|
528
|
+
segments.push(current);
|
|
529
|
+
current = "";
|
|
530
|
+
if (line[index + 1] === char)
|
|
531
|
+
index += 1;
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
current += char;
|
|
535
|
+
}
|
|
536
|
+
segments.push(current);
|
|
537
|
+
return segments;
|
|
538
|
+
}
|
|
539
|
+
function commandsIn(shell, explicitOnly) {
|
|
540
|
+
const found = [];
|
|
541
|
+
for (const segment of shellSegments(shell)) {
|
|
542
|
+
const line = segment.trim().replace(/^\$\s+/, "");
|
|
543
|
+
if (line === "" || line.startsWith("#"))
|
|
544
|
+
continue;
|
|
545
|
+
const named = INSTALL.exec(line)?.[1] ?? PROBE.exec(line)?.[1];
|
|
546
|
+
if (named !== undefined) {
|
|
547
|
+
found.push(named);
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
if (explicitOnly)
|
|
551
|
+
continue;
|
|
552
|
+
const words = line.split(/\s+/).filter((word) => !/^[A-Za-z_][A-Za-z0-9_]*=/.test(word));
|
|
553
|
+
const first = words[0] === "sudo" ? words[1] : words[0];
|
|
554
|
+
if (first === undefined || !COMMAND.test(first))
|
|
555
|
+
continue;
|
|
556
|
+
found.push(first);
|
|
557
|
+
}
|
|
558
|
+
return found;
|
|
559
|
+
}
|
|
560
|
+
var SHELL_FENCE = /^(sh|bash|shell|zsh|console|terminal)$/i;
|
|
561
|
+
function shellLinesRun(lines) {
|
|
562
|
+
const run = [];
|
|
563
|
+
let heredocEnd = null;
|
|
564
|
+
let quote = null;
|
|
565
|
+
let continued = false;
|
|
566
|
+
for (const line of lines) {
|
|
567
|
+
if (heredocEnd !== null) {
|
|
568
|
+
if (line.trim() === heredocEnd)
|
|
569
|
+
heredocEnd = null;
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
if (quote === null && !continued)
|
|
573
|
+
run.push(line);
|
|
574
|
+
continued = line.trimEnd().endsWith("\\");
|
|
575
|
+
heredocEnd = /<<-?\s*['"]?([A-Za-z_]+)/.exec(line)?.[1] ?? null;
|
|
576
|
+
for (const char of quote === null ? shellSegments(line).join(";") : line) {
|
|
577
|
+
if (quote === null && (char === '"' || char === "'"))
|
|
578
|
+
quote = char;
|
|
579
|
+
else if (char === quote)
|
|
580
|
+
quote = null;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return run;
|
|
584
|
+
}
|
|
585
|
+
function commandsRun(body, declared) {
|
|
586
|
+
const found = [];
|
|
587
|
+
let fence = null;
|
|
588
|
+
let fenced = [];
|
|
589
|
+
for (const line of body) {
|
|
590
|
+
const marker = /^\s*(?:```|~~~)\s*([\w-]*)/.exec(line);
|
|
591
|
+
if (marker !== null) {
|
|
592
|
+
if (fence === "shell") {
|
|
593
|
+
for (const run of shellLinesRun(fenced))
|
|
594
|
+
found.push(...commandsIn(run, false));
|
|
595
|
+
}
|
|
596
|
+
fenced = [];
|
|
597
|
+
fence = fence !== null ? null : SHELL_FENCE.test(marker[1] ?? "") ? "shell" : "other";
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
if (fence !== null) {
|
|
601
|
+
fenced.push(line);
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
for (const inline of line.matchAll(/`([^`]+)`/g))
|
|
605
|
+
found.push(...commandsIn(inline[1] ?? "", true));
|
|
606
|
+
}
|
|
607
|
+
return [...new Set(found)].filter((name) => !COMMON.has(name) && !declared.includes(name));
|
|
608
|
+
}
|
|
609
|
+
function requiresLine(lines) {
|
|
610
|
+
const name = lines.map((line) => /^\s*requires\s*:\s*(\S+)\s*$/.exec(line)?.[1]).find(Boolean);
|
|
611
|
+
return name === undefined ? [] : [unquote(name)];
|
|
612
|
+
}
|
|
613
|
+
function readRequires(markdown) {
|
|
614
|
+
const { lines, bodyStart } = frontmatter(markdown);
|
|
615
|
+
const cli = [...new Set([...declaredBins(lines), ...bashTools(lines), ...requiresLine(lines)])];
|
|
616
|
+
const description = declaredDescription(lines);
|
|
617
|
+
const body = markdown.split(`
|
|
618
|
+
`);
|
|
619
|
+
const runs = commandsRun(body.slice(bodyStart), cli);
|
|
620
|
+
for (let index = bodyStart;index < body.length; index += 1) {
|
|
621
|
+
const text = body[index] ?? "";
|
|
622
|
+
if (LOGIN.test(text)) {
|
|
623
|
+
return { cli, runs, login: { line: index + 1, text: text.trim().slice(0, 160) }, description };
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return { cli, runs, login: null, description };
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ../../packages/protocol/src/skill-needs.ts
|
|
630
|
+
var MARKDOWN = /\.(md|mdx|markdown)$/i;
|
|
631
|
+
var SCRIPT = /\.(sh|bash|zsh|py|js|mjs|cjs|ts|rb|pl|ps1|php|lua)$/i;
|
|
632
|
+
var COMMENT_LINE = /^\s*(?:#|\/\/|\*|--|;|rem\s)/i;
|
|
633
|
+
function isTestFile(path) {
|
|
634
|
+
return /(^|\/)(tests?|__tests__|spec|fixtures?)\/|\.(test|spec)\.[a-z]+$|(^|\/)test_[^/]+\.py$|_test\.(py|go|rb)$/i.test(path);
|
|
635
|
+
}
|
|
636
|
+
function isSecretName(name) {
|
|
637
|
+
return /(^|_)(API_?KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|PASS|CREDENTIALS|CREDS)$/.test(name);
|
|
638
|
+
}
|
|
639
|
+
var SET_IN_SKILL_MD = /(?:^|\s|`)(?:export\s+|set\s+|\$env:)?([A-Z][A-Z0-9_]{2,})=/g;
|
|
640
|
+
function lineAt(text, index) {
|
|
641
|
+
let line = 1;
|
|
642
|
+
for (let at = text.indexOf(`
|
|
643
|
+
`);at !== -1 && at < index; at = text.indexOf(`
|
|
644
|
+
`, at + 1))
|
|
645
|
+
line += 1;
|
|
646
|
+
return line;
|
|
647
|
+
}
|
|
648
|
+
function keysIn(files, settings) {
|
|
649
|
+
const skillMd = files.find((file) => file.path === "SKILL.md");
|
|
650
|
+
const toSet = new Set;
|
|
651
|
+
if (skillMd !== undefined)
|
|
652
|
+
for (const match of skillMd.text.matchAll(SET_IN_SKILL_MD))
|
|
653
|
+
toSet.add(match[1] ?? "");
|
|
654
|
+
const wanted = new Set(settings.filter((name) => isSecretName(name) || toSet.has(name)));
|
|
655
|
+
const ordered = [
|
|
656
|
+
...skillMd === undefined ? [] : [skillMd],
|
|
657
|
+
...files.filter((file) => file.path !== "SKILL.md" && !isTestFile(file.path))
|
|
658
|
+
];
|
|
659
|
+
const found = new Map;
|
|
660
|
+
for (const file of ordered) {
|
|
661
|
+
for (const [index, line] of file.text.split(`
|
|
662
|
+
`).entries()) {
|
|
663
|
+
for (const match of line.matchAll(/\b[A-Z][A-Z0-9_]{2,}\b/g)) {
|
|
664
|
+
const name = match[0];
|
|
665
|
+
if (!wanted.has(name) || found.has(name))
|
|
666
|
+
continue;
|
|
667
|
+
found.set(name, { name, why: isSecretName(name) ? "secret" : "set in SKILL.md", file: file.path, line: index + 1 });
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return [...found.values()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
672
|
+
}
|
|
673
|
+
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(" "));
|
|
674
|
+
var ALIASES = {
|
|
675
|
+
soffice: "LibreOffice",
|
|
676
|
+
libreoffice: "LibreOffice",
|
|
677
|
+
"google-chrome": "Chrome",
|
|
678
|
+
chromium: "Chrome",
|
|
679
|
+
"chromium-browser": "Chrome",
|
|
680
|
+
magick: "ImageMagick",
|
|
681
|
+
convert: "ImageMagick",
|
|
682
|
+
pdftoppm: "poppler",
|
|
683
|
+
pdftotext: "poppler",
|
|
684
|
+
python: "python3",
|
|
685
|
+
docker: "Docker"
|
|
686
|
+
};
|
|
687
|
+
var SPAWNS = [
|
|
688
|
+
/\b(?:execFileSync|execFile|spawnSync|spawn|execaSync|execa)\(\s*["'`]([a-z][\w.+-]*)["'`]/g,
|
|
689
|
+
/\b(?:execSync|exec)\(\s*["'`]([a-z][\w.+-]*)[\s"'`]/g,
|
|
690
|
+
/\bBun\.spawn(?:Sync)?\(\s*\[\s*["'`]([a-z][\w.+-]*)["'`]/g,
|
|
691
|
+
/\bnew\s+Deno\.Command\(\s*["'`]([a-z][\w.+-]*)["'`]/g,
|
|
692
|
+
/\bsubprocess\.(?:run|call|check_call|check_output|Popen)\(\s*\[\s*["']([a-z][\w.+-]*)["']/g,
|
|
693
|
+
/\bsubprocess\.(?:run|call|check_call|check_output|Popen)\(\s*["']([a-z][\w.+-]*)[\s"']/g,
|
|
694
|
+
/\bos\.(?:system|popen)\(\s*["']([a-z][\w.+-]*)[\s"']/g
|
|
695
|
+
];
|
|
696
|
+
var DRIVES = {
|
|
697
|
+
puppeteer: "Chrome",
|
|
698
|
+
"puppeteer-core": "Chrome",
|
|
699
|
+
"chrome-launcher": "Chrome",
|
|
700
|
+
"chrome-remote-interface": "Chrome",
|
|
701
|
+
pyppeteer: "Chrome",
|
|
702
|
+
playwright: "a Playwright browser",
|
|
703
|
+
"playwright-core": "a Playwright browser",
|
|
704
|
+
"@playwright/test": "a Playwright browser",
|
|
705
|
+
"selenium-webdriver": "a browser and its driver",
|
|
706
|
+
selenium: "a browser and its driver",
|
|
707
|
+
"fluent-ffmpeg": "ffmpeg",
|
|
708
|
+
"ffmpeg-python": "ffmpeg",
|
|
709
|
+
ffmpeg: "ffmpeg",
|
|
710
|
+
pydub: "ffmpeg",
|
|
711
|
+
moviepy: "ffmpeg",
|
|
712
|
+
"openai-whisper": "ffmpeg",
|
|
713
|
+
whisper: "ffmpeg",
|
|
714
|
+
pytesseract: "tesseract",
|
|
715
|
+
"node-tesseract-ocr": "tesseract",
|
|
716
|
+
pdf2image: "poppler",
|
|
717
|
+
"pdf-poppler": "poppler",
|
|
718
|
+
wand: "ImageMagick",
|
|
719
|
+
gm: "ImageMagick",
|
|
720
|
+
pypandoc: "pandoc",
|
|
721
|
+
"node-pandoc": "pandoc",
|
|
722
|
+
docker: "Docker",
|
|
723
|
+
dockerode: "Docker"
|
|
724
|
+
};
|
|
725
|
+
var IMPORTS = [
|
|
726
|
+
/\bfrom\s+["']([@\w./-]+)["']/g,
|
|
727
|
+
/\bimport\s*\(\s*["']([@\w./-]+)["']\s*\)/g,
|
|
728
|
+
/\bimport\s+["']([@\w./-]+)["']/g,
|
|
729
|
+
/\brequire\(\s*["']([@\w./-]+)["']\s*\)/g,
|
|
730
|
+
/^\s*(?:from\s+([\w.]+)\s+import|import\s+([\w.]+))/gm
|
|
731
|
+
];
|
|
732
|
+
function packageRoot(specifier) {
|
|
733
|
+
if (specifier.startsWith("@"))
|
|
734
|
+
return specifier.split("/").slice(0, 2).join("/");
|
|
735
|
+
return specifier.split("/")[0]?.split(".")[0] ?? specifier;
|
|
736
|
+
}
|
|
737
|
+
var NAMED_PROGRAMS = [
|
|
738
|
+
{ name: "ffmpeg", pattern: /\bffmpeg\b/i },
|
|
739
|
+
{ name: "ffprobe", pattern: /\bffprobe\b/i },
|
|
740
|
+
{ name: "Chrome", pattern: /\b(?:Google Chrome|Chrome|Chromium|chrome-headless-shell|google-chrome|chromium-browser)\b/ },
|
|
741
|
+
{ name: "Docker", pattern: /\b[Dd]ocker\b/ },
|
|
742
|
+
{ name: "git", pattern: /\bgit\b(?!\s*(?:hub|lab))/ },
|
|
743
|
+
{ name: "jq", pattern: /\bjq\b/ },
|
|
744
|
+
{ name: "pandoc", pattern: /\bpandoc\b/i },
|
|
745
|
+
{ name: "ImageMagick", pattern: /\b(?:ImageMagick|imagemagick|magick)\b/ },
|
|
746
|
+
{ name: "yt-dlp", pattern: /\byt-dlp\b/ },
|
|
747
|
+
{ name: "tesseract", pattern: /\btesseract\b/i },
|
|
748
|
+
{ name: "poppler", pattern: /\b(?:poppler|pdftoppm|pdftotext)\b/i },
|
|
749
|
+
{ name: "LibreOffice", pattern: /\b(?:LibreOffice|libreoffice|soffice)\b/ },
|
|
750
|
+
{ name: "sox", pattern: /\bsox\b/ },
|
|
751
|
+
{ name: "exiftool", pattern: /\bexiftool\b/i },
|
|
752
|
+
{ name: "ollama", pattern: /\b[Oo]llama\b/ },
|
|
753
|
+
{ name: "gh", pattern: /\bgh\s+(?:CLI|cli)\b|\bGitHub CLI\b/ },
|
|
754
|
+
{ name: "python3", pattern: /\b[Pp]ython\s?3?\b(?![-.\w])/ },
|
|
755
|
+
{ name: "node", pattern: /\b(?:Node\.js|node)\b(?![-.\w])/ },
|
|
756
|
+
{ name: "Java", pattern: /\b(?:Java|JDK|JRE)\b/ }
|
|
757
|
+
];
|
|
758
|
+
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;
|
|
759
|
+
var NEEDED_GLOBAL = new RegExp(NEEDED.source, "gi");
|
|
760
|
+
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;
|
|
761
|
+
var NOT_AFTER = /\b(?:requires?|needs?)\s+(?:no|nothing|neither)\b/i;
|
|
762
|
+
function saysWhatItNeeds(path, isScript) {
|
|
763
|
+
if (isScript)
|
|
764
|
+
return true;
|
|
765
|
+
if (!MARKDOWN.test(path))
|
|
766
|
+
return false;
|
|
767
|
+
const base = path.slice(path.lastIndexOf("/") + 1).toLowerCase();
|
|
768
|
+
return base === "skill.md" || /^(readme|requirements?|prerequisites?|setup|install(ation)?|dependencies|getting-started)\b/.test(base);
|
|
769
|
+
}
|
|
770
|
+
var COMMENT_OR_STRING = /^\s*(?:#|\/\/|\*|\/\*|"""|''')|["'`][^"'`]*\b(?:requires?|needs?|install)/i;
|
|
771
|
+
function namedAfterVerb(line, program) {
|
|
772
|
+
for (const verb of line.matchAll(NEEDED_GLOBAL)) {
|
|
773
|
+
const at = verb.index;
|
|
774
|
+
const after = line.slice(at + verb[0].length).slice(0, 40).split(/[.;:!?](?:\s|$)/)[0] ?? "";
|
|
775
|
+
const before = line.slice(Math.max(0, at - 30), at).split(/[.;:!?](?:\s|$)/).at(-1) ?? "";
|
|
776
|
+
if (program.test(after) || program.test(before))
|
|
777
|
+
return true;
|
|
778
|
+
}
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
function programsNeededIn(files) {
|
|
782
|
+
const out = new Map;
|
|
783
|
+
const add = (program) => {
|
|
784
|
+
if (!out.has(program.name))
|
|
785
|
+
out.set(program.name, program);
|
|
786
|
+
};
|
|
787
|
+
for (const file of files) {
|
|
788
|
+
if (isTestFile(file.path))
|
|
789
|
+
continue;
|
|
790
|
+
const lines = file.text.split(`
|
|
791
|
+
`);
|
|
792
|
+
const isScript = SCRIPT.test(file.path) || lines[0]?.startsWith("#!") === true;
|
|
793
|
+
if (isScript && !/\.(sh|bash|zsh)$/i.test(file.path)) {
|
|
794
|
+
for (const pattern of SPAWNS) {
|
|
795
|
+
for (const match of file.text.matchAll(pattern)) {
|
|
796
|
+
const name = match[1] ?? "";
|
|
797
|
+
const line = lineAt(file.text, match.index);
|
|
798
|
+
if (name === "" || EVERYWHERE.has(name) || COMMENT_LINE.test(lines[line - 1] ?? ""))
|
|
799
|
+
continue;
|
|
800
|
+
add({ name: ALIASES[name] ?? name, from: "spawned", file: file.path, line });
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
for (const pattern of IMPORTS) {
|
|
804
|
+
for (const match of file.text.matchAll(pattern)) {
|
|
805
|
+
const root = packageRoot(match[1] ?? match[2] ?? "");
|
|
806
|
+
const driven = DRIVES[root];
|
|
807
|
+
const line = lineAt(file.text, match.index);
|
|
808
|
+
if (driven === undefined || COMMENT_LINE.test(lines[line - 1] ?? ""))
|
|
809
|
+
continue;
|
|
810
|
+
add({ name: driven, from: "package", file: file.path, line, via: root });
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (/(^|\/)package\.json$/.test(file.path)) {
|
|
815
|
+
for (const [index, line] of lines.entries()) {
|
|
816
|
+
const name = /^\s*"(@?[\w./-]+)"\s*:\s*"/.exec(line)?.[1];
|
|
817
|
+
const driven = name === undefined ? undefined : DRIVES[name];
|
|
818
|
+
if (driven !== undefined)
|
|
819
|
+
add({ name: driven, from: "package", file: file.path, line: index + 1, via: name ?? "" });
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if (/(^|\/)requirements[\w.-]*\.txt$/i.test(file.path)) {
|
|
823
|
+
for (const [index, line] of lines.entries()) {
|
|
824
|
+
const name = /^\s*([A-Za-z0-9][\w.-]*)/.exec(line.replace(/#.*/, ""))?.[1]?.toLowerCase();
|
|
825
|
+
const driven = name === undefined ? undefined : DRIVES[name];
|
|
826
|
+
if (driven !== undefined)
|
|
827
|
+
add({ name: driven, from: "package", file: file.path, line: index + 1, via: name ?? "" });
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if (saysWhatItNeeds(file.path, isScript)) {
|
|
831
|
+
for (const [index, line] of lines.entries()) {
|
|
832
|
+
if (NOT_NEEDED.test(line) || NOT_AFTER.test(line))
|
|
833
|
+
continue;
|
|
834
|
+
if (isScript && !COMMENT_OR_STRING.test(line))
|
|
835
|
+
continue;
|
|
836
|
+
for (const { name, pattern } of NAMED_PROGRAMS) {
|
|
837
|
+
if (namedAfterVerb(line, pattern))
|
|
838
|
+
add({ name, from: "named as needed", file: file.path, line: index + 1 });
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return [...out.values()];
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// ../../packages/protocol/src/skill-contents.ts
|
|
847
|
+
var CONTENTS_LIMITS = {
|
|
848
|
+
files: 200,
|
|
849
|
+
fileChars: 512 * 1024,
|
|
850
|
+
items: 40,
|
|
851
|
+
lineChars: 160
|
|
852
|
+
};
|
|
853
|
+
var SCRIPT2 = /\.(sh|bash|zsh|py|js|mjs|cjs|ts|rb|pl|ps1|php|lua)$/i;
|
|
854
|
+
var MARKDOWN2 = /\.(md|mdx|markdown)$/i;
|
|
855
|
+
var FENCE = /^\s*(?:```|~~~)/;
|
|
856
|
+
function cap(text) {
|
|
857
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
858
|
+
return flat.length > CONTENTS_LIMITS.lineChars ? `${flat.slice(0, CONTENTS_LIMITS.lineChars - 1)}…` : flat;
|
|
859
|
+
}
|
|
860
|
+
function compare(a, b) {
|
|
861
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
862
|
+
}
|
|
863
|
+
var BASE_SHELLS = new Set(["sh", "bash", "zsh", "dash"]);
|
|
864
|
+
function interpreterOf(firstLine) {
|
|
865
|
+
const match = /^#!\s*(\S+)(?:\s+(?:-\S+\s+)*(\S+))?/.exec(firstLine);
|
|
866
|
+
if (match === null)
|
|
867
|
+
return null;
|
|
868
|
+
const binary = match[1]?.split("/").pop() ?? "";
|
|
869
|
+
const name = binary === "env" ? match[2] ?? "" : binary;
|
|
870
|
+
return /^[a-z][\w.-]*$/i.test(name) && !BASE_SHELLS.has(name) ? name : null;
|
|
871
|
+
}
|
|
872
|
+
var SCRIPT_PROBE = /(?:^|[;&|(!{]|\bif|\bthen|\$\()\s*(?:!\s*)?(?:command\s+-v|which|type\s+-p|hash)\s+([a-z][\w.+-]*)/g;
|
|
873
|
+
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;
|
|
874
|
+
var COMMENT_LINE2 = /^\s*(?:#|\/\/|\*|--|;|rem\s)/i;
|
|
875
|
+
var PYTHON_PROBE = /\bshutil\.which\(\s*["']([a-z][\w.+-]*)["']/g;
|
|
876
|
+
function programsInScriptLine(line, shell) {
|
|
877
|
+
if (COMMENT_LINE2.test(line))
|
|
878
|
+
return [];
|
|
879
|
+
const matches = shell ? [...line.matchAll(SCRIPT_PROBE), ...line.matchAll(SYSTEM_INSTALL)] : [...line.matchAll(PYTHON_PROBE)];
|
|
880
|
+
return matches.map((match) => match[1] ?? "").filter((name) => name !== "");
|
|
881
|
+
}
|
|
882
|
+
var PACKAGE_INSTALL = /\b(pip3?|pipx|uv\s+pip|uv\s+add|poetry\s+add|npm|pnpm|yarn|bun|gem|cargo)\s+(?:install|add|i)\b([^\n|;&#`)]*)/gi;
|
|
883
|
+
function packagesIn(line) {
|
|
884
|
+
const out = [];
|
|
885
|
+
for (const match of line.matchAll(PACKAGE_INSTALL)) {
|
|
886
|
+
const manager = (match[1] ?? "").replace(/\s+/g, " ").toLowerCase();
|
|
887
|
+
for (const raw of (match[2] ?? "").trim().split(/\s+/)) {
|
|
888
|
+
const word = raw.replace(/^["']|["']$/g, "");
|
|
889
|
+
if (word === "" || word.startsWith("-") || word.startsWith(".") || /[:\\<>{}$]/.test(word))
|
|
890
|
+
continue;
|
|
891
|
+
if (/\.(txt|toml|json|cfg|lock|whl|tgz|tar\.gz|zip)$/i.test(word))
|
|
892
|
+
continue;
|
|
893
|
+
const name = word.replace(/[=<>!~[].*$/, "");
|
|
894
|
+
if (/^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*(@[\w.^~-]+)?$/i.test(name))
|
|
895
|
+
out.push({ manager, name });
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return out;
|
|
899
|
+
}
|
|
900
|
+
function requirementsIn(text) {
|
|
901
|
+
return text.split(`
|
|
902
|
+
`).map((line) => line.replace(/#.*/, "").trim()).filter((line) => line !== "" && !line.startsWith("-") && !/^(https?|git\+)/.test(line)).map((line) => /^[A-Za-z0-9][\w.-]*/.exec(line)?.[0] ?? "").filter((name) => name !== "");
|
|
903
|
+
}
|
|
904
|
+
var SHELL_VARIABLES = new Set(("HOME PATH PWD OLDPWD USER LOGNAME SHELL TERM LANG LC_ALL TMPDIR TMP TEMP HOSTNAME IFS PS1 PS2 " + "RANDOM SECONDS LINENO BASH_SOURCE BASH_VERSION FUNCNAME PIPESTATUS OPTARG OPTIND EUID UID PPID " + "REPLY EDITOR VISUAL PAGER DISPLAY XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME CI NODE_ENV DEBUG").split(" "));
|
|
905
|
+
var READS_IN_CODE = [
|
|
906
|
+
/\bprocess\.env\.([A-Z][A-Z0-9_]+)/g,
|
|
907
|
+
/\bprocess\.env\[\s*["']([A-Z][A-Z0-9_]+)["']\s*\]/g,
|
|
908
|
+
/\bBun\.env\.([A-Z][A-Z0-9_]+)/g,
|
|
909
|
+
/\bDeno\.env\.get\(\s*["']([A-Z][A-Z0-9_]+)["']/g,
|
|
910
|
+
/\bimport\.meta\.env\.([A-Z][A-Z0-9_]+)/g,
|
|
911
|
+
/\bos\.environ\[\s*["']([A-Z][A-Z0-9_]+)["']\s*\]/g,
|
|
912
|
+
/\b(?:os\.environ\.get|os\.getenv|getenv|environ\.get)\(\s*["']([A-Z][A-Z0-9_]+)["']/g,
|
|
913
|
+
/\bENV(?:\.fetch\(|\[)\s*["']([A-Z][A-Z0-9_]+)["']/g,
|
|
914
|
+
/\$env:([A-Za-z][A-Za-z0-9_]+)/g
|
|
915
|
+
];
|
|
916
|
+
var SHELL_READ = /\$\{?([A-Z][A-Z0-9_]{2,})\b/g;
|
|
917
|
+
var SHELL_ASSIGN = /^\s*(?:export\s+|local\s+|readonly\s+|declare\s+(?:-\w+\s+)?)?([A-Z][A-Z0-9_]*)=/;
|
|
918
|
+
var SHELL_LOOP = /\b(?:for|read(?:\s+-\w+)*)\s+([A-Z][A-Z0-9_]*)\b/g;
|
|
919
|
+
var EXPORT_IN_PROSE = /\bexport\s+([A-Z][A-Z0-9_]{2,})=/g;
|
|
920
|
+
var KEY_NAMED = /\b([A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*_(?:API_KEY|APIKEY|KEY|TOKEN|SECRET|PASSWORD))\b/g;
|
|
921
|
+
function shellReads(lines) {
|
|
922
|
+
const assigned = new Set;
|
|
923
|
+
for (const line of lines) {
|
|
924
|
+
const name = SHELL_ASSIGN.exec(line)?.[1];
|
|
925
|
+
if (name !== undefined)
|
|
926
|
+
assigned.add(name);
|
|
927
|
+
for (const loop of line.matchAll(SHELL_LOOP))
|
|
928
|
+
assigned.add(loop[1] ?? "");
|
|
929
|
+
}
|
|
930
|
+
return lines.flatMap((line) => [...line.matchAll(SHELL_READ)].map((match) => match[1] ?? "")).filter((name) => !assigned.has(name));
|
|
931
|
+
}
|
|
932
|
+
function isShell(path, text) {
|
|
933
|
+
return /\.(sh|bash|zsh)$/i.test(path) || /^#!\s*\S*\/(?:env\s+)?(?:ba|z|da)?sh\b/.test(text);
|
|
934
|
+
}
|
|
935
|
+
function settingsIn(path, text, code) {
|
|
936
|
+
const found = new Set;
|
|
937
|
+
for (const pattern of READS_IN_CODE) {
|
|
938
|
+
for (const match of text.matchAll(pattern))
|
|
939
|
+
found.add((match[1] ?? "").toUpperCase());
|
|
940
|
+
}
|
|
941
|
+
if (MARKDOWN2.test(path)) {
|
|
942
|
+
for (const match of text.matchAll(EXPORT_IN_PROSE))
|
|
943
|
+
found.add(match[1] ?? "");
|
|
944
|
+
for (const match of text.matchAll(KEY_NAMED))
|
|
945
|
+
found.add(match[1] ?? "");
|
|
946
|
+
} else if (isShell(path, text)) {
|
|
947
|
+
for (const name of shellReads(code))
|
|
948
|
+
found.add(name);
|
|
949
|
+
}
|
|
950
|
+
return [...found].filter((name) => name.length > 0 && !SHELL_VARIABLES.has(name));
|
|
951
|
+
}
|
|
952
|
+
function codeOf(path, text, isScript) {
|
|
953
|
+
const lines = text.split(`
|
|
954
|
+
`);
|
|
955
|
+
if (isScript)
|
|
956
|
+
return lines;
|
|
957
|
+
if (!MARKDOWN2.test(path))
|
|
958
|
+
return [];
|
|
959
|
+
const out = [];
|
|
960
|
+
let fenced = false;
|
|
961
|
+
for (const line of lines) {
|
|
962
|
+
if (FENCE.test(line)) {
|
|
963
|
+
fenced = !fenced;
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
if (fenced)
|
|
967
|
+
out.push(line);
|
|
968
|
+
else
|
|
969
|
+
for (const inline of line.matchAll(/`([^`]+)`/g))
|
|
970
|
+
out.push(inline[1] ?? "");
|
|
971
|
+
}
|
|
972
|
+
return out;
|
|
973
|
+
}
|
|
974
|
+
var FETCH_AND_RUN = [
|
|
975
|
+
{
|
|
976
|
+
kind: "pipe to a shell",
|
|
977
|
+
pattern: /\b(?:curl|wget|iwr|irm|Invoke-WebRequest|Invoke-RestMethod)\b[^|\n]*\|\s*(?:sudo\s+(?:-\S+\s+)*)?(?:sh|bash|zsh|dash|python3?|node|perl|ruby|iex|Invoke-Expression)\b/i
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
kind: "runs a downloaded script",
|
|
981
|
+
pattern: /\b(?:sh|bash|zsh|source)\s+<\(\s*(?:curl|wget)\b|\b(?:sh|bash|zsh)\s+-c\s+["']?\$\(\s*(?:curl|wget)\b|\b(?:iex|Invoke-Expression)\b[^\n]*\b(?:irm|iwr|Invoke-WebRequest|Invoke-RestMethod|DownloadString)\b|\bdeno\s+run\b[^\n]*\bhttps?:\/\//i
|
|
982
|
+
},
|
|
983
|
+
{
|
|
984
|
+
kind: "installs from a URL",
|
|
985
|
+
pattern: /\b(?:pip3?|pipx|uv\s+pip|npm|pnpm|yarn|bun|gem)\s+(?:install|add|i)\b[^\n|;&]*\s(?:https?:\/\/|git\+|github:)/i
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
kind: "runs a package fetched at run time",
|
|
989
|
+
pattern: /\bnpx\s+(?:-y|--yes)\b|\b(?:pnpm|yarn)\s+dlx\s+\S|\buvx\s+[a-z]|\bpipx\s+run\s+[a-z]/i
|
|
990
|
+
}
|
|
991
|
+
];
|
|
992
|
+
var URL_HOST = /\bhttps?:\/\/([a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+)/gi;
|
|
993
|
+
var PLACEHOLDER_HOST = /(^|\.)(example\.(com|org|net)|localhost|local|test|invalid|example|internal)$|^(127\.\d+\.\d+\.\d+|0\.0\.0\.0)$|^(www\.)?(your|my)-?(domain|site|server|host|app|api|company|website)\./;
|
|
994
|
+
var FILE_LIKE_HOST = /\.(jpe?g|png|gif|webp|svg|mp[34]|wav|mov|webm|pdf|md|json|txt|html?|js|ts|py|sh)$/;
|
|
995
|
+
function domainsIn(text) {
|
|
996
|
+
const out = [];
|
|
997
|
+
for (const match of text.matchAll(URL_HOST)) {
|
|
998
|
+
const host = (match[1] ?? "").toLowerCase();
|
|
999
|
+
if (!/\.[a-z]{2,}$/.test(host) && !/^\d+\.\d+\.\d+\.\d+$/.test(host))
|
|
1000
|
+
continue;
|
|
1001
|
+
if (PLACEHOLDER_HOST.test(host) || FILE_LIKE_HOST.test(host))
|
|
1002
|
+
continue;
|
|
1003
|
+
out.push(host);
|
|
1004
|
+
}
|
|
1005
|
+
return out;
|
|
1006
|
+
}
|
|
1007
|
+
var INVISIBLE = /[----]|\uDB40[\uDC00-\uDDEF]/g;
|
|
1008
|
+
var PICTOGRAPH = /\p{Extended_Pictographic}/u;
|
|
1009
|
+
function codePoint(point) {
|
|
1010
|
+
return `U+${point.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
1011
|
+
}
|
|
1012
|
+
function invisibleIn(line, lineIndex) {
|
|
1013
|
+
const counts = new Map;
|
|
1014
|
+
const spelled = [];
|
|
1015
|
+
for (const match of line.matchAll(INVISIBLE)) {
|
|
1016
|
+
const char = match[0];
|
|
1017
|
+
const at = match.index;
|
|
1018
|
+
if (lineIndex === 0 && at === 0 && char === "\uFEFF")
|
|
1019
|
+
continue;
|
|
1020
|
+
if (char === "") {
|
|
1021
|
+
const before = [...line.slice(0, at)].at(-1) ?? "";
|
|
1022
|
+
const after = [...line.slice(at + 1)][0] ?? "";
|
|
1023
|
+
if (PICTOGRAPH.test(before) && PICTOGRAPH.test(after))
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
const point = char.codePointAt(0) ?? 0;
|
|
1027
|
+
if (point >= 917536 && point <= 917630)
|
|
1028
|
+
spelled.push(String.fromCharCode(point - 917504));
|
|
1029
|
+
const name = point >= 917504 && point <= 917631 ? "Unicode tag" : codePoint(point);
|
|
1030
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
1031
|
+
}
|
|
1032
|
+
if (counts.size === 0)
|
|
1033
|
+
return null;
|
|
1034
|
+
const listed = [...counts.entries()].map(([name, n]) => `${n} × ${name}`).join(", ");
|
|
1035
|
+
return spelled.length === 0 ? listed : `${listed}, spelling "${cap(spelled.join(""))}"`;
|
|
1036
|
+
}
|
|
1037
|
+
var BASE64_RUN = /[A-Za-z0-9+/]{100,}={0,2}/g;
|
|
1038
|
+
var DATA_URI = /data:([\w.+-]+\/[\w.+-]+)?(?:;[\w=-]+)*;base64,([A-Za-z0-9+/]{100,}={0,2})/g;
|
|
1039
|
+
var URL_TEXT = /\bhttps?:\/\/\S+/g;
|
|
1040
|
+
function encodedIn(line) {
|
|
1041
|
+
const out = [];
|
|
1042
|
+
const withoutUris = line.replace(DATA_URI, (_whole, type, body) => {
|
|
1043
|
+
out.push(`data URI${type === undefined ? "" : ` ${type}`}, ${body.length} characters of base64`);
|
|
1044
|
+
return " ";
|
|
1045
|
+
});
|
|
1046
|
+
for (const match of withoutUris.replace(URL_TEXT, " ").matchAll(BASE64_RUN)) {
|
|
1047
|
+
const run = match[0];
|
|
1048
|
+
if (/[A-Z]/.test(run) && /[a-z]/.test(run) && /[0-9]/.test(run))
|
|
1049
|
+
out.push(`${run.length} characters of base64`);
|
|
1050
|
+
}
|
|
1051
|
+
return out;
|
|
1052
|
+
}
|
|
1053
|
+
var IGNORE_INSTRUCTIONS = /\b(?:ignore|disregard|forget|override)\b[^.\n]{0,40}?\b(?:previous|prior|above|earlier|preceding|all|any|other|system|your)\b[^.\n]{0,25}?\b(?:instructions?|prompts?|rules|guidelines|directions)\b/i;
|
|
1054
|
+
var KEEP_FROM_PERSON = /\b(?:do not|don't|never)\s+(?:tell|inform|notify|alert|(?:mention|reveal)(?:\s+(?:this|it))?\s+to)\s+(?:the\s+)?(?:user|human|person|operator)\b|\bwithout\s+(?:telling|informing|notifying|alerting)\s+(?:the\s+)?(?:user|human|person|operator)\b/i;
|
|
1055
|
+
function unquoted(line) {
|
|
1056
|
+
return line.replace(/"[^"]*"|\u201C[^\u201D]*\u201D|\u300C[^\u300D]*\u300D|`[^`]*`/g, " ");
|
|
1057
|
+
}
|
|
1058
|
+
var COMMENT_WORDS = 4;
|
|
1059
|
+
function hiddenInMarkdown(path, text) {
|
|
1060
|
+
const out = [];
|
|
1061
|
+
const push = (kind, line, value) => out.push({ kind, file: path, line, text: value });
|
|
1062
|
+
let fenced = false;
|
|
1063
|
+
let comment = null;
|
|
1064
|
+
for (const [index, line] of text.split(`
|
|
1065
|
+
`).entries()) {
|
|
1066
|
+
const invisible = invisibleIn(line, index);
|
|
1067
|
+
if (invisible !== null)
|
|
1068
|
+
push("hidden characters", index + 1, invisible);
|
|
1069
|
+
for (const encoded of encodedIn(line))
|
|
1070
|
+
push("encoded block", index + 1, encoded);
|
|
1071
|
+
const visible = line.replace(INVISIBLE, "");
|
|
1072
|
+
if (IGNORE_INSTRUCTIONS.test(unquoted(visible)))
|
|
1073
|
+
push("says to ignore instructions", index + 1, cap(visible));
|
|
1074
|
+
if (KEEP_FROM_PERSON.test(unquoted(visible)))
|
|
1075
|
+
push("says not to tell the person", index + 1, cap(visible));
|
|
1076
|
+
if (comment === null && FENCE.test(line)) {
|
|
1077
|
+
fenced = !fenced;
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
if (fenced)
|
|
1081
|
+
continue;
|
|
1082
|
+
let rest = visible;
|
|
1083
|
+
while (rest.length > 0) {
|
|
1084
|
+
if (comment === null) {
|
|
1085
|
+
const open = rest.indexOf("<!--");
|
|
1086
|
+
if (open === -1)
|
|
1087
|
+
break;
|
|
1088
|
+
comment = { start: index + 1, text: [] };
|
|
1089
|
+
rest = rest.slice(open + 4);
|
|
1090
|
+
}
|
|
1091
|
+
const close = rest.indexOf("-->");
|
|
1092
|
+
if (close === -1) {
|
|
1093
|
+
comment.text.push(rest);
|
|
1094
|
+
break;
|
|
1095
|
+
}
|
|
1096
|
+
comment.text.push(rest.slice(0, close));
|
|
1097
|
+
const inner = comment.text.join(" ").replace(/\s+/g, " ").trim();
|
|
1098
|
+
if (inner.split(" ").filter((word) => /[\p{L}\p{N}]/u.test(word)).length >= COMMENT_WORDS) {
|
|
1099
|
+
push("HTML comment", comment.start, cap(inner));
|
|
1100
|
+
}
|
|
1101
|
+
comment = null;
|
|
1102
|
+
rest = rest.slice(close + 3);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
if (comment !== null)
|
|
1106
|
+
push("HTML comment", comment.start, cap(`(never closed) ${comment.text.join(" ")}`));
|
|
1107
|
+
return out;
|
|
1108
|
+
}
|
|
1109
|
+
function bounded(name, items, cut) {
|
|
1110
|
+
if (items.length <= CONTENTS_LIMITS.items)
|
|
1111
|
+
return [...items];
|
|
1112
|
+
cut.push(name);
|
|
1113
|
+
return items.slice(0, CONTENTS_LIMITS.items);
|
|
1114
|
+
}
|
|
1115
|
+
function escapeRegExp(text) {
|
|
1116
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1117
|
+
}
|
|
1118
|
+
function namesFile(markdown, path) {
|
|
1119
|
+
const base = path.slice(path.lastIndexOf("/") + 1);
|
|
1120
|
+
return markdown.includes(path) || new RegExp(`(^|[\\s/\`'"(])${escapeRegExp(base)}($|[\\s\`'")]|[.,:;](\\s|$))`, "m").test(markdown);
|
|
1121
|
+
}
|
|
1122
|
+
function readSkillContents(input) {
|
|
1123
|
+
const sorted = [...input].sort((a, b) => compare(a.path, b.path));
|
|
1124
|
+
const skipped = [];
|
|
1125
|
+
const files = [];
|
|
1126
|
+
for (const file of sorted) {
|
|
1127
|
+
if (files.length >= CONTENTS_LIMITS.files)
|
|
1128
|
+
skipped.push({ path: file.path, why: "over the file limit" });
|
|
1129
|
+
else if (file.text === null)
|
|
1130
|
+
skipped.push({ path: file.path, why: file.tooLarge === true ? "too large" : file.notFetched === true ? "not fetched" : "binary" });
|
|
1131
|
+
else if (file.text.length > CONTENTS_LIMITS.fileChars)
|
|
1132
|
+
skipped.push({ path: file.path, why: "too large" });
|
|
1133
|
+
else
|
|
1134
|
+
files.push({ path: file.path, text: file.text.replace(/\r\n?/g, `
|
|
1135
|
+
`) });
|
|
1136
|
+
}
|
|
1137
|
+
const skillMd = files.find((file) => file.path === "SKILL.md")?.text ?? "";
|
|
1138
|
+
const programs = new Map;
|
|
1139
|
+
const add = (name, from) => {
|
|
1140
|
+
if (!programs.has(name))
|
|
1141
|
+
programs.set(name, { name, from });
|
|
1142
|
+
};
|
|
1143
|
+
const requires = readRequires(skillMd);
|
|
1144
|
+
for (const name of requires.cli)
|
|
1145
|
+
add(name, "declared");
|
|
1146
|
+
for (const name of requires.runs)
|
|
1147
|
+
add(name, "runs");
|
|
1148
|
+
const packages = new Map;
|
|
1149
|
+
const settings = new Set;
|
|
1150
|
+
const domains = new Set;
|
|
1151
|
+
const fetchAndRun = [];
|
|
1152
|
+
const hidden = [];
|
|
1153
|
+
const scripts = skipped.filter((file) => file.why === "not fetched" && SCRIPT2.test(file.path)).map((file) => ({ path: file.path, namedInSkillMd: namesFile(skillMd, file.path) }));
|
|
1154
|
+
for (const file of files) {
|
|
1155
|
+
const lines = file.text.split(`
|
|
1156
|
+
`);
|
|
1157
|
+
const isScript = SCRIPT2.test(file.path) || lines[0]?.startsWith("#!") === true;
|
|
1158
|
+
const code = codeOf(file.path, file.text, isScript);
|
|
1159
|
+
const shell = isShell(file.path, file.text);
|
|
1160
|
+
for (const line of code) {
|
|
1161
|
+
const installed = packagesIn(line);
|
|
1162
|
+
for (const pkg of installed)
|
|
1163
|
+
packages.set(`${pkg.manager} ${pkg.name}`, pkg);
|
|
1164
|
+
if (!isScript)
|
|
1165
|
+
continue;
|
|
1166
|
+
for (const name of programsInScriptLine(line, shell)) {
|
|
1167
|
+
if (!installed.some((pkg) => pkg.name === name))
|
|
1168
|
+
add(name, "script");
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
if (isScript) {
|
|
1172
|
+
scripts.push({ path: file.path, namedInSkillMd: namesFile(skillMd, file.path) });
|
|
1173
|
+
const interpreter = interpreterOf(lines[0] ?? "");
|
|
1174
|
+
if (interpreter !== null)
|
|
1175
|
+
add(interpreter, "interpreter");
|
|
1176
|
+
}
|
|
1177
|
+
if (/(^|\/)requirements[\w.-]*\.txt$/i.test(file.path)) {
|
|
1178
|
+
for (const name of requirementsIn(file.text))
|
|
1179
|
+
packages.set(`pip ${name}`, { manager: "pip", name });
|
|
1180
|
+
}
|
|
1181
|
+
for (const setting of settingsIn(file.path, file.text, code))
|
|
1182
|
+
settings.add(setting);
|
|
1183
|
+
for (const host of domainsIn(file.text))
|
|
1184
|
+
domains.add(host);
|
|
1185
|
+
for (const [index, line] of lines.entries()) {
|
|
1186
|
+
const run = FETCH_AND_RUN.find(({ pattern }) => pattern.test(line));
|
|
1187
|
+
if (run !== undefined)
|
|
1188
|
+
fetchAndRun.push({ kind: run.kind, file: file.path, line: index + 1, text: cap(line) });
|
|
1189
|
+
}
|
|
1190
|
+
if (MARKDOWN2.test(file.path))
|
|
1191
|
+
hidden.push(...hiddenInMarkdown(file.path, file.text));
|
|
1192
|
+
}
|
|
1193
|
+
const packageNames = new Set([...packages.values()].map((pkg) => pkg.name));
|
|
1194
|
+
for (const [name, program] of programs)
|
|
1195
|
+
if (program.from !== "declared" && packageNames.has(name))
|
|
1196
|
+
programs.delete(name);
|
|
1197
|
+
for (const needed of programsNeededIn(files))
|
|
1198
|
+
if (!programs.has(needed.name))
|
|
1199
|
+
programs.set(needed.name, needed);
|
|
1200
|
+
const cut = [];
|
|
1201
|
+
return {
|
|
1202
|
+
programs: bounded("programs", [...programs.values()].sort((a, b) => compare(a.name, b.name)), cut),
|
|
1203
|
+
packages: bounded("packages", [...packages.values()].sort((a, b) => compare(`${a.manager} ${a.name}`, `${b.manager} ${b.name}`)), cut),
|
|
1204
|
+
settings: bounded("settings", [...settings].sort(compare), cut),
|
|
1205
|
+
keys: bounded("keys", keysIn(files, [...settings]), cut),
|
|
1206
|
+
scripts: bounded("scripts", scripts.sort((a, b) => compare(a.path, b.path)), cut),
|
|
1207
|
+
fetchAndRun: bounded("fetchAndRun", fetchAndRun, cut),
|
|
1208
|
+
domains: bounded("domains", [...domains].sort(compare), cut),
|
|
1209
|
+
hidden: bounded("hidden", hidden, cut),
|
|
1210
|
+
read: {
|
|
1211
|
+
files: files.length,
|
|
1212
|
+
bytes: files.reduce((sum, file) => sum + new TextEncoder().encode(file.text).byteLength, 0),
|
|
1213
|
+
skipped
|
|
1214
|
+
},
|
|
1215
|
+
cut
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// ../../packages/protocol/src/pulse.ts
|
|
1220
|
+
var MAX_SKILL_FILES_READ = 200;
|
|
1221
|
+
var MAX_SCRIPTS_READ = 5;
|
|
1222
|
+
var MAX_DIRECTORY_FILES_READ = 80;
|
|
1223
|
+
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;
|
|
1224
|
+
var GITHUB_READ_TIMEOUT_MS = 1e4;
|
|
1225
|
+
var INSTALLER_ORDER = [
|
|
1226
|
+
"",
|
|
1227
|
+
"skills/",
|
|
1228
|
+
"skills/.curated/",
|
|
1229
|
+
"skills/.experimental/",
|
|
1230
|
+
"skills/.system/",
|
|
1231
|
+
".agents/skills/",
|
|
1232
|
+
".claude/skills/",
|
|
1233
|
+
".cline/skills/",
|
|
1234
|
+
".codebuddy/skills/",
|
|
1235
|
+
".codex/skills/",
|
|
1236
|
+
".commandcode/skills/",
|
|
1237
|
+
".continue/skills/",
|
|
1238
|
+
".factory/skills/",
|
|
1239
|
+
".github/skills/",
|
|
1240
|
+
".goose/skills/",
|
|
1241
|
+
".grok/skills/",
|
|
1242
|
+
".iflow/skills/",
|
|
1243
|
+
".junie/skills/",
|
|
1244
|
+
".kilo/skills/",
|
|
1245
|
+
".kilocode/skills/",
|
|
1246
|
+
".kimchi/skills/",
|
|
1247
|
+
".kiro/skills/",
|
|
1248
|
+
".minimax/skills/",
|
|
1249
|
+
".mux/skills/",
|
|
1250
|
+
".neovate/skills/",
|
|
1251
|
+
".opencode/skills/",
|
|
1252
|
+
".openhands/skills/",
|
|
1253
|
+
".pi/skills/",
|
|
1254
|
+
".posit/assistant/skills/",
|
|
1255
|
+
".qoder/skills/",
|
|
1256
|
+
".roo/skills/",
|
|
1257
|
+
".trae/skills/",
|
|
1258
|
+
".windsurf/skills/",
|
|
1259
|
+
".zcode/skills/",
|
|
1260
|
+
".zencoder/skills/"
|
|
1261
|
+
];
|
|
1262
|
+
function installerRank(path) {
|
|
1263
|
+
let best = null;
|
|
1264
|
+
INSTALLER_ORDER.forEach((prefix, rank) => {
|
|
1265
|
+
if (best !== null || !path.startsWith(prefix))
|
|
1266
|
+
return;
|
|
1267
|
+
const depth = path.slice(prefix.length).split("/").length;
|
|
1268
|
+
if (prefix === "" ? depth <= 2 : depth <= 4)
|
|
1269
|
+
best = rank;
|
|
1270
|
+
});
|
|
1271
|
+
return best;
|
|
1272
|
+
}
|
|
1273
|
+
function normaliseName(name) {
|
|
1274
|
+
return name.trim().toLowerCase().replace(/[\s_]+/g, "-");
|
|
1275
|
+
}
|
|
1276
|
+
function frontmatterName(markdown) {
|
|
1277
|
+
const lines = markdown.split(`
|
|
1278
|
+
`);
|
|
1279
|
+
if (lines[0]?.trim() !== "---")
|
|
1280
|
+
return null;
|
|
1281
|
+
for (const line of lines.slice(1)) {
|
|
1282
|
+
if (line.trim() === "---")
|
|
1283
|
+
return null;
|
|
1284
|
+
const match = /^name\s*:\s*(.+?)\s*$/.exec(line);
|
|
1285
|
+
if (match !== null)
|
|
1286
|
+
return (match[1] ?? "").replace(/^["']|["']$/g, "");
|
|
1287
|
+
}
|
|
1288
|
+
return null;
|
|
1289
|
+
}
|
|
1290
|
+
var KEY_NAME2 = /\b[A-Z][A-Z0-9_]*(?:API_KEY|APIKEY|_TOKEN|_SECRET)\b/g;
|
|
1291
|
+
var PAID_WORD = /\b(pricing|paid|subscription|credits?|billing|free tier|per minute)\b/gi;
|
|
1292
|
+
var HOST = /https?:\/\/([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g;
|
|
1293
|
+
var NOT_A_SERVICE = /(^|\.)(github\.com|githubusercontent\.com|example\.(com|org|net))$/;
|
|
1294
|
+
var LOOKS_LIKE_A_FILE = /\.(jpe?g|png|gif|webp|mp[34]|wav|mov|webm|pdf|md|json|txt)$/;
|
|
1295
|
+
var SCRIPT3 = /\.(sh|bash|py|js|mjs|cjs|ts|rb|pl)$/i;
|
|
1296
|
+
var RUNS_FILE = /\b(?:node|bun|deno|tsx|python3?|bash|sh)\s+((?:~|\.{1,2})?\/?[\w.@-]+(?:\/[\w.@-]+)*\.(?:mjs|cjs|js|ts|py|sh))\b/g;
|
|
1297
|
+
var PLACEHOLDER = /(^|\/)(path|your|my|foo|example|script|file|input|output)(\/|\.|$)/i;
|
|
1298
|
+
function filesItRuns(markdown, repositoryFiles) {
|
|
1299
|
+
const named = [...markdown.matchAll(RUNS_FILE)].map((match) => match[1] ?? "");
|
|
1300
|
+
const found = new Set;
|
|
1301
|
+
for (const name of named) {
|
|
1302
|
+
if (name.startsWith("/") || name.startsWith("~") || PLACEHOLDER.test(name))
|
|
1303
|
+
continue;
|
|
1304
|
+
const tail = name.replace(/^(\.{1,2}\/)+/, "");
|
|
1305
|
+
const hit = repositoryFiles.find((file) => file.path === tail || file.path.endsWith(`/${tail}`));
|
|
1306
|
+
if (hit !== undefined)
|
|
1307
|
+
found.add(hit.path);
|
|
1308
|
+
}
|
|
1309
|
+
return [...found];
|
|
1310
|
+
}
|
|
1311
|
+
function absentFiles(markdown, repositoryFiles) {
|
|
1312
|
+
const named = [...markdown.matchAll(RUNS_FILE)].map((match) => match[1] ?? "");
|
|
1313
|
+
return [...new Set(named)].filter((name) => {
|
|
1314
|
+
if (name.startsWith("/") || name.startsWith("~") || PLACEHOLDER.test(name))
|
|
1315
|
+
return false;
|
|
1316
|
+
const tail = name.replace(/^(\.{1,2}\/)+/, "");
|
|
1317
|
+
return !repositoryFiles.some((file) => file.path === tail || file.path.endsWith(`/${tail}`));
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
function readSkillFacts(markdown, filesInDirectory, repositoryFiles = filesInDirectory) {
|
|
1321
|
+
const hosts = [...markdown.matchAll(HOST)].map((match) => (match[1] ?? "").toLowerCase()).filter((host) => !NOT_A_SERVICE.test(host) && !LOOKS_LIKE_A_FILE.test(host));
|
|
1322
|
+
return {
|
|
1323
|
+
files: filesInDirectory.length,
|
|
1324
|
+
bytes: filesInDirectory.reduce((total, file) => total + file.size, 0),
|
|
1325
|
+
scripts: filesInDirectory.filter((file) => SCRIPT3.test(file.path)).length,
|
|
1326
|
+
keys: [...new Set(markdown.match(KEY_NAME2) ?? [])],
|
|
1327
|
+
paidWords: [...new Set((markdown.match(PAID_WORD) ?? []).map((word) => word.toLowerCase()))],
|
|
1328
|
+
hosts: [...new Set(hosts)],
|
|
1329
|
+
runsAbsentFiles: absentFiles(markdown, repositoryFiles),
|
|
1330
|
+
keysInScriptsItRuns: [],
|
|
1331
|
+
requires: readRequires(markdown),
|
|
1332
|
+
contains: readSkillContents([{ path: "SKILL.md", text: markdown }])
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
function parentOf(path) {
|
|
1336
|
+
const at = path.lastIndexOf("/");
|
|
1337
|
+
return at === -1 ? "" : path.slice(0, at);
|
|
1338
|
+
}
|
|
1339
|
+
var MAX_NAME_READS = 12;
|
|
1340
|
+
var NAME_BATCH = 6;
|
|
1341
|
+
var wordsOf = (text) => text.toLowerCase().split(/[^a-z0-9]+/).filter((word) => word.length > 1);
|
|
1342
|
+
function byNearness(files, wanted) {
|
|
1343
|
+
const words = new Set(wordsOf(wanted));
|
|
1344
|
+
const shared = (file) => wordsOf(parentOf(file.path)).filter((word) => words.has(word)).length;
|
|
1345
|
+
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));
|
|
1346
|
+
}
|
|
1347
|
+
async function locate(source, repo, skill, listing) {
|
|
1348
|
+
const wanted = normaliseName(skill);
|
|
1349
|
+
const candidates = listing.files.filter((file) => skillFilesIn([file]).length === 1);
|
|
1350
|
+
const guessed = candidates.filter((file) => normaliseName(directoryOf(file.path)) === wanted);
|
|
1351
|
+
const others = candidates.filter((file) => !guessed.includes(file));
|
|
1352
|
+
const read = async (files) => (await Promise.all(files.map(async (file) => ({ file, markdown: await source.file(repo, file.path) })))).filter((entry) => entry.markdown !== null && normaliseName(frontmatterName(entry.markdown) ?? "") === wanted);
|
|
1353
|
+
let named = await read(guessed);
|
|
1354
|
+
if (named.length === 0) {
|
|
1355
|
+
if (others.length > MAX_SKILL_FILES_READ) {
|
|
1356
|
+
return { status: "incomplete", reason: "too_many_skill_files" };
|
|
1357
|
+
}
|
|
1358
|
+
const nearest = byNearness(others, wanted).slice(0, MAX_NAME_READS);
|
|
1359
|
+
const sharing = nearest.filter((entry) => entry.shared > 0).length;
|
|
1360
|
+
let readTo = 0;
|
|
1361
|
+
while (readTo < nearest.length && named.length === 0) {
|
|
1362
|
+
const size = readTo < sharing ? sharing - readTo : NAME_BATCH;
|
|
1363
|
+
named = await read(nearest.slice(readTo, readTo + size).map((entry) => entry.file));
|
|
1364
|
+
readTo += size;
|
|
1365
|
+
}
|
|
1366
|
+
if (named.length > 0) {
|
|
1367
|
+
named = [...named, ...await read(nearest.slice(readTo, sharing).map((entry) => entry.file))];
|
|
1368
|
+
} else if (nearest.length < others.length) {
|
|
1369
|
+
return { status: "incomplete", reason: "too_many_skill_files" };
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
if (named.length === 0) {
|
|
1373
|
+
if (listing.truncated)
|
|
1374
|
+
return { status: "incomplete", reason: "truncated_listing" };
|
|
1375
|
+
return { status: "absent", skillFiles: candidates.length };
|
|
1376
|
+
}
|
|
1377
|
+
const ranked = named.map((entry) => ({ ...entry, rank: installerRank(entry.file.path) })).sort((a, b) => (a.rank ?? Infinity) - (b.rank ?? Infinity));
|
|
1378
|
+
const [first, ...rest] = ranked;
|
|
1379
|
+
if (first === undefined)
|
|
1380
|
+
return { status: "absent", skillFiles: candidates.length };
|
|
1381
|
+
const differs = rest.filter((entry) => entry.file.sha !== first.file.sha);
|
|
1382
|
+
if (first.rank === null && differs.length > 0) {
|
|
1383
|
+
return { status: "ambiguous", paths: ranked.map((entry) => entry.file.path) };
|
|
1384
|
+
}
|
|
1385
|
+
return {
|
|
1386
|
+
status: "found",
|
|
1387
|
+
file: first.file,
|
|
1388
|
+
markdown: first.markdown,
|
|
1389
|
+
alsoAt: rest.filter((entry) => entry.file.sha === first.file.sha).map((entry) => entry.file.path),
|
|
1390
|
+
differsAt: differs.map((entry) => entry.file.path)
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
function usualPaths(skill) {
|
|
1394
|
+
return [`skills/${skill}/SKILL.md`, `${skill}/SKILL.md`, `.claude/skills/${skill}/SKILL.md`, "SKILL.md"];
|
|
1395
|
+
}
|
|
1396
|
+
async function readFileOnly(result, source) {
|
|
1397
|
+
if (!/^[\w.-]+$/.test(result.skill) || /^\.+$/.test(result.skill))
|
|
1398
|
+
return null;
|
|
1399
|
+
const wanted = normaliseName(result.skill);
|
|
1400
|
+
for (const path of usualPaths(result.skill)) {
|
|
1401
|
+
const markdown = await source.file(result.source, path);
|
|
1402
|
+
if (markdown === null || normaliseName(frontmatterName(markdown) ?? "") !== wanted)
|
|
1403
|
+
continue;
|
|
1404
|
+
const { keys, paidWords, hosts, requires, contains } = readSkillFacts(markdown, []);
|
|
1405
|
+
return { ...result, status: "read_file_only", path, keys, paidWords, hosts, requires, contains };
|
|
1406
|
+
}
|
|
1407
|
+
return null;
|
|
1408
|
+
}
|
|
1409
|
+
var SCRIPT_READ_CAP = { files: MAX_SCRIPTS_READ, bytesEach: 256 * 1024, bytes: 512 * 1024 };
|
|
1410
|
+
var NOT_CODE_DIRECTORY = /(^|\/)(examples?|assets?|fonts?|images?|media|samples?|fixtures?)\//i;
|
|
1411
|
+
function withinReadCap(paths, listed) {
|
|
1412
|
+
const sizeOf = new Map(listed.map((file) => [file.path, file.size]));
|
|
1413
|
+
const kept = [];
|
|
1414
|
+
let bytes = 0;
|
|
1415
|
+
for (const path of paths) {
|
|
1416
|
+
const size = sizeOf.get(path) ?? 0;
|
|
1417
|
+
if (kept.length >= SCRIPT_READ_CAP.files || NOT_CODE_DIRECTORY.test(path))
|
|
1418
|
+
continue;
|
|
1419
|
+
if (size > SCRIPT_READ_CAP.bytesEach || bytes + size > SCRIPT_READ_CAP.bytes)
|
|
1420
|
+
continue;
|
|
1421
|
+
kept.push(path);
|
|
1422
|
+
bytes += size;
|
|
1423
|
+
}
|
|
1424
|
+
return kept;
|
|
1425
|
+
}
|
|
1426
|
+
async function readPulse(result, source, depth) {
|
|
1427
|
+
if (!/^(?!\.{1,2}\/)[\w.-]+\/(?!\.{1,2}$)[\w.-]+$/.test(result.source))
|
|
1428
|
+
return { ...result, status: "not_github" };
|
|
1429
|
+
const listing = await source.listing(result.source);
|
|
1430
|
+
if (listing === "rate_limited")
|
|
1431
|
+
return await readFileOnly(result, source) ?? { ...result, status: "rate_limited" };
|
|
1432
|
+
if (listing === "unreachable")
|
|
1433
|
+
return { ...result, status: "repository_unreachable" };
|
|
1434
|
+
const located = await locate(source, result.source, result.skill, listing);
|
|
1435
|
+
if (located.status === "absent")
|
|
1436
|
+
return { ...result, status: "absent", skillFiles: located.skillFiles };
|
|
1437
|
+
if (located.status === "ambiguous")
|
|
1438
|
+
return { ...result, status: "ambiguous", paths: located.paths };
|
|
1439
|
+
if (located.status === "incomplete")
|
|
1440
|
+
return { ...result, status: "incomplete", reason: located.reason };
|
|
1441
|
+
const directory = parentOf(located.file.path);
|
|
1442
|
+
const prefix = directory === "" ? "" : `${directory}/`;
|
|
1443
|
+
const inDirectory = listing.files.filter((file) => file.path.startsWith(prefix));
|
|
1444
|
+
const scripts = withinReadCap(filesItRuns(located.markdown, listing.files), listing.files);
|
|
1445
|
+
const scriptTexts = await Promise.all(scripts.map(async (file) => ({ file, text: await source.file(result.source, file) })));
|
|
1446
|
+
const keysInScriptsItRuns = scriptTexts.map(({ file, text }) => ({ file, keys: [...new Set(text?.match(KEY_NAME2) ?? [])] })).filter((entry) => entry.keys.length > 0);
|
|
1447
|
+
const relative = (path) => path.startsWith(prefix) ? path.slice(prefix.length) : path;
|
|
1448
|
+
const fetched = new Set(scriptTexts.filter((entry) => entry.text !== null).map((entry) => entry.file));
|
|
1449
|
+
const others = depth === undefined ? [] : await readDirectory(source, result.source, inDirectory, new Set([located.file.path, ...fetched]), depth.directory.by);
|
|
1450
|
+
for (const file of others)
|
|
1451
|
+
if (file.text !== null)
|
|
1452
|
+
fetched.add(file.path);
|
|
1453
|
+
const contains = readSkillContents([
|
|
1454
|
+
{ path: "SKILL.md", text: located.markdown },
|
|
1455
|
+
...scriptTexts.flatMap(({ file, text }) => text === null ? [] : [{ path: relative(file), text }]),
|
|
1456
|
+
...others.flatMap(({ path, text }) => text === null ? [] : [{ path: relative(path), text }]),
|
|
1457
|
+
...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_LIMITS.fileChars ? { path: relative(file.path), text: null, notFetched: true, tooLarge: true } : { path: relative(file.path), text: null, notFetched: true })
|
|
1458
|
+
]);
|
|
1459
|
+
return {
|
|
1460
|
+
...result,
|
|
1461
|
+
status: "read",
|
|
1462
|
+
path: located.file.path,
|
|
1463
|
+
alsoAt: located.alsoAt,
|
|
1464
|
+
differsAt: located.differsAt,
|
|
1465
|
+
contentId: directory === "" ? listing.rootTreeId : listing.trees.get(directory) ?? located.file.sha,
|
|
1466
|
+
skillMdSha: located.file.sha,
|
|
1467
|
+
repository: await source.repository(result.source),
|
|
1468
|
+
...listing.readAt === undefined ? {} : { listingReadAt: listing.readAt },
|
|
1469
|
+
...readSkillFacts(located.markdown, inDirectory, listing.files),
|
|
1470
|
+
keysInScriptsItRuns,
|
|
1471
|
+
contains
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
async function readDirectory(source, repo, inDirectory, done, by) {
|
|
1475
|
+
let late = false;
|
|
1476
|
+
by.then(() => {
|
|
1477
|
+
late = true;
|
|
1478
|
+
});
|
|
1479
|
+
const plan = inDirectory.filter((file) => !done.has(file.path) && !NOT_TEXT.test(file.path) && file.size <= CONTENTS_LIMITS.fileChars).slice(0, MAX_DIRECTORY_FILES_READ);
|
|
1480
|
+
const out = [];
|
|
1481
|
+
for (let at = 0;at < plan.length && !late; at += 8) {
|
|
1482
|
+
const batch = Promise.all(plan.slice(at, at + 8).map(async (file) => ({ path: file.path, text: await source.file(repo, file.path).catch(() => null) })));
|
|
1483
|
+
const settled = await Promise.race([batch, by.then(() => null)]);
|
|
1484
|
+
if (settled === null)
|
|
1485
|
+
break;
|
|
1486
|
+
out.push(...settled);
|
|
1487
|
+
}
|
|
1488
|
+
return out;
|
|
1489
|
+
}
|
|
1490
|
+
function githubPulseSource(options = {}) {
|
|
1491
|
+
const get = options.fetch ?? fetch;
|
|
1492
|
+
const timeoutMs = options.timeoutMs ?? GITHUB_READ_TIMEOUT_MS;
|
|
1493
|
+
const bounded2 = () => {
|
|
1494
|
+
const limit = AbortSignal.timeout(timeoutMs);
|
|
1495
|
+
return options.signal === undefined ? limit : AbortSignal.any([limit, options.signal]);
|
|
1496
|
+
};
|
|
1497
|
+
const headers = {
|
|
1498
|
+
accept: "application/vnd.github+json",
|
|
1499
|
+
"user-agent": "arp-pulse",
|
|
1500
|
+
...options.token === undefined ? {} : { authorization: `Bearer ${options.token}` }
|
|
1501
|
+
};
|
|
1502
|
+
const listings = new Map;
|
|
1503
|
+
const repositories = new Map;
|
|
1504
|
+
const fileReads = new Map;
|
|
1505
|
+
const quota = { apiRequests: 0, limited: false, resetsAt: null };
|
|
1506
|
+
const noteLimit = (response) => {
|
|
1507
|
+
const hit = response.status === 429 || response.status === 403 && (response.headers.get("x-ratelimit-remaining") === "0" || response.headers.has("retry-after"));
|
|
1508
|
+
if (!hit)
|
|
1509
|
+
return false;
|
|
1510
|
+
quota.limited = true;
|
|
1511
|
+
const reset = Number(response.headers.get("x-ratelimit-reset"));
|
|
1512
|
+
if (Number.isFinite(reset) && reset > 0)
|
|
1513
|
+
quota.resetsAt = new Date(reset * 1000).toISOString();
|
|
1514
|
+
return true;
|
|
1515
|
+
};
|
|
1516
|
+
const readListing = async (repo) => {
|
|
1517
|
+
const cached = await options.cache?.get(repo).catch(() => null);
|
|
1518
|
+
if (cached !== null && cached !== undefined)
|
|
1519
|
+
return cached;
|
|
1520
|
+
if (quota.limited)
|
|
1521
|
+
return "rate_limited";
|
|
1522
|
+
quota.apiRequests += 1;
|
|
1523
|
+
const response = await get(`https://api.github.com/repos/${repo}/git/trees/HEAD?recursive=1`, { headers, signal: bounded2() });
|
|
1524
|
+
if (noteLimit(response))
|
|
1525
|
+
return "rate_limited";
|
|
1526
|
+
if (!response.ok)
|
|
1527
|
+
return "unreachable";
|
|
1528
|
+
const body = await response.json();
|
|
1529
|
+
const files = [];
|
|
1530
|
+
const trees = new Map;
|
|
1531
|
+
for (const entry of body.tree ?? []) {
|
|
1532
|
+
if (typeof entry.path !== "string" || typeof entry.sha !== "string")
|
|
1533
|
+
continue;
|
|
1534
|
+
if (entry.type === "tree")
|
|
1535
|
+
trees.set(entry.path, entry.sha);
|
|
1536
|
+
if (entry.type === "blob") {
|
|
1537
|
+
files.push({ path: entry.path, sha: entry.sha, size: typeof entry.size === "number" ? entry.size : 0 });
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
const listing = {
|
|
1541
|
+
rootTreeId: typeof body.sha === "string" ? body.sha : "",
|
|
1542
|
+
files,
|
|
1543
|
+
trees,
|
|
1544
|
+
truncated: body.truncated === true
|
|
1545
|
+
};
|
|
1546
|
+
await options.cache?.set(repo, listing).catch(() => {
|
|
1547
|
+
return;
|
|
1548
|
+
});
|
|
1549
|
+
return listing;
|
|
1550
|
+
};
|
|
1551
|
+
const readRepository = async (repo) => {
|
|
1552
|
+
if (options.withRepository === false || quota.limited)
|
|
1553
|
+
return null;
|
|
1554
|
+
quota.apiRequests += 1;
|
|
1555
|
+
const response = await get(`https://api.github.com/repos/${repo}`, { headers, signal: bounded2() });
|
|
1556
|
+
noteLimit(response);
|
|
1557
|
+
if (!response.ok)
|
|
1558
|
+
return null;
|
|
1559
|
+
const body = await response.json();
|
|
1560
|
+
return {
|
|
1561
|
+
stars: typeof body.stargazers_count === "number" ? body.stargazers_count : 0,
|
|
1562
|
+
pushedAt: typeof body.pushed_at === "string" ? body.pushed_at : null,
|
|
1563
|
+
archived: body.archived === true
|
|
1564
|
+
};
|
|
1565
|
+
};
|
|
1566
|
+
const once = (cache, repo, read) => {
|
|
1567
|
+
const hit = cache.get(repo);
|
|
1568
|
+
if (hit !== undefined)
|
|
1569
|
+
return hit;
|
|
1570
|
+
const started = read(repo);
|
|
1571
|
+
cache.set(repo, started);
|
|
1572
|
+
return started;
|
|
1573
|
+
};
|
|
1574
|
+
return {
|
|
1575
|
+
listing: (repo) => once(listings, repo, readListing),
|
|
1576
|
+
repository: (repo) => once(repositories, repo, readRepository),
|
|
1577
|
+
file: (repo, path) => {
|
|
1578
|
+
const key = `${repo}
|
|
1579
|
+
${path}`;
|
|
1580
|
+
const kept = options.files?.get(key);
|
|
1581
|
+
if (kept !== undefined)
|
|
1582
|
+
return Promise.resolve(kept);
|
|
1583
|
+
return once(fileReads, key, async () => {
|
|
1584
|
+
const encoded = path.split("/").map(encodeURIComponent).join("/");
|
|
1585
|
+
const response = await get(`https://raw.githubusercontent.com/${repo}/HEAD/${encoded}`, { signal: bounded2() });
|
|
1586
|
+
const text = response.ok ? await response.text() : null;
|
|
1587
|
+
if (response.ok || response.status === 404)
|
|
1588
|
+
options.files?.set(key, text);
|
|
1589
|
+
return text;
|
|
1590
|
+
});
|
|
1591
|
+
},
|
|
1592
|
+
quota: () => ({ ...quota })
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// ../../packages/protocol/src/before-install.ts
|
|
1597
|
+
var PAID_SERVICES = [
|
|
1598
|
+
{ name: "Anthropic", hosts: ["api.anthropic.com"], keyPrefixes: ["ANTHROPIC_"] },
|
|
1599
|
+
{ name: "AssemblyAI", hosts: ["api.assemblyai.com"], keyPrefixes: ["ASSEMBLYAI_"] },
|
|
1600
|
+
{ name: "Azure OpenAI", hosts: ["openai.azure.com"], keyPrefixes: ["AZURE_OPENAI_"] },
|
|
1601
|
+
{ name: "Brave Search", hosts: ["api.search.brave.com"], keyPrefixes: ["BRAVE_"] },
|
|
1602
|
+
{ name: "Browserbase", hosts: ["api.browserbase.com"], keyPrefixes: ["BROWSERBASE_"] },
|
|
1603
|
+
{ name: "Cohere", hosts: ["api.cohere.ai", "api.cohere.com"], keyPrefixes: ["COHERE_", "CO_API_"] },
|
|
1604
|
+
{ name: "Deepgram", hosts: ["api.deepgram.com"], keyPrefixes: ["DEEPGRAM_"] },
|
|
1605
|
+
{ name: "DeepSeek", hosts: ["api.deepseek.com"], keyPrefixes: ["DEEPSEEK_"] },
|
|
1606
|
+
{ name: "ElevenLabs", hosts: ["api.elevenlabs.io"], keyPrefixes: ["ELEVENLABS_", "ELEVEN_"] },
|
|
1607
|
+
{ name: "Exa", hosts: ["api.exa.ai"], keyPrefixes: ["EXA_"] },
|
|
1608
|
+
{ name: "fal", hosts: ["fal.run", "queue.fal.run"], keyPrefixes: ["FAL_"] },
|
|
1609
|
+
{ name: "Firecrawl", hosts: ["api.firecrawl.dev"], keyPrefixes: ["FIRECRAWL_"] },
|
|
1610
|
+
{ name: "Fireworks AI", hosts: ["api.fireworks.ai"], keyPrefixes: ["FIREWORKS_"] },
|
|
1611
|
+
{ name: "Google Gemini", hosts: ["generativelanguage.googleapis.com"], keyPrefixes: ["GEMINI_"] },
|
|
1612
|
+
{ name: "Google Vertex AI", hosts: ["aiplatform.googleapis.com"], keyPrefixes: ["VERTEX_"] },
|
|
1613
|
+
{ name: "Groq", hosts: ["api.groq.com"], keyPrefixes: ["GROQ_"] },
|
|
1614
|
+
{ name: "HeyGen", hosts: ["api.heygen.com"], keyPrefixes: ["HEYGEN_"] },
|
|
1615
|
+
{ name: "Mistral", hosts: ["api.mistral.ai"], keyPrefixes: ["MISTRAL_"] },
|
|
1616
|
+
{ name: "OpenAI", hosts: ["api.openai.com"], keyPrefixes: ["OPENAI_"] },
|
|
1617
|
+
{ name: "OpenRouter", hosts: ["openrouter.ai"], keyPrefixes: ["OPENROUTER_"] },
|
|
1618
|
+
{ name: "Perplexity", hosts: ["api.perplexity.ai"], keyPrefixes: ["PERPLEXITY_", "PPLX_"] },
|
|
1619
|
+
{ name: "Replicate", hosts: ["api.replicate.com"], keyPrefixes: ["REPLICATE_"] },
|
|
1620
|
+
{ name: "Resend", hosts: ["api.resend.com"], keyPrefixes: ["RESEND_"] },
|
|
1621
|
+
{ name: "Runway", hosts: ["api.dev.runwayml.com", "api.runwayml.com"], keyPrefixes: ["RUNWAY_", "RUNWAYML_"] },
|
|
1622
|
+
{ name: "SerpApi", hosts: ["serpapi.com"], keyPrefixes: ["SERPAPI_", "SERP_API_"] },
|
|
1623
|
+
{ name: "Stability AI", hosts: ["api.stability.ai"], keyPrefixes: ["STABILITY_"] },
|
|
1624
|
+
{ name: "Tavily", hosts: ["api.tavily.com"], keyPrefixes: ["TAVILY_"] },
|
|
1625
|
+
{ name: "Together AI", hosts: ["api.together.xyz", "api.together.ai"], keyPrefixes: ["TOGETHER_"] },
|
|
1626
|
+
{ name: "Twilio", hosts: ["api.twilio.com"], keyPrefixes: ["TWILIO_"] },
|
|
1627
|
+
{ name: "xAI", hosts: ["api.x.ai"], keyPrefixes: ["XAI_"] }
|
|
1628
|
+
];
|
|
1629
|
+
var STATIC_HOSTS = [
|
|
1630
|
+
"fonts.googleapis.com",
|
|
1631
|
+
"gstatic.com",
|
|
1632
|
+
"cdn.jsdelivr.net",
|
|
1633
|
+
"unpkg.com",
|
|
1634
|
+
"cdnjs.cloudflare.com",
|
|
1635
|
+
"esm.sh",
|
|
1636
|
+
"cdn.skypack.dev",
|
|
1637
|
+
"ga.jspm.io",
|
|
1638
|
+
"githubusercontent.com",
|
|
1639
|
+
"cdn.tailwindcss.com",
|
|
1640
|
+
"code.jquery.com",
|
|
1641
|
+
"use.typekit.net",
|
|
1642
|
+
"fonts.bunny.net",
|
|
1643
|
+
"rsms.me",
|
|
1644
|
+
"cartocdn.com",
|
|
1645
|
+
"tile.openstreetmap.org",
|
|
1646
|
+
"server.arcgisonline.com",
|
|
1647
|
+
"images.unsplash.com"
|
|
1648
|
+
];
|
|
1649
|
+
var STATIC_SHAPE = /^(?:cdn|static|fonts?|tiles?|assets|img|images|media)\d*[.-]|(?:^|[.-])cdn(?:[.-]|$)|\.cdn\./;
|
|
1650
|
+
var API_SHAPE = /^api\d*[.-]|[.-]api\.|^.*\.googleapis\.com$/;
|
|
1651
|
+
function under(host, base) {
|
|
1652
|
+
return host === base || host.endsWith(`.${base}`);
|
|
1653
|
+
}
|
|
1654
|
+
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;
|
|
1655
|
+
function beforeInstall(input) {
|
|
1656
|
+
const services = new Map;
|
|
1657
|
+
const service = (name) => {
|
|
1658
|
+
const found = services.get(name) ?? { name, hosts: [], keys: [] };
|
|
1659
|
+
services.set(name, found);
|
|
1660
|
+
return found;
|
|
1661
|
+
};
|
|
1662
|
+
const apiHosts = [];
|
|
1663
|
+
const staticHosts = [];
|
|
1664
|
+
const otherHosts = [];
|
|
1665
|
+
for (const host of input.domains) {
|
|
1666
|
+
const known = PAID_SERVICES.find((entry) => entry.hosts.some((base) => under(host, base)));
|
|
1667
|
+
if (known !== undefined) {
|
|
1668
|
+
service(known.name).hosts.push(host);
|
|
1669
|
+
apiHosts.push(host);
|
|
1670
|
+
} else if (STATIC_HOSTS.some((base) => under(host, base)) || STATIC_SHAPE.test(host))
|
|
1671
|
+
staticHosts.push(host);
|
|
1672
|
+
else if (API_SHAPE.test(host))
|
|
1673
|
+
apiHosts.push(host);
|
|
1674
|
+
else
|
|
1675
|
+
otherHosts.push(host);
|
|
1676
|
+
}
|
|
1677
|
+
for (const key of input.keys) {
|
|
1678
|
+
if (key.why !== "secret")
|
|
1679
|
+
continue;
|
|
1680
|
+
const known = PAID_SERVICES.find((entry) => entry.keyPrefixes.some((prefix) => key.name.startsWith(prefix)));
|
|
1681
|
+
if (known !== undefined)
|
|
1682
|
+
service(known.name).keys.push(key.name);
|
|
1683
|
+
}
|
|
1684
|
+
const asked = new Set(input.keys.map((key) => key.name));
|
|
1685
|
+
return {
|
|
1686
|
+
fetchAndRun: [...input.fetchAndRun],
|
|
1687
|
+
keys: [...input.keys],
|
|
1688
|
+
otherSettings: input.settings.filter((name) => !asked.has(name)).length,
|
|
1689
|
+
services: [...services.values()].sort((a, b) => a.name < b.name ? -1 : 1),
|
|
1690
|
+
programs: [...input.programs],
|
|
1691
|
+
scripts: [...input.scripts],
|
|
1692
|
+
apiHosts,
|
|
1693
|
+
staticHosts,
|
|
1694
|
+
otherHosts,
|
|
1695
|
+
hidden: [...input.hidden],
|
|
1696
|
+
notRead: input.skipped.filter((file) => file.why !== "binary" && !ASSET.test(file.path)),
|
|
1697
|
+
cut: [...input.cut]
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
var plural2 = (count, one, many = `${one}s`) => `${count} ${count === 1 ? one : many}`;
|
|
1701
|
+
function programNames(programs, shown = 4) {
|
|
1702
|
+
const names = programs.map((program) => program.name);
|
|
1703
|
+
return names.length <= shown ? names.join(", ") : `${names.slice(0, shown).join(", ")} +${names.length - shown}`;
|
|
1704
|
+
}
|
|
1705
|
+
function beforeInstallSummary(view) {
|
|
1706
|
+
const secrets = view.keys.filter((key) => key.why === "secret").length;
|
|
1707
|
+
const toSet = view.keys.length - secrets;
|
|
1708
|
+
const asks = secrets + toSet === 0 ? "no keys" : [secrets === 0 ? null : plural2(secrets, "key"), toSet === 0 ? null : plural2(toSet, "setting")].filter((part) => part !== null).join(", ");
|
|
1709
|
+
return [
|
|
1710
|
+
...view.fetchAndRun.length === 0 ? [] : ["fetches and runs code"],
|
|
1711
|
+
asks,
|
|
1712
|
+
view.services.length === 0 ? "no paid services" : plural2(view.services.length, "paid service"),
|
|
1713
|
+
view.programs.length === 0 ? "nothing to install named" : `needs ${programNames(view.programs)}`,
|
|
1714
|
+
view.scripts.length === 0 ? "no scripts" : plural2(view.scripts.length, "script"),
|
|
1715
|
+
...view.hidden.length === 0 ? [] : [plural2(view.hidden.length, "hidden text", "hidden texts")],
|
|
1716
|
+
...view.notRead.length === 0 ? [] : [`${plural2(view.notRead.length, "file")} not read`]
|
|
1717
|
+
].join(" · ");
|
|
1718
|
+
}
|
|
1719
|
+
function programSourceText(program) {
|
|
1720
|
+
const at = program.file === undefined ? "" : `${program.file}${program.line === undefined ? "" : `:${program.line}`}`;
|
|
1721
|
+
switch (program.from) {
|
|
1722
|
+
case "declared":
|
|
1723
|
+
return "declared in SKILL.md";
|
|
1724
|
+
case "runs":
|
|
1725
|
+
return "run in SKILL.md";
|
|
1726
|
+
case "interpreter":
|
|
1727
|
+
return "script interpreter";
|
|
1728
|
+
case "script":
|
|
1729
|
+
return "a script checks for it or installs it";
|
|
1730
|
+
case "spawned":
|
|
1731
|
+
return `run by ${at}`;
|
|
1732
|
+
case "package":
|
|
1733
|
+
return `via ${program.via ?? "a package"}, ${at}`;
|
|
1734
|
+
case "named as needed":
|
|
1735
|
+
return `said to be needed, ${at}`;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
function contentsLines(contents, perList = 6) {
|
|
1739
|
+
const view = beforeInstall({ ...contents, skipped: contents.read.skipped });
|
|
1740
|
+
const list = (items, cut2) => `${items.slice(0, perList).join(", ")}${items.length > perList || cut2 ? `, and ${Math.max(0, items.length - perList)}${cut2 ? "+" : ""} more` : ""}`;
|
|
1741
|
+
const cut = (name) => view.cut.includes(name);
|
|
1742
|
+
const at = (rows, name) => list(rows.map((row) => `${row.kind} at ${row.file}:${row.line} "${row.text}"`), cut(name));
|
|
1743
|
+
const others = view.otherSettings === 0 ? "" : `; ${plural2(view.otherSettings, "other setting")} it reads`;
|
|
1744
|
+
const hostParts = [
|
|
1745
|
+
view.apiHosts.length === 0 ? "no API host" : list(view.apiHosts, false),
|
|
1746
|
+
...view.staticHosts.length === 0 ? [] : [`fonts, CDNs and map tiles: ${view.staticHosts.length}`],
|
|
1747
|
+
...view.otherHosts.length === 0 ? [] : [`other sites named: ${view.otherHosts.length}`]
|
|
1748
|
+
];
|
|
1749
|
+
return [
|
|
1750
|
+
`Before you install: ${beforeInstallSummary(view)}`,
|
|
1751
|
+
...view.fetchAndRun.length === 0 ? [] : [`Fetches and runs code: ${at(view.fetchAndRun, "fetchAndRun")}`],
|
|
1752
|
+
`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}`}`,
|
|
1753
|
+
`Paid services: ${view.services.length === 0 ? "none named" : list(view.services.map((entry) => `${entry.name} (${[...entry.hosts, ...entry.keys].join(", ")})`), false)}`,
|
|
1754
|
+
`Needs installed: ${view.programs.length === 0 ? "none named" : list(view.programs.map((program) => `${program.name} (${programSourceText(program)})`), cut("programs"))}`,
|
|
1755
|
+
`Runs on your machine: ${view.scripts.length === 0 ? "no scripts" : `${plural2(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" : ""}`,
|
|
1756
|
+
`Sends data to (hosts its files name, not shown to be contacted): ${hostParts.join("; ")}`,
|
|
1757
|
+
...view.hidden.length === 0 ? [] : [`Hidden or encoded text: ${at(view.hidden, "hidden")}`],
|
|
1758
|
+
...view.notRead.length === 0 ? [] : [`Not read: ${list(view.notRead.map((file) => `${file.path} (${file.why})`), false)}`]
|
|
1759
|
+
];
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
// ../../packages/protocol/src/skill-install.ts
|
|
1763
|
+
var MAX_SKILLS_READ = 3;
|
|
1764
|
+
var ADD = new Set(["add", "a", "install", "i"]);
|
|
1765
|
+
var RUNNERS = new Set(["npx", "pnpx", "bunx"]);
|
|
1766
|
+
async function namesUnder(install, source) {
|
|
1767
|
+
const listing = await source.listing(install.repo);
|
|
1768
|
+
if (listing === "rate_limited" || listing === "unreachable")
|
|
1769
|
+
return listing;
|
|
1770
|
+
const prefix = install.subpath === null ? "" : `${install.subpath}/`;
|
|
1771
|
+
const files = skillFilesIn(listing.files).filter((file) => file.path.startsWith(prefix)).sort((a, b) => a.path.split("/").length - b.path.split("/").length || a.path.localeCompare(b.path));
|
|
1772
|
+
const names = await Promise.all(files.slice(0, MAX_SKILLS_READ).map(async (file) => {
|
|
1773
|
+
const markdown = await source.file(install.repo, file.path);
|
|
1774
|
+
const parts = file.path.split("/");
|
|
1775
|
+
return (markdown === null ? null : frontmatterName(markdown)) ?? parts.at(-2) ?? install.repo.split("/")[1] ?? "";
|
|
1776
|
+
}));
|
|
1777
|
+
return { names, total: files.length };
|
|
1778
|
+
}
|
|
1779
|
+
async function readSkillInstall(install, source, deadline, directoryBy = deadline) {
|
|
1780
|
+
if (install.kind === "elsewhere")
|
|
1781
|
+
return install;
|
|
1782
|
+
const late = deadline.then(() => "late");
|
|
1783
|
+
let names;
|
|
1784
|
+
let total;
|
|
1785
|
+
if (install.skills === "all") {
|
|
1786
|
+
const found = await Promise.race([
|
|
1787
|
+
namesUnder(install, source).catch(() => "unreachable"),
|
|
1788
|
+
late
|
|
1789
|
+
]);
|
|
1790
|
+
if (found === "late")
|
|
1791
|
+
return {
|
|
1792
|
+
kind: "github",
|
|
1793
|
+
install,
|
|
1794
|
+
readings: [],
|
|
1795
|
+
notRead: 0,
|
|
1796
|
+
listing: "no answer in time"
|
|
1797
|
+
};
|
|
1798
|
+
if (found === "rate_limited" || found === "unreachable") {
|
|
1799
|
+
return { kind: "github", install, readings: [], notRead: 0, listing: found };
|
|
1800
|
+
}
|
|
1801
|
+
({ names, total } = found);
|
|
1802
|
+
} else {
|
|
1803
|
+
names = install.skills.slice(0, MAX_SKILLS_READ);
|
|
1804
|
+
total = install.skills.length;
|
|
1805
|
+
}
|
|
1806
|
+
const readings = await Promise.all(names.map(async (skill) => {
|
|
1807
|
+
const pulse = readPulse({ 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" }));
|
|
1808
|
+
const settled = await Promise.race([pulse, late]);
|
|
1809
|
+
return settled === "late" ? { skill, pulse: null, why: "no answer in time" } : settled;
|
|
1810
|
+
}));
|
|
1811
|
+
return {
|
|
1812
|
+
kind: "github",
|
|
1813
|
+
install,
|
|
1814
|
+
readings,
|
|
1815
|
+
notRead: Math.max(0, total - names.length),
|
|
1816
|
+
listing: null
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
var plural3 = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
1820
|
+
function readingLines(repo, reading) {
|
|
1821
|
+
const head = `${repo} · ${reading.skill}`;
|
|
1822
|
+
const { pulse } = reading;
|
|
1823
|
+
if (pulse === null)
|
|
1824
|
+
return [
|
|
1825
|
+
`${head}: not read (${reading.why === "no answer in time" ? "GitHub did not answer in time" : "the read failed"})`
|
|
1826
|
+
];
|
|
1827
|
+
switch (pulse.status) {
|
|
1828
|
+
case "read": {
|
|
1829
|
+
const facts = [
|
|
1830
|
+
`${pulse.path}`,
|
|
1831
|
+
plural3(pulse.files, "file"),
|
|
1832
|
+
...pulse.repository === null ? [] : [
|
|
1833
|
+
plural3(pulse.repository.stars, "star"),
|
|
1834
|
+
...pulse.repository.pushedAt === null ? [] : [`last push ${pulse.repository.pushedAt.slice(0, 10)}`],
|
|
1835
|
+
...pulse.repository.archived ? ["archived"] : []
|
|
1836
|
+
]
|
|
1837
|
+
];
|
|
1838
|
+
return [
|
|
1839
|
+
`${head} (${facts.join(", ")})`,
|
|
1840
|
+
...contentsLines(pulse.contains).map((line) => ` ${line}`),
|
|
1841
|
+
...pulse.differsAt.length === 0 ? [] : [` Other files carry this name and differ: ${pulse.differsAt.join(", ")}`]
|
|
1842
|
+
];
|
|
1843
|
+
}
|
|
1844
|
+
case "read_file_only":
|
|
1845
|
+
return [
|
|
1846
|
+
`${head} (${pulse.path}; GitHub's hourly limit was reached, so SKILL.md alone was read, not its scripts)`,
|
|
1847
|
+
...contentsLines(pulse.contains).map((line) => ` ${line}`)
|
|
1848
|
+
];
|
|
1849
|
+
case "absent":
|
|
1850
|
+
return [
|
|
1851
|
+
`${head}: no SKILL.md in the repository names it (${plural3(pulse.skillFiles, "skill file")} read)`
|
|
1852
|
+
];
|
|
1853
|
+
case "ambiguous":
|
|
1854
|
+
return [
|
|
1855
|
+
`${head}: several files carry this name and differ, and which one installs is not knowable here: ${pulse.paths.join(", ")}`
|
|
1856
|
+
];
|
|
1857
|
+
case "incomplete":
|
|
1858
|
+
return [
|
|
1859
|
+
`${head}: not read (${pulse.reason === "truncated_listing" ? "GitHub cut the repository's file list" : "too many skill files to search"})`
|
|
1860
|
+
];
|
|
1861
|
+
case "rate_limited":
|
|
1862
|
+
return [`${head}: not read (GitHub's hourly limit; GITHUB_TOKEN raises it)`];
|
|
1863
|
+
case "repository_unreachable":
|
|
1864
|
+
return [`${head}: not read (the repository did not answer)`];
|
|
1865
|
+
case "not_github":
|
|
1866
|
+
return [`${head}: not read (not a GitHub repository)`];
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
function skillInstallReport(read) {
|
|
1870
|
+
const title = "ARP read this skill before the install (what its own files contain; facts, not a verdict):";
|
|
1871
|
+
if (read.kind === "elsewhere") {
|
|
1872
|
+
return `${title}
|
|
1873
|
+
${read.source}: not read (ARP reads GitHub sources only)`;
|
|
1874
|
+
}
|
|
1875
|
+
const { install } = read;
|
|
1876
|
+
const lines = [title];
|
|
1877
|
+
if (read.listing !== null) {
|
|
1878
|
+
const why = read.listing === "rate_limited" ? "GitHub's hourly limit; GITHUB_TOKEN raises it" : read.listing === "no answer in time" ? "GitHub did not answer in time" : "the repository did not answer";
|
|
1879
|
+
lines.push(`${install.repo}${install.subpath === null ? "" : `/${install.subpath}`}: not read (${why})`);
|
|
1880
|
+
} else if (read.readings.length === 0) {
|
|
1881
|
+
lines.push(`${install.repo}${install.subpath === null ? "" : `/${install.subpath}`}: no SKILL.md found`);
|
|
1882
|
+
}
|
|
1883
|
+
for (const reading of read.readings)
|
|
1884
|
+
lines.push(...readingLines(install.repo, reading));
|
|
1885
|
+
if (read.notRead > 0)
|
|
1886
|
+
lines.push(`${plural3(read.notRead, "more skill")} in this install not read (ARP reads ${MAX_SKILLS_READ})`);
|
|
1887
|
+
if (install.ref !== null)
|
|
1888
|
+
lines.push(`Read at the default branch's HEAD; the command names ${install.ref}.`);
|
|
1889
|
+
return lines.join(`
|
|
1890
|
+
`);
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
// src/check.ts
|
|
1894
|
+
var CHECK_BUDGET_MS = 8000;
|
|
1895
|
+
var WHERE = "Intuition testnet 13579";
|
|
1896
|
+
var IN_CONTEXT_OF = _13579_default.arp.endpointBinding.inContextOf;
|
|
1897
|
+
var PAYERS = [_13579_default.arp.deployer, _13579_default.arp.sponsor];
|
|
1898
|
+
function skillRefOf(arg) {
|
|
1899
|
+
const part = /^[\w.-]+$/;
|
|
1900
|
+
const fromUrl = /^https:\/\/github\.com\/([\w.-]+)\/([\w.-]+)\/?\?skill=([^&#]+)$/.exec(arg.trim());
|
|
1901
|
+
if (fromUrl !== null) {
|
|
1902
|
+
const skill = decodeURIComponent(fromUrl[3] ?? "");
|
|
1903
|
+
return part.test(skill) ? { repo: `${fromUrl[1]}/${fromUrl[2]}`, skill } : null;
|
|
1904
|
+
}
|
|
1905
|
+
const pieces = arg.trim().split("/");
|
|
1906
|
+
if (pieces.length !== 3 || !pieces.every((piece) => part.test(piece)))
|
|
1907
|
+
return null;
|
|
1908
|
+
return { repo: `${pieces[0]}/${pieces[1]}`, skill: pieces[2] ?? "" };
|
|
1909
|
+
}
|
|
1910
|
+
function neededNames(usage) {
|
|
1911
|
+
const byName = new Map;
|
|
1912
|
+
for (const row of usage?.declarations ?? []) {
|
|
1913
|
+
for (const line of row.lines) {
|
|
1914
|
+
if (line.outcome !== "failed")
|
|
1915
|
+
continue;
|
|
1916
|
+
for (const name of line.needs ?? []) {
|
|
1917
|
+
byName.set(name, (byName.get(name) ?? new Set).add(row.writer.toLowerCase()));
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
return new Map([...byName].map(([name, wallets]) => [name, wallets.size]));
|
|
1922
|
+
}
|
|
1923
|
+
function thisMachine(env = process.env, platform = process.platform) {
|
|
1924
|
+
const dirs = (env["PATH"] ?? "").split(delimiter).filter((dir) => dir !== "");
|
|
1925
|
+
const exts = platform === "win32" ? (env["PATHEXT"] ?? ".EXE;.CMD;.BAT").split(";") : [""];
|
|
1926
|
+
return {
|
|
1927
|
+
hasProgram: (name) => dirs.some((dir) => exts.some((ext) => {
|
|
1928
|
+
const path = join(dir, name + ext);
|
|
1929
|
+
try {
|
|
1930
|
+
return existsSync(path) && statSync(path).isFile();
|
|
1931
|
+
} catch {
|
|
1932
|
+
return false;
|
|
1933
|
+
}
|
|
1934
|
+
})),
|
|
1935
|
+
hasVariable: (name) => (env[name] ?? "") !== ""
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
function namedInFiles(read) {
|
|
1939
|
+
if (read.kind !== "github")
|
|
1940
|
+
return [];
|
|
1941
|
+
const names = new Set;
|
|
1942
|
+
for (const reading of read.readings) {
|
|
1943
|
+
const pulse = reading.pulse;
|
|
1944
|
+
if (pulse === null || pulse.status !== "read")
|
|
1945
|
+
continue;
|
|
1946
|
+
for (const program of pulse.contains.programs)
|
|
1947
|
+
names.add(program.name);
|
|
1948
|
+
for (const setting of pulse.contains.settings)
|
|
1949
|
+
names.add(setting);
|
|
1950
|
+
}
|
|
1951
|
+
return [...names];
|
|
1952
|
+
}
|
|
1953
|
+
var stateOf = (name, machine) => {
|
|
1954
|
+
if (KEY_NAME.test(name))
|
|
1955
|
+
return machine.hasVariable(name) ? "set" : "not set";
|
|
1956
|
+
if (PROGRAM_NAME.test(name))
|
|
1957
|
+
return machine.hasProgram(name) ? "found on PATH" : "missing here";
|
|
1958
|
+
return "not checked";
|
|
1959
|
+
};
|
|
1960
|
+
function hereLines(needed, named, machine) {
|
|
1961
|
+
const fromFiles = [...new Set(named)].filter((name) => !needed.has(name)).sort((a, b) => a.localeCompare(b));
|
|
1962
|
+
if (needed.size === 0 && fromFiles.length === 0)
|
|
1963
|
+
return ["Here: nothing named to check."];
|
|
1964
|
+
const lines = ["Here (checked on this machine, nothing sent):"];
|
|
1965
|
+
const wallets = (n) => `${n} wallet${n === 1 ? "" : "s"}`;
|
|
1966
|
+
for (const [name, n] of [...needed].sort(([a, x], [b, y]) => y - x || a.localeCompare(b))) {
|
|
1967
|
+
lines.push(` ${name}: ${stateOf(name, machine)} (needed when it failed, ${wallets(n)})`);
|
|
1968
|
+
}
|
|
1969
|
+
for (const name of fromFiles)
|
|
1970
|
+
lines.push(` ${name}: ${stateOf(name, machine)} (named in its files)`);
|
|
1971
|
+
return lines;
|
|
1972
|
+
}
|
|
1973
|
+
async function checkSkill(ref, deps) {
|
|
1974
|
+
const budgetMs = deps.budgetMs ?? CHECK_BUDGET_MS;
|
|
1975
|
+
const token = (deps.env["GITHUB_TOKEN"] ?? deps.env["GH_TOKEN"])?.trim() || undefined;
|
|
1976
|
+
const deadline = new Promise((resolve) => setTimeout(resolve, budgetMs).unref?.());
|
|
1977
|
+
const source = githubPulseSource({
|
|
1978
|
+
...deps.fetch === undefined ? {} : { fetch: deps.fetch },
|
|
1979
|
+
...token === undefined ? {} : { token },
|
|
1980
|
+
timeoutMs: budgetMs + 500,
|
|
1981
|
+
signal: AbortSignal.timeout(budgetMs + 500)
|
|
1982
|
+
});
|
|
1983
|
+
const contents = readSkillInstall({ kind: "github", repo: ref.repo, ref: null, subpath: null, skills: [ref.skill] }, source, deadline);
|
|
1984
|
+
const url = `https://github.com/${ref.repo}?skill=${encodeURIComponent(ref.skill)}`;
|
|
1985
|
+
const { uri, label } = skillAtom(url, (text) => createHash("sha256").update(text, "utf8").digest());
|
|
1986
|
+
const usage = readSkillLines(uri, {
|
|
1987
|
+
graphqlUrl: deps.env["INTUITION_GRAPHQL_URL"] ?? _13579_default.chain.graphqlUrl,
|
|
1988
|
+
inContextOf: IN_CONTEXT_OF,
|
|
1989
|
+
payers: PAYERS,
|
|
1990
|
+
timeoutMs: budgetMs,
|
|
1991
|
+
...deps.fetch === undefined ? {} : { fetch: deps.fetch }
|
|
1992
|
+
}).then((read2) => ({ ok: true, read: read2 }), () => ({ ok: false }));
|
|
1993
|
+
const [read, lines] = await Promise.all([contents, usage]);
|
|
1994
|
+
const blocks = [skillInstallReport(read)];
|
|
1995
|
+
blocks.push(lines.ok ? linesSentence(lines.read, label, WHERE) ?? `Lines: no agent wrote an outcome on ${label} (${WHERE}).` : `Lines: not read (${WHERE} did not answer).`);
|
|
1996
|
+
blocks.push(hereLines(lines.ok ? neededNames(lines.read) : new Map, namedInFiles(read), deps.machine).join(`
|
|
1997
|
+
`));
|
|
1998
|
+
return `${blocks.join(`
|
|
1999
|
+
|
|
2000
|
+
`)}
|
|
2001
|
+
`;
|
|
2002
|
+
}
|
|
2003
|
+
export {
|
|
2004
|
+
thisMachine,
|
|
2005
|
+
skillRefOf,
|
|
2006
|
+
neededNames,
|
|
2007
|
+
namedInFiles,
|
|
2008
|
+
hereLines,
|
|
2009
|
+
checkSkill,
|
|
2010
|
+
CHECK_BUDGET_MS
|
|
2011
|
+
};
|