pi-bro 0.22.0 → 0.22.1
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 +32 -0
- package/README.md +116 -987
- package/bro.ts +2 -2
- package/docs/README.md +6 -2
- package/docs/pig-compatibility.md +1 -1
- package/docs/user-guide.md +986 -0
- package/package.json +10 -4
- package/sources.ts +65 -11
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-bro",
|
|
3
|
-
"version": "0.22.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.22.1",
|
|
4
|
+
"description": "A sidekick for you and your Pi agent: plain-language explanations, side questions, diagrams, PR review and second opinions, without cluttering the agent's context.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Tran Hoang Nguyen",
|
|
@@ -33,7 +33,12 @@
|
|
|
33
33
|
"advisor",
|
|
34
34
|
"second-opinion",
|
|
35
35
|
"codex",
|
|
36
|
-
"muse"
|
|
36
|
+
"muse",
|
|
37
|
+
"coding-agent",
|
|
38
|
+
"ai-agents",
|
|
39
|
+
"cli",
|
|
40
|
+
"developer-tools",
|
|
41
|
+
"code-review"
|
|
37
42
|
],
|
|
38
43
|
"files": [
|
|
39
44
|
"bro.ts",
|
|
@@ -52,6 +57,7 @@
|
|
|
52
57
|
"CHANGELOG.md",
|
|
53
58
|
"LICENSE",
|
|
54
59
|
"THIRD_PARTY_NOTICES.md",
|
|
60
|
+
"docs/user-guide.md",
|
|
55
61
|
"docs/pig-compatibility.md",
|
|
56
62
|
"docs/guided-review.md"
|
|
57
63
|
],
|
|
@@ -63,7 +69,7 @@
|
|
|
63
69
|
},
|
|
64
70
|
"scripts": {
|
|
65
71
|
"typecheck": "tsc --noEmit",
|
|
66
|
-
"test": "npm run typecheck && node .github/scripts/release-utils.mjs selftest && node --test review-ux.test.mjs review-doctor.test.mjs review-versions.test.mjs review-regeneration.test.mjs review-modal.test.mjs review.test.ts review-ui.test.mjs review-session.test.mjs helpers.test.mjs prompt.test.ts backend.test.ts claude.test.ts grok.test.ts codex.test.ts muse.test.ts settings.test.ts release.test.ts show-html.test.ts smoke-rpc.test.ts ui-capabilities.test.ts benchmark/*.test.ts && sh ./smoke-test.sh",
|
|
72
|
+
"test": "npm run typecheck && node .github/scripts/release-utils.mjs selftest && node --test test/review-ux.test.mjs test/review-doctor.test.mjs test/review-versions.test.mjs test/review-regeneration.test.mjs test/review-modal.test.mjs test/review.test.ts test/review-ui.test.mjs test/review-session.test.mjs test/helpers.test.mjs test/prompt.test.ts test/backend.test.ts test/claude.test.ts test/grok.test.ts test/codex.test.ts test/muse.test.ts test/settings.test.ts test/sources.test.ts test/release.test.ts test/show-html.test.ts test/smoke-rpc.test.ts test/ui-capabilities.test.ts benchmark/*.test.ts && sh ./test/smoke-test.sh",
|
|
67
73
|
"benchmark:dry-run": "node benchmark/run.ts dry-run",
|
|
68
74
|
"benchmark:run": "node benchmark/run.ts run",
|
|
69
75
|
"benchmark:report": "node benchmark/run.ts report",
|
package/sources.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile, stat, realpath } from "node:fs/promises";
|
|
2
2
|
import { isAbsolute, relative, resolve, extname, sep } from "node:path";
|
|
3
|
-
import { request as httpRequest, type IncomingMessage } from "node:http";
|
|
3
|
+
import { request as httpRequest, type IncomingMessage, type ClientRequest } from "node:http";
|
|
4
4
|
import { request as httpsRequest } from "node:https";
|
|
5
5
|
import { lookup } from "node:dns/promises";
|
|
6
6
|
import { BlockList, isIP } from "node:net";
|
|
@@ -122,6 +122,7 @@ for (const [network, prefix] of [
|
|
|
122
122
|
NON_PUBLIC_ADDRESSES.addSubnet(network, prefix, "ipv4");
|
|
123
123
|
}
|
|
124
124
|
for (const [network, prefix] of [
|
|
125
|
+
["::", 96],
|
|
125
126
|
["::", 128],
|
|
126
127
|
["::1", 128],
|
|
127
128
|
["64:ff9b::", 96],
|
|
@@ -134,6 +135,7 @@ for (const [network, prefix] of [
|
|
|
134
135
|
["5f00::", 16],
|
|
135
136
|
["fc00::", 7],
|
|
136
137
|
["fe80::", 10],
|
|
138
|
+
["fec0::", 10],
|
|
137
139
|
["ff00::", 8],
|
|
138
140
|
] as const) {
|
|
139
141
|
NON_PUBLIC_ADDRESSES.addSubnet(network, prefix, "ipv6");
|
|
@@ -187,18 +189,53 @@ export function parseWebRedirect(current: URL, location: string): URL {
|
|
|
187
189
|
return next;
|
|
188
190
|
}
|
|
189
191
|
|
|
192
|
+
export type WebDnsLookup = (
|
|
193
|
+
hostname: string,
|
|
194
|
+
options: { all: true; verbatim: true },
|
|
195
|
+
) => Promise<Array<{ address: string; family: number }>>;
|
|
196
|
+
|
|
197
|
+
export type WebHttpDispatch = (
|
|
198
|
+
url: URL | string,
|
|
199
|
+
options: any,
|
|
200
|
+
callback?: (res: IncomingMessage) => void,
|
|
201
|
+
) => ClientRequest;
|
|
202
|
+
|
|
203
|
+
export type WebTransport = {
|
|
204
|
+
// Injectable for offline tests only -- production always uses native Node dns/http/https modules.
|
|
205
|
+
lookup?: WebDnsLookup;
|
|
206
|
+
httpRequest?: WebHttpDispatch;
|
|
207
|
+
httpsRequest?: WebHttpDispatch;
|
|
208
|
+
};
|
|
209
|
+
|
|
190
210
|
function headerValue(value: string | string[] | undefined): string {
|
|
191
211
|
return Array.isArray(value) ? value[0] ?? "" : value ?? "";
|
|
192
212
|
}
|
|
193
213
|
|
|
194
|
-
async function resolvePublicAddress(
|
|
214
|
+
async function resolvePublicAddress(
|
|
215
|
+
hostname: string,
|
|
216
|
+
signal?: AbortSignal,
|
|
217
|
+
dnsLookup: WebDnsLookup = lookup as unknown as WebDnsLookup,
|
|
218
|
+
): Promise<{ address: string; family: 4 | 6 }> {
|
|
219
|
+
if (signal?.aborted) throw new Error("Canceled.");
|
|
195
220
|
const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
196
221
|
let addresses: Array<{ address: string; family: number }>;
|
|
197
222
|
try {
|
|
198
|
-
|
|
223
|
+
const lookupPromise = dnsLookup(host, { all: true, verbatim: true });
|
|
224
|
+
if (signal) {
|
|
225
|
+
addresses = await Promise.race([
|
|
226
|
+
lookupPromise,
|
|
227
|
+
new Promise<never>((_, reject) => {
|
|
228
|
+
signal.addEventListener("abort", () => reject(new Error("Canceled.")), { once: true });
|
|
229
|
+
}),
|
|
230
|
+
]);
|
|
231
|
+
} else {
|
|
232
|
+
addresses = await lookupPromise;
|
|
233
|
+
}
|
|
199
234
|
} catch (error) {
|
|
235
|
+
if (signal?.aborted) throw new Error("Canceled.");
|
|
200
236
|
throw new Error(`Could not resolve webpage host: ${errorMessage(error)}`);
|
|
201
237
|
}
|
|
238
|
+
if (signal?.aborted) throw new Error("Canceled.");
|
|
202
239
|
if (!addresses.length) throw new Error("The webpage host has no network address.");
|
|
203
240
|
if (addresses.some((item) => !isPublicWebAddress(item.address))) {
|
|
204
241
|
throw new Error("Bro cannot connect to local, private, or reserved network addresses.");
|
|
@@ -206,19 +243,27 @@ async function resolvePublicAddress(hostname: string): Promise<{ address: string
|
|
|
206
243
|
return { address: addresses[0].address, family: addresses[0].family === 6 ? 6 : 4 };
|
|
207
244
|
}
|
|
208
245
|
|
|
209
|
-
function requestWebPage(
|
|
246
|
+
function requestWebPage(
|
|
247
|
+
url: URL,
|
|
248
|
+
address: { address: string; family: 4 | 6 },
|
|
249
|
+
signal: AbortSignal,
|
|
250
|
+
requestFn?: WebHttpDispatch,
|
|
251
|
+
): Promise<IncomingMessage> {
|
|
252
|
+
const defaultRequest = (url.protocol === "https:" ? httpsRequest : httpRequest) as unknown as WebHttpDispatch;
|
|
253
|
+
const execute = requestFn ?? defaultRequest;
|
|
210
254
|
return new Promise((resolveResponse, rejectResponse) => {
|
|
211
|
-
const request = (
|
|
255
|
+
const request = execute(
|
|
212
256
|
url,
|
|
213
257
|
{
|
|
214
258
|
method: "GET",
|
|
215
259
|
signal,
|
|
260
|
+
agent: false,
|
|
216
261
|
headers: {
|
|
217
262
|
Accept: "text/html,application/xhtml+xml",
|
|
218
263
|
"Accept-Encoding": "identity",
|
|
219
264
|
"User-Agent": "pi-bro URL reader (+https://github.com/tranhoangnguyen03/pi-bro)",
|
|
220
265
|
},
|
|
221
|
-
lookup: (_hostname, options, callback) => {
|
|
266
|
+
lookup: (_hostname: string, options: any, callback: any) => {
|
|
222
267
|
if (options.all) callback(null, [address]);
|
|
223
268
|
else callback(null, address.address, address.family);
|
|
224
269
|
},
|
|
@@ -283,17 +328,22 @@ function assertWebElementLimit(html: string): void {
|
|
|
283
328
|
}
|
|
284
329
|
}
|
|
285
330
|
|
|
286
|
-
async function fetchPublicHtml(
|
|
331
|
+
export async function fetchPublicHtml(
|
|
332
|
+
startUrl: URL,
|
|
333
|
+
signal: AbortSignal,
|
|
334
|
+
transport?: WebTransport,
|
|
335
|
+
): Promise<{ html: string; url: URL }> {
|
|
287
336
|
let url = startUrl;
|
|
288
337
|
const visited = new Set<string>();
|
|
289
338
|
|
|
290
339
|
for (let redirects = 0; ; redirects++) {
|
|
291
340
|
if (visited.has(url.href)) throw new Error("Webpage redirect loop detected.");
|
|
292
341
|
visited.add(url.href);
|
|
293
|
-
const address = await resolvePublicAddress(url.hostname);
|
|
342
|
+
const address = await resolvePublicAddress(url.hostname, signal, transport?.lookup);
|
|
294
343
|
let response: IncomingMessage;
|
|
295
344
|
try {
|
|
296
|
-
|
|
345
|
+
const requestFn = url.protocol === "https:" ? transport?.httpsRequest : transport?.httpRequest;
|
|
346
|
+
response = await requestWebPage(url, address, signal, requestFn);
|
|
297
347
|
} catch (error) {
|
|
298
348
|
throw new Error(`Could not fetch webpage: ${errorMessage(error)}`);
|
|
299
349
|
}
|
|
@@ -353,11 +403,15 @@ export async function extractWebHtml(html: string, url: string): Promise<BroSour
|
|
|
353
403
|
return { text, label: [parsedUrl.hostname, title].filter(Boolean).join(" · ") };
|
|
354
404
|
}
|
|
355
405
|
|
|
356
|
-
export async function extractWebPage(
|
|
406
|
+
export async function extractWebPage(
|
|
407
|
+
input: string,
|
|
408
|
+
signal?: AbortSignal,
|
|
409
|
+
transport?: WebTransport,
|
|
410
|
+
): Promise<BroSource> {
|
|
357
411
|
const timeout = AbortSignal.timeout(WEB_TIMEOUT_MS);
|
|
358
412
|
const combinedSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
359
413
|
try {
|
|
360
|
-
const fetched = await fetchPublicHtml(parseWebUrl(input), combinedSignal);
|
|
414
|
+
const fetched = await fetchPublicHtml(parseWebUrl(input), combinedSignal, transport);
|
|
361
415
|
return await extractWebHtml(fetched.html, fetched.url.href);
|
|
362
416
|
} catch (error) {
|
|
363
417
|
if (signal?.aborted) throw new Error("Canceled.");
|