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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-bro",
3
- "version": "0.22.0",
4
- "description": "An Earendil Pi extension for plain-language explanations, conversation diagrams, side questions, guided PR understanding and assessment, and a second-opinion advisor tool.",
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(hostname: string): Promise<{ address: string; family: 4 | 6 }> {
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
- addresses = await lookup(host, { all: true, verbatim: true });
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(url: URL, address: { address: string; family: 4 | 6 }, signal: AbortSignal): Promise<IncomingMessage> {
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 = (url.protocol === "https:" ? httpsRequest : httpRequest)(
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(startUrl: URL, signal: AbortSignal): Promise<{ html: string; url: URL }> {
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
- response = await requestWebPage(url, address, signal);
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(input: string, signal?: AbortSignal): Promise<BroSource> {
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.");