@herbertgao/pi-extensions 2026.9.12 → 2026.9.14
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/README.md +4 -4
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/node_modules/@herbertgao/pi-bark/package.json +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +4 -4
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +14 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +4 -4
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +5 -4
- package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +39 -16
- package/node_modules/@herbertgao/resume-from/package.json +2 -2
- package/node_modules/@herbertgao/sol-pi/package.json +5 -5
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +2 -0
- package/node_modules/@narumitw/pi-btw/README.md +21 -4
- package/node_modules/@narumitw/pi-btw/dist/index.ts +1668 -958
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
- package/node_modules/@narumitw/pi-btw/docs/workflows.md +10 -2
- package/node_modules/@narumitw/pi-btw/package.json +1 -1
- package/node_modules/@narumitw/pi-btw/src/btw.ts +17 -79
- package/node_modules/@narumitw/pi-btw/src/conversation-context.ts +74 -0
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +196 -7
- package/node_modules/@narumitw/pi-btw/src/main-thread-updates.ts +40 -0
- package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
- package/node_modules/@narumitw/pi-btw/src/settings.ts +49 -0
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +9 -1
- package/node_modules/@narumitw/pi-btw/src/workspace-layout.ts +559 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +26 -0
- package/node_modules/pi-mcp-adapter/README.md +39 -9
- package/node_modules/pi-mcp-adapter/cli.js +33 -22
- package/node_modules/pi-mcp-adapter/commands.ts +3 -3
- package/node_modules/pi-mcp-adapter/config.ts +9 -1
- package/node_modules/pi-mcp-adapter/dist/config.js +9 -1
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/jev-client.d.ts +1 -2
- package/node_modules/pi-mcp-adapter/dist/jev-client.js +59 -32
- package/node_modules/pi-mcp-adapter/dist/jev-client.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/jev-contracts.d.ts +1 -1
- package/node_modules/pi-mcp-adapter/dist/jev-key-store.d.ts +33 -4
- package/node_modules/pi-mcp-adapter/dist/jev-key-store.js +148 -28
- package/node_modules/pi-mcp-adapter/dist/jev-key-store.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js +1 -2
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +7 -1
- package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
- package/node_modules/pi-mcp-adapter/index.ts +51 -38
- package/node_modules/pi-mcp-adapter/jev-client.ts +58 -31
- package/node_modules/pi-mcp-adapter/jev-contracts.ts +2 -2
- package/node_modules/pi-mcp-adapter/jev-key-store.ts +144 -31
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
- package/node_modules/pi-mcp-adapter/mcp-code.ts +6 -6
- package/node_modules/pi-mcp-adapter/package.json +5 -5
- package/node_modules/pi-mcp-adapter/semantic-search.ts +3 -3
- package/node_modules/pi-mcp-adapter/types.ts +7 -1
- package/node_modules/pi-multi-account/CHANGELOG.md +29 -0
- package/node_modules/pi-multi-account/README.md +39 -11
- package/node_modules/pi-multi-account/index.ts +371 -96
- package/node_modules/pi-multi-account/package.json +5 -4
- package/node_modules/pi-multi-account/provider-payload-stream.ts +36 -28
- package/node_modules/pi-multi-account/usage.ts +31 -2
- package/node_modules/pi-typesafe/README.md +3 -1
- package/node_modules/pi-typesafe/dist/auth.d.ts +10 -3
- package/node_modules/pi-typesafe/dist/auth.js +13 -7
- package/node_modules/pi-typesafe/dist/backends.d.ts +41 -0
- package/node_modules/pi-typesafe/dist/backends.js +59 -0
- package/node_modules/pi-typesafe/dist/client.d.ts +4 -10
- package/node_modules/pi-typesafe/dist/client.js +72 -44
- package/node_modules/pi-typesafe/dist/credentials.d.ts +11 -5
- package/node_modules/pi-typesafe/dist/credentials.js +15 -8
- package/node_modules/pi-typesafe/dist/errors.d.ts +10 -2
- package/node_modules/pi-typesafe/dist/errors.js +28 -7
- package/node_modules/pi-typesafe/dist/extension.js +6 -2
- package/node_modules/pi-typesafe/dist/index.d.ts +1 -1
- package/node_modules/pi-typesafe/dist/index.js +1 -1
- package/node_modules/pi-typesafe/dist/login.d.ts +13 -7
- package/node_modules/pi-typesafe/dist/login.js +17 -8
- package/node_modules/pi-typesafe/dist/schema.js +19 -13
- package/node_modules/pi-typesafe/package.json +4 -4
- package/node_modules/pi-web-access/CHANGELOG.md +22 -0
- package/node_modules/pi-web-access/README.md +21 -8
- package/node_modules/pi-web-access/baizhi.ts +124 -0
- package/node_modules/pi-web-access/crawl4ai.ts +2 -10
- package/node_modules/pi-web-access/curator-page.ts +3 -1
- package/node_modules/pi-web-access/curator-server.ts +1 -0
- package/node_modules/pi-web-access/dist/index.js +540 -220
- package/node_modules/pi-web-access/firecrawl.ts +2 -10
- package/node_modules/pi-web-access/gemini-search.ts +5 -10
- package/node_modules/pi-web-access/index.ts +134 -23
- package/node_modules/pi-web-access/package.json +5 -1
- package/node_modules/pi-web-access/storage.ts +1 -0
- package/node_modules/pi-web-access/tool-activation.ts +134 -0
- package/node_modules/pi-web-access/utils.ts +9 -1
- package/package.json +14 -14
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import net from "node:net";
|
|
3
2
|
import { activityMonitor } from "./activity.ts";
|
|
4
3
|
import { normalizeDomain } from "./domain-filter-normalization.ts";
|
|
5
4
|
import { redactCredential, resolveCredential } from "./credential-source.ts";
|
|
@@ -8,7 +7,7 @@ import type { SearchOptions, SearchResponse } from "./perplexity.ts";
|
|
|
8
7
|
import { formatSearchResultsAsAnswer } from "./search-answer-formatting.ts";
|
|
9
8
|
import { normalizeSearchResultCount } from "./search-result-count-normalization.ts";
|
|
10
9
|
import { loadSsrfConfig, validateRemoteUrl, type Lookup } from "./ssrf-protection.ts";
|
|
11
|
-
import { getWebSearchConfigPath } from "./utils.ts";
|
|
10
|
+
import { getWebSearchConfigPath, isLoopbackHostname } from "./utils.ts";
|
|
12
11
|
|
|
13
12
|
const CONFIG_PATH = getWebSearchConfigPath();
|
|
14
13
|
const DEFAULT_API_VERSION = "v2";
|
|
@@ -188,13 +187,6 @@ function ssrfOptions(options?: FirecrawlExtractOptions | FirecrawlSearchOptions)
|
|
|
188
187
|
};
|
|
189
188
|
}
|
|
190
189
|
|
|
191
|
-
function isLoopbackApiUrl(url: URL): boolean {
|
|
192
|
-
const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
|
|
193
|
-
if (hostname === "localhost" || hostname === "::1") return true;
|
|
194
|
-
if (net.isIP(hostname) !== 4) return false;
|
|
195
|
-
return hostname.split(".")[0] === "127";
|
|
196
|
-
}
|
|
197
|
-
|
|
198
190
|
function firecrawlApiSsrfOptions(
|
|
199
191
|
options: FirecrawlExtractOptions | FirecrawlSearchOptions | undefined,
|
|
200
192
|
allowLoopback: boolean,
|
|
@@ -218,7 +210,7 @@ async function fetchFirecrawlApi(
|
|
|
218
210
|
init: { method: string; headers: Record<string, string>; body: string; signal: AbortSignal },
|
|
219
211
|
options: FirecrawlExtractOptions | FirecrawlSearchOptions | undefined,
|
|
220
212
|
): Promise<Response> {
|
|
221
|
-
const allowLoopback =
|
|
213
|
+
const allowLoopback = isLoopbackHostname(new URL(url).hostname);
|
|
222
214
|
let current = await validateRemoteUrl(url, firecrawlApiSsrfOptions(options, allowLoopback));
|
|
223
215
|
let headers = init.headers;
|
|
224
216
|
for (let redirects = 0; redirects <= DEFAULT_MAX_REDIRECTS; redirects++) {
|
|
@@ -38,12 +38,13 @@ import { isSerpBaseAvailable, searchWithSerpBase } from "./serpbase.ts";
|
|
|
38
38
|
import { isSerpApiAvailable, searchWithSerpApi } from "./serpapi.ts";
|
|
39
39
|
import { isSerperAvailable, searchWithSerper } from "./serper.ts";
|
|
40
40
|
import { isSerplyAvailable, searchWithSerply } from "./serply.ts";
|
|
41
|
+
import { isBaizhiAvailable, searchWithBaizhi } from "./baizhi.ts";
|
|
41
42
|
import { isValyuAvailable, searchWithValyu } from "./valyu.ts";
|
|
42
43
|
import { isKimiSearchAvailable, searchWithKimi } from "./kimi-search.ts";
|
|
43
44
|
import { isMistralAvailable, searchWithMistral } from "./mistral-search.ts";
|
|
44
45
|
import { getWebSearchConfigPath } from "./utils.ts";
|
|
45
46
|
|
|
46
|
-
export const RESOLVED_SEARCH_PROVIDERS = ["openai", "brave", "parallel", "parallel-mcp", "tinyfish", "search1api", "searchinfinity", "querit", "tavily", "firecrawl", "jina", "searxng", "duckduckgo", "perplexity", "gemini", "kimi", "exa", "serpdive", "kagi", "ollama", "anysearch", "xai", "mistral", "brightdata", "serpbase", "serpapi", "serper", "serply", "valyu", "bocha", "xcrawl"] as const;
|
|
47
|
+
export const RESOLVED_SEARCH_PROVIDERS = ["openai", "brave", "parallel", "parallel-mcp", "tinyfish", "search1api", "searchinfinity", "querit", "tavily", "firecrawl", "jina", "searxng", "duckduckgo", "perplexity", "gemini", "kimi", "exa", "serpdive", "kagi", "ollama", "anysearch", "xai", "mistral", "brightdata", "serpbase", "serpapi", "serper", "serply", "valyu", "bocha", "xcrawl", "baizhi"] as const;
|
|
47
48
|
export const SEARCH_PROVIDERS = ["auto", "all", ...RESOLVED_SEARCH_PROVIDERS] as const;
|
|
48
49
|
|
|
49
50
|
export type ResolvedSearchProvider = typeof RESOLVED_SEARCH_PROVIDERS[number];
|
|
@@ -270,20 +271,11 @@ function isAbortError(err: unknown): boolean {
|
|
|
270
271
|
return errorMessage(err).toLowerCase().includes("abort");
|
|
271
272
|
}
|
|
272
273
|
|
|
273
|
-
function shouldTryOpenAIInAuto(options: SearchOptions): boolean {
|
|
274
|
-
if (options.recencyFilter) return false;
|
|
275
|
-
if (typeof options.numResults === "number" && Number.isFinite(options.numResults) && Math.floor(options.numResults) !== 5) {
|
|
276
|
-
return false;
|
|
277
|
-
}
|
|
278
|
-
return true;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
274
|
function isOpenAICodexSelected(ctx?: ExtensionContext): boolean {
|
|
282
275
|
return ctx?.model?.provider === "openai-codex";
|
|
283
276
|
}
|
|
284
277
|
|
|
285
278
|
async function tryOpenAIInAuto(query: string, options: FullSearchOptions, fallbackErrors: string[]): Promise<AttributedSearchResponse | null> {
|
|
286
|
-
if (!shouldTryOpenAIInAuto(options)) return null;
|
|
287
279
|
try {
|
|
288
280
|
if (await isOpenAISearchAvailable(options.extensionContext)) {
|
|
289
281
|
const result = await searchWithOpenAI(query, options, options.extensionContext);
|
|
@@ -411,6 +403,7 @@ async function searchWithResolvedProvider(
|
|
|
411
403
|
if (provider === "serpapi") return { ...(await searchWithSerpApi(query, options)), provider };
|
|
412
404
|
if (provider === "serper") return { ...(await searchWithSerper(query, options)), provider };
|
|
413
405
|
if (provider === "serply") return { ...(await searchWithSerply(query, options)), provider };
|
|
406
|
+
if (provider === "baizhi") return { ...(await searchWithBaizhi(query, options)), provider };
|
|
414
407
|
if (provider === "valyu") return { ...(await searchWithValyu(query, options)), provider };
|
|
415
408
|
if (provider === "xcrawl") return { ...(await searchWithXCrawl(query, options)), provider };
|
|
416
409
|
if (provider === "perplexity") return { ...(await searchWithPerplexity(query, options)), provider };
|
|
@@ -461,6 +454,7 @@ async function isResolvedProviderAvailable(provider: ResolvedSearchProvider, opt
|
|
|
461
454
|
if (provider === "serpapi") return isSerpApiAvailable();
|
|
462
455
|
if (provider === "serper") return isSerperAvailable();
|
|
463
456
|
if (provider === "serply") return isSerplyAvailable();
|
|
457
|
+
if (provider === "baizhi") return isBaizhiAvailable();
|
|
464
458
|
if (provider === "valyu") return isValyuAvailable();
|
|
465
459
|
if (provider === "xcrawl") return isXcrawlAvailable();
|
|
466
460
|
if (provider === "perplexity") return isPerplexityAvailable();
|
|
@@ -502,6 +496,7 @@ export function providerLabel(provider: ResolvedSearchProvider): string {
|
|
|
502
496
|
if (provider === "serpapi") return "SerpApi";
|
|
503
497
|
if (provider === "serper") return "Serper";
|
|
504
498
|
if (provider === "serply") return "Serply";
|
|
499
|
+
if (provider === "baizhi") return "Baizhi";
|
|
505
500
|
if (provider === "valyu") return "Valyu";
|
|
506
501
|
return provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
507
502
|
}
|
|
@@ -72,6 +72,7 @@ import { isSerpBaseAvailable } from "./serpbase.ts";
|
|
|
72
72
|
import { isSerpApiAvailable } from "./serpapi.ts";
|
|
73
73
|
import { isSerperAvailable } from "./serper.ts";
|
|
74
74
|
import { isSerplyAvailable } from "./serply.ts";
|
|
75
|
+
import { isBaizhiAvailable } from "./baizhi.ts";
|
|
75
76
|
import { isValyuAvailable } from "./valyu.ts";
|
|
76
77
|
import { isXcrawlAvailable } from "./xcrawl.ts";
|
|
77
78
|
import { buildSearchErrorPlan, type SearchErrorDetails, type SearchErrorPlan } from "./render-search-error.ts";
|
|
@@ -85,6 +86,7 @@ import {
|
|
|
85
86
|
type RecencyFilter,
|
|
86
87
|
type ResearchArtifact,
|
|
87
88
|
} from "./source-check.ts";
|
|
89
|
+
import { registerWebToolActivation, type WebActivationTool } from "./tool-activation.ts";
|
|
88
90
|
|
|
89
91
|
// Match pi-ai's StringEnum without loading its compat barrel during registration.
|
|
90
92
|
function StringEnum<T extends string[]>(values: T, options?: { description?: string; default?: T[number] }) {
|
|
@@ -155,6 +157,7 @@ interface WebSearchConfig {
|
|
|
155
157
|
serpapiApiKey?: unknown;
|
|
156
158
|
serperApiKey?: unknown;
|
|
157
159
|
serplyApiKey?: unknown;
|
|
160
|
+
baizhiApiKey?: unknown;
|
|
158
161
|
tinyfishApiKey?: unknown;
|
|
159
162
|
valyuApiKey?: unknown;
|
|
160
163
|
xaiApiKey?: unknown;
|
|
@@ -336,6 +339,7 @@ function resolveToolNames(config: WebSearchConfig): ToolNames {
|
|
|
336
339
|
const seen = new Map<string, keyof ToolNames>();
|
|
337
340
|
for (const key of registeredKeys) {
|
|
338
341
|
const name = names[key];
|
|
342
|
+
if (name === "web_enable") throw new Error(`toolNames.${key} in ${WEB_SEARCH_CONFIG_PATH} uses reserved loader name web_enable`);
|
|
339
343
|
const previous = seen.get(name);
|
|
340
344
|
if (previous) throw new Error(`toolNames.${key} duplicates toolNames.${previous} in ${WEB_SEARCH_CONFIG_PATH}`);
|
|
341
345
|
seen.set(name, key);
|
|
@@ -488,6 +492,7 @@ async function getProviderAvailability(ctx: ExtensionContext): Promise<ProviderA
|
|
|
488
492
|
serpapi: allowedProviders.has("serpapi") && isSerpApiAvailable(),
|
|
489
493
|
serper: allowedProviders.has("serper") && isSerperAvailable(),
|
|
490
494
|
serply: allowedProviders.has("serply") && isSerplyAvailable(),
|
|
495
|
+
baizhi: allowedProviders.has("baizhi") && isBaizhiAvailable(),
|
|
491
496
|
valyu: allowedProviders.has("valyu") && isValyuAvailable(),
|
|
492
497
|
};
|
|
493
498
|
return {
|
|
@@ -621,11 +626,12 @@ interface PendingCurate {
|
|
|
621
626
|
|
|
622
627
|
|
|
623
628
|
const DEFAULT_MAX_INLINE_CONTENT_CHARS = 30_000;
|
|
629
|
+
const MIN_INLINE_CONTENT_CHARS = 1_000;
|
|
624
630
|
const MAX_INLINE_CONTENT_CHARS = 200_000;
|
|
625
631
|
|
|
626
632
|
function getMaxInlineContentChars(config = loadConfig()): number {
|
|
627
633
|
const value = config.maxInlineContentChars;
|
|
628
|
-
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value
|
|
634
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value < MIN_INLINE_CONTENT_CHARS) {
|
|
629
635
|
return DEFAULT_MAX_INLINE_CONTENT_CHARS;
|
|
630
636
|
}
|
|
631
637
|
return Math.min(value, MAX_INLINE_CONTENT_CHARS);
|
|
@@ -772,15 +778,40 @@ function hasFullInlineCoverage(urls: string[], inlineContent: ExtractedContent[]
|
|
|
772
778
|
|
|
773
779
|
function formatFullResults(queryData: QueryResultData): string {
|
|
774
780
|
let output = `## Results for: "${queryData.query}"\n\n`;
|
|
781
|
+
const providers = queryData.providers ?? (queryData.provider ? [queryData.provider] : []);
|
|
782
|
+
if (providers.length > 0) output += `**Provider${providers.length === 1 ? "" : "s"}:** ${providers.join(", ")}\n\n`;
|
|
775
783
|
if (queryData.answer) {
|
|
776
784
|
output += `${queryData.answer}\n\n---\n\n`;
|
|
777
785
|
}
|
|
778
786
|
for (const r of queryData.results) {
|
|
779
|
-
output += `### ${r.title}\n${r.url}\n\n`;
|
|
787
|
+
output += `### ${r.title}\n${r.url}${r.snippet ? `\n\n${r.snippet}` : ""}\n\n`;
|
|
780
788
|
}
|
|
781
789
|
return output;
|
|
782
790
|
}
|
|
783
791
|
|
|
792
|
+
function boundSearchPresentation(
|
|
793
|
+
text: string,
|
|
794
|
+
guidance: string,
|
|
795
|
+
truncationGuidance: string,
|
|
796
|
+
maxChars: number,
|
|
797
|
+
): { text: string; truncated: boolean; originalChars: number; returnedChars: number; omittedChars: number } {
|
|
798
|
+
const fullText = `${text}${guidance}`;
|
|
799
|
+
if (fullText.length <= maxChars) {
|
|
800
|
+
return { text: fullText, truncated: false, originalChars: text.length, returnedChars: text.length, omittedChars: 0 };
|
|
801
|
+
}
|
|
802
|
+
const marker = `\n\n---\n[Output truncated.]${truncationGuidance}`;
|
|
803
|
+
// Reserve marker space so the complete model-visible response never exceeds the configured ceiling.
|
|
804
|
+
const prefixLength = maxChars - marker.length;
|
|
805
|
+
const bounded = `${text.slice(0, prefixLength)}${marker}`;
|
|
806
|
+
return {
|
|
807
|
+
text: bounded,
|
|
808
|
+
truncated: true,
|
|
809
|
+
originalChars: text.length,
|
|
810
|
+
returnedChars: prefixLength,
|
|
811
|
+
omittedChars: text.length - prefixLength,
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
|
|
784
815
|
function abortPendingFetches(): void {
|
|
785
816
|
for (const controller of pendingFetches.values()) {
|
|
786
817
|
controller.abort();
|
|
@@ -1365,6 +1396,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1365
1396
|
const tr = opts.results.reduce((sum, r) => sum + r.results.length, 0);
|
|
1366
1397
|
|
|
1367
1398
|
const hasApprovedSummary = typeof opts.approvedSummary === "string" && opts.approvedSummary.trim().length > 0;
|
|
1399
|
+
const maxInlineContentChars = getMaxInlineContentChars(initConfig);
|
|
1368
1400
|
let output = "";
|
|
1369
1401
|
if (hasApprovedSummary) {
|
|
1370
1402
|
output = opts.approvedSummary!.trim();
|
|
@@ -1372,6 +1404,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1372
1404
|
if (opts.curated) {
|
|
1373
1405
|
output += "[These results were manually curated by the user in the browser. Use them as-is — do not re-search or discard.]\n\n";
|
|
1374
1406
|
}
|
|
1407
|
+
const providerNames = opts.results.map(result => {
|
|
1408
|
+
const providers = result.providers ?? (result.provider ? [result.provider] : []);
|
|
1409
|
+
return providers.join(", ") || "unknown";
|
|
1410
|
+
});
|
|
1411
|
+
output += opts.results.length === 1
|
|
1412
|
+
? `**Provider:** ${providerNames[0]}\n\n`
|
|
1413
|
+
: `**Providers used:** ${providerNames.map((name, index) => `Query ${index + 1}: ${name}`).join("; ")}\n\n`;
|
|
1375
1414
|
const duplicateQueries = opts.curated ? duplicateQuerySet(opts.results) : new Set<string>();
|
|
1376
1415
|
for (const { query, answer, results, error, provider } of opts.results) {
|
|
1377
1416
|
if (opts.queryList.length > 1) {
|
|
@@ -1395,26 +1434,37 @@ export default function (pi: ExtensionAPI) {
|
|
|
1395
1434
|
urls: opts.inlineContent,
|
|
1396
1435
|
} satisfies StoredSearchData & { type: "fetch"; urls: ExtractedContent[] };
|
|
1397
1436
|
pi.appendEntry("web-search-results", storeFetchedContentResult(fetchId, data));
|
|
1398
|
-
if (!hasApprovedSummary) {
|
|
1399
|
-
output += `---\nFull content for ${opts.inlineContent.length} sources available [${fetchId}].`;
|
|
1400
|
-
}
|
|
1401
1437
|
} else if (opts.includeContent) {
|
|
1402
1438
|
fetchId = startBackgroundFetch(opts.urls, opts.proxy);
|
|
1403
|
-
if (fetchId && !hasApprovedSummary) {
|
|
1404
|
-
output += `---\nContent fetching in background [${fetchId}]. Will notify when ready.`;
|
|
1405
|
-
}
|
|
1406
1439
|
}
|
|
1407
1440
|
|
|
1408
1441
|
const searchId = storeAndPublishSearch(opts.results);
|
|
1409
1442
|
const isBackgroundFetch = fetchId !== null && !hasInlineReady;
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1443
|
+
const unboundedPresentation = output.trim();
|
|
1444
|
+
const buildGuidance = (forTruncation: boolean): string => {
|
|
1445
|
+
let value = "";
|
|
1446
|
+
if (hasInlineReady && opts.inlineContent && fetchId) {
|
|
1447
|
+
value += `\n---\nFull content for ${opts.inlineContent.length} sources is ready as responseId "${fetchId}". `;
|
|
1448
|
+
value += getSearchContentEnabled
|
|
1449
|
+
? `Use ${toolNames.getSearchContent}({ responseId: "${fetchId}", urlIndex: 0, offset: 0, limit: ${maxInlineContentChars} }) to retrieve the first bounded page.`
|
|
1450
|
+
: forTruncation ? `Enable ${toolNames.getSearchContent} to retrieve the full stored content.` : "";
|
|
1451
|
+
} else if (isBackgroundFetch && fetchId) {
|
|
1452
|
+
value += `\n---\nContent fetching in background as responseId "${fetchId}". Will notify when ready.`;
|
|
1453
|
+
}
|
|
1454
|
+
if (getSearchContentEnabled || forTruncation) {
|
|
1455
|
+
value += `\n---\nFull search results are stored as responseId "${searchId}". `;
|
|
1456
|
+
value += getSearchContentEnabled
|
|
1457
|
+
? `Use ${toolNames.getSearchContent}({ responseId: "${searchId}", queryIndex: 0, offset: 0, limit: ${maxInlineContentChars} }) to retrieve the first bounded page${opts.results.length > 1 ? `; repeat with queryIndex 1 through ${opts.results.length - 1}` : ""}.`
|
|
1458
|
+
: `Enable ${toolNames.getSearchContent} to retrieve the full stored results.`;
|
|
1459
|
+
}
|
|
1460
|
+
return value;
|
|
1461
|
+
};
|
|
1462
|
+
const presentation = hasApprovedSummary
|
|
1463
|
+
? { text: unboundedPresentation, truncated: false, originalChars: unboundedPresentation.length, returnedChars: unboundedPresentation.length, omittedChars: 0 }
|
|
1464
|
+
: boundSearchPresentation(unboundedPresentation, buildGuidance(false), buildGuidance(true), maxInlineContentChars);
|
|
1415
1465
|
|
|
1416
1466
|
return {
|
|
1417
|
-
content: [{ type: "text", text:
|
|
1467
|
+
content: [{ type: "text", text: presentation.text }],
|
|
1418
1468
|
details: {
|
|
1419
1469
|
queries: opts.queryList,
|
|
1420
1470
|
queryCount: opts.queryList.length,
|
|
@@ -1424,6 +1474,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
1424
1474
|
fetchId,
|
|
1425
1475
|
fetchUrls: isBackgroundFetch ? opts.urls : undefined,
|
|
1426
1476
|
searchId,
|
|
1477
|
+
queryProviders: opts.results.map(result => ({
|
|
1478
|
+
query: result.query,
|
|
1479
|
+
providers: result.providers ?? (result.provider ? [result.provider] : []),
|
|
1480
|
+
})),
|
|
1481
|
+
truncated: presentation.truncated,
|
|
1482
|
+
originalChars: presentation.originalChars,
|
|
1483
|
+
returnedChars: presentation.returnedChars,
|
|
1484
|
+
omittedChars: presentation.omittedChars,
|
|
1427
1485
|
...(opts.curated ? {
|
|
1428
1486
|
curated: true,
|
|
1429
1487
|
curatedFrom: opts.curatedFrom,
|
|
@@ -1775,7 +1833,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1775
1833
|
name: toolNames.webSearch,
|
|
1776
1834
|
label: "Web Search",
|
|
1777
1835
|
description:
|
|
1778
|
-
`Search the web with ${allowedSearchProviders.map(providerLabel).join(", ")}. Provider arrays run simultaneously; ${allPolicyDescription}.
|
|
1836
|
+
`Search the web with ${allowedSearchProviders.map(providerLabel).join(", ")}. Provider arrays run simultaneously; ${allPolicyDescription}. The default workflow is none: it returns bounded source-linked search results or provider answers without a curator or generated summary, identifies the providers used, and stores full results for retrieval by responseId. For comprehensive research, prefer queries (plural) with 2-4 varied angles over a single query. When includeContent is true, full page content is fetched in the background. Set workflow to "summary-review" to open the curator with an auto-generated summary draft or "auto-summary" to generate a summary without the browser curator. The configured provider is used when provider is omitted or set to auto; omit provider unless explicitly overriding it.`,
|
|
1779
1837
|
promptSnippet:
|
|
1780
1838
|
"Use for web research questions. Prefer {queries:[...]} with 2-4 varied angles over a single query for broader coverage. Omit provider unless explicitly overriding the configured default.",
|
|
1781
1839
|
parameters: Type.Object({
|
|
@@ -2030,7 +2088,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2030
2088
|
});
|
|
2031
2089
|
|
|
2032
2090
|
try {
|
|
2033
|
-
const { answer, results, inlineContent, provider } = await search(query, {
|
|
2091
|
+
const { answer, results, inlineContent, provider, providerResponses } = await search(query, {
|
|
2034
2092
|
provider: resolvedProvider,
|
|
2035
2093
|
numResults: params.numResults,
|
|
2036
2094
|
recencyFilter,
|
|
@@ -2040,7 +2098,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
2040
2098
|
extensionContext: ctx,
|
|
2041
2099
|
});
|
|
2042
2100
|
|
|
2043
|
-
|
|
2101
|
+
const providers = providerResponses?.map(response => response.provider) ?? [provider];
|
|
2102
|
+
return { result: { query, answer, results, error: null, provider, providers } satisfies QueryResultData, inlineContent };
|
|
2044
2103
|
} catch (err) {
|
|
2045
2104
|
if (signal?.aborted || isAbortError(err)) throw err;
|
|
2046
2105
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -2830,7 +2889,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2830
2889
|
pi.registerTool({
|
|
2831
2890
|
name: toolNames.getSearchContent,
|
|
2832
2891
|
label: "Get Search Content",
|
|
2833
|
-
description: `Retrieve bounded
|
|
2892
|
+
description: `Retrieve bounded pages of full stored search results or fetched content, or find matching passages, from a previous ${storedContentSources} call.`,
|
|
2834
2893
|
promptSnippet:
|
|
2835
2894
|
`Use after ${storedContentSources} to retrieve stored content via responseId. Use findText to locate passages without paging through the full content.`,
|
|
2836
2895
|
parameters: Type.Object({
|
|
@@ -2839,8 +2898,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
2839
2898
|
queryIndex: Type.Optional(Type.Integer({ minimum: 0, description: "Get content for query at index" })),
|
|
2840
2899
|
url: Type.Optional(Type.String({ description: "Get content for this URL" })),
|
|
2841
2900
|
urlIndex: Type.Optional(Type.Integer({ minimum: 0, description: "Get content for URL at index" })),
|
|
2842
|
-
offset: Type.Optional(Type.Integer({ minimum: 0, description: "Character offset
|
|
2843
|
-
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: maxInlineContentChars, description: "
|
|
2901
|
+
offset: Type.Optional(Type.Integer({ minimum: 0, description: "Character offset in stored search or fetched URL content (default 0). Ignored when findText is supplied." })),
|
|
2902
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: maxInlineContentChars, description: "Requested maximum stored-content characters (default and max use maxInlineContentChars). Search-page continuation guidance shares the global output cap and may reduce returnedChars. Ignored when findText is supplied." })),
|
|
2844
2903
|
findText: Type.Optional(Type.Union([
|
|
2845
2904
|
Type.String({ minLength: 1, maxLength: 500 }),
|
|
2846
2905
|
Type.Array(Type.String({ minLength: 1, maxLength: 500 }), { minItems: 1, maxItems: 10 }),
|
|
@@ -2961,7 +3020,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2961
3020
|
const { text, ...findDetails } = found;
|
|
2962
3021
|
return {
|
|
2963
3022
|
content: [{ type: "text", text }],
|
|
2964
|
-
details: { query: queryData.query, resultCount: queryData.results.length, findMode: params.findMode ?? "case-insensitive", ...findDetails },
|
|
3023
|
+
details: { responseId: params.responseId, query: queryData.query, resultCount: queryData.results.length, contentLength: fullResults.length, findMode: params.findMode ?? "case-insensitive", ...findDetails },
|
|
2965
3024
|
};
|
|
2966
3025
|
} catch (err) {
|
|
2967
3026
|
const error = err instanceof Error ? err.message : String(err);
|
|
@@ -2972,9 +3031,53 @@ export default function (pi: ExtensionAPI) {
|
|
|
2972
3031
|
}
|
|
2973
3032
|
}
|
|
2974
3033
|
|
|
3034
|
+
const offset = params.offset ?? 0;
|
|
3035
|
+
const limit = params.limit ?? maxInlineContentChars;
|
|
3036
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
3037
|
+
return {
|
|
3038
|
+
content: [{ type: "text", text: `Invalid offset: received ${formatInputValue(offset)} for query ${formatInputValue(queryData.query)}; offset must be a non-negative integer. Use 0 or a larger integer.` }],
|
|
3039
|
+
details: { error: "Invalid offset", offset },
|
|
3040
|
+
};
|
|
3041
|
+
}
|
|
3042
|
+
if (!Number.isInteger(limit) || limit <= 0 || limit > maxInlineContentChars) {
|
|
3043
|
+
return {
|
|
3044
|
+
content: [{ type: "text", text: `Invalid limit: received ${formatInputValue(limit)} for query ${formatInputValue(queryData.query)}; limit must be an integer from 1 to ${maxInlineContentChars}. Use a value in that range.` }],
|
|
3045
|
+
details: { error: "Invalid limit", limit, maxLimit: maxInlineContentChars },
|
|
3046
|
+
};
|
|
3047
|
+
}
|
|
3048
|
+
if (offset > fullResults.length) {
|
|
3049
|
+
return {
|
|
3050
|
+
content: [{ type: "text", text: `Offset ${offset} is out of range for query ${formatInputValue(queryData.query)} in responseId ${formatInputValue(params.responseId)}. Received offset ${offset}; valid range is 0-${fullResults.length}. Use an offset within that range.` }],
|
|
3051
|
+
details: { error: "Offset out of range", offset, contentLength: fullResults.length },
|
|
3052
|
+
};
|
|
3053
|
+
}
|
|
3054
|
+
const queryIndex = data.queries.indexOf(queryData);
|
|
3055
|
+
let returnedChars = Math.min(limit, fullResults.length - offset);
|
|
3056
|
+
let endOffset = offset + returnedChars;
|
|
3057
|
+
let continuation = "";
|
|
3058
|
+
while (endOffset < fullResults.length) {
|
|
3059
|
+
continuation = `\n\n---\nShowing chars ${offset}-${endOffset} of ${fullResults.length}. Use ${toolNames.getSearchContent}({ responseId: "${params.responseId}", queryIndex: ${queryIndex}, offset: ${endOffset}, limit: ${limit} }) for the next slice.`;
|
|
3060
|
+
const overflow = returnedChars + continuation.length - maxInlineContentChars;
|
|
3061
|
+
if (overflow <= 0) break;
|
|
3062
|
+
returnedChars -= overflow;
|
|
3063
|
+
endOffset = offset + returnedChars;
|
|
3064
|
+
}
|
|
3065
|
+
const resultSlice = fullResults.slice(offset, endOffset);
|
|
3066
|
+
const hasMore = endOffset < fullResults.length;
|
|
3067
|
+
const text = `${resultSlice}${hasMore ? continuation : ""}`;
|
|
2975
3068
|
return {
|
|
2976
|
-
content: [{ type: "text", text
|
|
2977
|
-
details: {
|
|
3069
|
+
content: [{ type: "text", text }],
|
|
3070
|
+
details: {
|
|
3071
|
+
responseId: params.responseId,
|
|
3072
|
+
query: queryData.query,
|
|
3073
|
+
resultCount: queryData.results.length,
|
|
3074
|
+
contentLength: fullResults.length,
|
|
3075
|
+
offset,
|
|
3076
|
+
limit,
|
|
3077
|
+
returnedChars,
|
|
3078
|
+
nextOffset: hasMore ? endOffset : null,
|
|
3079
|
+
truncated: hasMore,
|
|
3080
|
+
},
|
|
2978
3081
|
};
|
|
2979
3082
|
}
|
|
2980
3083
|
|
|
@@ -3161,6 +3264,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
3161
3264
|
});
|
|
3162
3265
|
}
|
|
3163
3266
|
|
|
3267
|
+
const activationTools: WebActivationTool[] = [
|
|
3268
|
+
...(webSearchEnabled ? [{ name: toolNames.webSearch, capability: "search" as const }] : []),
|
|
3269
|
+
...(sourceCheckEnabled ? [{ name: toolNames.sourceCheck, capability: "source-check" as const }] : []),
|
|
3270
|
+
...(fetchContentEnabled ? [{ name: toolNames.fetchContent, capability: "fetch" as const }] : []),
|
|
3271
|
+
...(getSearchContentEnabled ? [{ name: toolNames.getSearchContent, capability: "stored-content" as const }] : []),
|
|
3272
|
+
];
|
|
3273
|
+
registerWebToolActivation(pi, activationTools);
|
|
3274
|
+
|
|
3164
3275
|
if (isCommandEnabled(initConfig, "websearch")) pi.registerCommand("websearch", {
|
|
3165
3276
|
description: "Open web search curator",
|
|
3166
3277
|
handler: async (args, ctx) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-web-access",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Web search, URL fetching, GitHub repo cloning, PDF extraction, YouTube video understanding, and local video analysis for Pi coding agent. Supports OpenAI, Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Crawl4AI, Jina, SERPdive, Kagi, Ollama, AnySearch, Bright Data SERP, SerpBase, SerpApi, Serply, SearXNG, Exa, Perplexity, Gemini, Kimi, and Mistral.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -45,11 +45,15 @@
|
|
|
45
45
|
"pi-web-fetch-demo.mp4"
|
|
46
46
|
],
|
|
47
47
|
"devDependencies": {
|
|
48
|
+
"@earendil-works/pi-ai": "0.86.1",
|
|
49
|
+
"@earendil-works/pi-coding-agent": "0.86.1",
|
|
50
|
+
"@earendil-works/pi-tui": "0.86.1",
|
|
48
51
|
"@types/turndown": "^5.0.6",
|
|
49
52
|
"esbuild": "^0.28.1",
|
|
50
53
|
"typescript": "^7.0.2"
|
|
51
54
|
},
|
|
52
55
|
"dependencies": {
|
|
56
|
+
"@modelcontextprotocol/sdk": "1.27.1",
|
|
53
57
|
"@mozilla/readability": "^0.6.0",
|
|
54
58
|
"defuddle": "0.19.3",
|
|
55
59
|
"linkedom": "^0.16.0",
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { buildSessionContext, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { Type } from "typebox";
|
|
6
|
+
|
|
7
|
+
export type WebCapability = "search" | "source-check" | "fetch" | "stored-content";
|
|
8
|
+
|
|
9
|
+
export interface WebActivationTool {
|
|
10
|
+
name: string;
|
|
11
|
+
capability: WebCapability;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const LOADER_NAME = "web_enable";
|
|
15
|
+
const CAPABILITY_LABELS: Record<WebCapability, string> = {
|
|
16
|
+
search: "web search",
|
|
17
|
+
"source-check": "source checking",
|
|
18
|
+
fetch: "content fetching",
|
|
19
|
+
"stored-content": "stored-result retrieval",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function supportsDynamicTools(pi: ExtensionAPI): boolean {
|
|
23
|
+
if (typeof pi.getAllTools !== "function" || typeof pi.getActiveTools !== "function" || typeof pi.setActiveTools !== "function") return false;
|
|
24
|
+
try {
|
|
25
|
+
const packagePath = join(dirname(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))), "..", "package.json");
|
|
26
|
+
const [major, minor, patch] = JSON.parse(readFileSync(packagePath, "utf8")).version.split(".").map(Number);
|
|
27
|
+
return major > 0 || minor > 86 || minor === 86 && patch >= 1;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hasToolDeclarations(messages: unknown[]): boolean {
|
|
34
|
+
return messages.some(message => message && typeof message === "object" && (
|
|
35
|
+
"toolsAdded" in message || "toolsRemoved" in message
|
|
36
|
+
));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function currentTranscriptToolNames(messages: unknown[]): Promise<string[]> {
|
|
40
|
+
const moduleName = "@earendil-works/pi-ai/utils/transcript";
|
|
41
|
+
const { getCurrentTools } = await import(moduleName);
|
|
42
|
+
return getCurrentTools(messages).map((tool: { name: string }) => tool.name);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function registerWebToolActivation(pi: ExtensionAPI, tools: ReadonlyArray<WebActivationTool>): void {
|
|
46
|
+
if (tools.length === 0) return;
|
|
47
|
+
if (!supportsDynamicTools(pi)) {
|
|
48
|
+
console.warn("[pi-web-access] Dynamic tool activation requires Pi 0.86.1 or newer; web tools remain eagerly available.");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const names = tools.map(tool => tool.name);
|
|
52
|
+
const capabilities = tools.map(tool => CAPABILITY_LABELS[tool.capability]).join(", ");
|
|
53
|
+
|
|
54
|
+
const parameters = Type.Object({}, { additionalProperties: false });
|
|
55
|
+
pi.registerTool<typeof parameters, Record<string, unknown>>({
|
|
56
|
+
name: LOADER_NAME,
|
|
57
|
+
label: "Enable Web Access",
|
|
58
|
+
description: "Enable configured pi-web-access tools for web research and content retrieval. Does not search or fetch. Enabled tools are available on the next model request; disabled capabilities remain unavailable.",
|
|
59
|
+
promptSnippet: `pi-web-access is configured for ${capabilities}. Call web_enable to activate these tools; use them on the next model request.`,
|
|
60
|
+
parameters,
|
|
61
|
+
async execute() {
|
|
62
|
+
const registered = new Set(pi.getAllTools().map(tool => tool.name));
|
|
63
|
+
const unavailable = names.filter(name => !registered.has(name));
|
|
64
|
+
if (unavailable.length > 0) {
|
|
65
|
+
return {
|
|
66
|
+
isError: true,
|
|
67
|
+
content: [{ type: "text" as const, text: `Cannot enable unavailable tools: ${unavailable.join(", ")}.` }],
|
|
68
|
+
details: { unavailable },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
pi.setActiveTools([...new Set([...pi.getActiveTools(), ...names])]);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
76
|
+
return {
|
|
77
|
+
isError: true,
|
|
78
|
+
content: [{ type: "text" as const, text: `Activation failed: ${message}.` }],
|
|
79
|
+
details: { error: message },
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const active = new Set(pi.getActiveTools());
|
|
84
|
+
const missing = names.filter(name => !active.has(name));
|
|
85
|
+
return missing.length > 0
|
|
86
|
+
? {
|
|
87
|
+
isError: true,
|
|
88
|
+
content: [{ type: "text" as const, text: `Tools still inactive after activation: ${missing.join(", ")}.` }],
|
|
89
|
+
details: { missing },
|
|
90
|
+
}
|
|
91
|
+
: {
|
|
92
|
+
content: [{ type: "text" as const, text: `Enabled: ${names.join(", ")}.` }],
|
|
93
|
+
details: { enabled: names },
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
function loaderAvailable(): boolean {
|
|
99
|
+
return pi.getAllTools().some(tool => tool.name === LOADER_NAME);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let warned = false;
|
|
103
|
+
async function selectFromSession(ctx: ExtensionContext): Promise<void> {
|
|
104
|
+
if (!loaderAvailable()) return;
|
|
105
|
+
try {
|
|
106
|
+
const messages = buildSessionContext(ctx.sessionManager.getBranch()).messages;
|
|
107
|
+
const recorded = hasToolDeclarations(messages)
|
|
108
|
+
? new Set(await currentTranscriptToolNames(messages))
|
|
109
|
+
: messages.length > 0
|
|
110
|
+
? new Set(names)
|
|
111
|
+
: new Set<string>();
|
|
112
|
+
const heavy = new Set(names);
|
|
113
|
+
const active = pi.getActiveTools().filter(name => !heavy.has(name));
|
|
114
|
+
for (const name of names) if (recorded.has(name)) active.push(name);
|
|
115
|
+
pi.setActiveTools([...new Set([...active, LOADER_NAME])]);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (!warned) {
|
|
118
|
+
warned = true;
|
|
119
|
+
console.warn(`[pi-web-access] Keeping web tools eagerly available because activation setup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
pi.on("session_start", (_event, ctx) => selectFromSession(ctx));
|
|
125
|
+
pi.on("session_tree", (_event, ctx) => selectFromSession(ctx));
|
|
126
|
+
pi.on("before_agent_start", () => {
|
|
127
|
+
if (!loaderAvailable() || pi.getActiveTools().includes(LOADER_NAME)) return;
|
|
128
|
+
try {
|
|
129
|
+
pi.setActiveTools([...pi.getActiveTools(), LOADER_NAME]);
|
|
130
|
+
} catch {
|
|
131
|
+
// Best effort: preserve the current selection if Pi rejects the update.
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -2,6 +2,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { isIP } from "node:net";
|
|
5
6
|
import { homedir, hostname, tmpdir } from "node:os";
|
|
6
7
|
import { join } from "node:path";
|
|
7
8
|
|
|
@@ -43,6 +44,13 @@ interface ApiBaseUrlOptions {
|
|
|
43
44
|
environmentValue: string | undefined;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
export function isLoopbackHostname(hostnameValue: string): boolean {
|
|
48
|
+
const normalized = hostnameValue.replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase();
|
|
49
|
+
return normalized === "localhost"
|
|
50
|
+
|| normalized === "::1"
|
|
51
|
+
|| (isIP(normalized) === 4 && normalized.split(".", 1)[0] === "127");
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
export function resolveApiBaseUrl(options: ApiBaseUrlOptions): string {
|
|
47
55
|
const fromEnvironment = options.environmentValue !== undefined;
|
|
48
56
|
const value = fromEnvironment ? options.environmentValue : options.configuredValue;
|
|
@@ -61,7 +69,7 @@ export function resolveApiBaseUrl(options: ApiBaseUrlOptions): string {
|
|
|
61
69
|
} catch {
|
|
62
70
|
throw new Error(`${source} must be an absolute HTTP(S) URL`);
|
|
63
71
|
}
|
|
64
|
-
if (url.protocol !== "https:") {
|
|
72
|
+
if (url.protocol !== "https:" && (url.protocol !== "http:" || !isLoopbackHostname(url.hostname))) {
|
|
65
73
|
throw new Error(`${source} must be an absolute HTTPS URL`);
|
|
66
74
|
}
|
|
67
75
|
if (url.username || url.password) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@herbertgao/pi-extensions",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.14",
|
|
4
4
|
"description": "Aggregate installer for HerbertGao-maintained Pi extensions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"extensions",
|
|
@@ -38,14 +38,14 @@
|
|
|
38
38
|
"@ast-grep/napi": "^0.45.0",
|
|
39
39
|
"@bufbuild/protobuf": "^2.14.0",
|
|
40
40
|
"@dietrichgebert/ponytail": "4.10.0",
|
|
41
|
-
"@earendil-works/pi-ai": "^0.
|
|
42
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
43
|
-
"@earendil-works/pi-tui": "^0.
|
|
44
|
-
"@herbertgao/pi-bark": "0.1.
|
|
45
|
-
"@herbertgao/pi-cc-extensions": "0.9.
|
|
46
|
-
"@herbertgao/pi-subagents": "0.18.
|
|
47
|
-
"@herbertgao/resume-from": "0.2.
|
|
48
|
-
"@herbertgao/sol-pi": "0.3.
|
|
41
|
+
"@earendil-works/pi-ai": "^0.87.1",
|
|
42
|
+
"@earendil-works/pi-coding-agent": "^0.87.1",
|
|
43
|
+
"@earendil-works/pi-tui": "^0.87.1",
|
|
44
|
+
"@herbertgao/pi-bark": "0.1.3",
|
|
45
|
+
"@herbertgao/pi-cc-extensions": "0.9.5",
|
|
46
|
+
"@herbertgao/pi-subagents": "0.18.3",
|
|
47
|
+
"@herbertgao/resume-from": "0.2.3",
|
|
48
|
+
"@herbertgao/sol-pi": "0.3.2",
|
|
49
49
|
"@juicesharp/rpiv-ask-user-question": "2.11.0",
|
|
50
50
|
"@juicesharp/rpiv-config": "^2.11.0",
|
|
51
51
|
"@modelcontextprotocol/client": "2.0.0",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
56
56
|
"@mozilla/readability": "^0.6.0",
|
|
57
57
|
"@napi-rs/keyring": "^1.3.0",
|
|
58
|
-
"@narumitw/pi-btw": "0.
|
|
58
|
+
"@narumitw/pi-btw": "0.61.0",
|
|
59
59
|
"@narumitw/pi-caffeinate": "0.49.8",
|
|
60
60
|
"@narumitw/pi-tui-kit": "^0.64.0",
|
|
61
61
|
"@noble/ed25519": "^3.1.0",
|
|
@@ -90,10 +90,10 @@
|
|
|
90
90
|
"pi-footer": "0.5.1",
|
|
91
91
|
"pi-jev-auto-mode": "0.4.1",
|
|
92
92
|
"pi-lens": "4.2.1",
|
|
93
|
-
"pi-mcp-adapter": "2.
|
|
94
|
-
"pi-multi-account": "1.
|
|
95
|
-
"pi-typesafe": "0.
|
|
96
|
-
"pi-web-access": "0.
|
|
93
|
+
"pi-mcp-adapter": "2.37.0",
|
|
94
|
+
"pi-multi-account": "1.23.1",
|
|
95
|
+
"pi-typesafe": "0.7.4",
|
|
96
|
+
"pi-web-access": "0.31.0",
|
|
97
97
|
"pidusage": "^4.0.1",
|
|
98
98
|
"promise.try": "^2.0.1",
|
|
99
99
|
"proper-lockfile": "4.1.2",
|