@herbertgao/pi-extensions 2026.9.18 → 2026.9.20
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 +20 -28
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +76 -59
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +76 -59
- package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/preview.webp +0 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +93 -34
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +346 -117
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/core.ts +6 -36
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/index.ts +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +24 -29
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +330 -360
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +56 -108
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/session.ts +97 -15
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/subagent.ts +80 -45
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/flush-docked-bash.ts +36 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer-layout.ts +229 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer.ts +479 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/startup-header.ts +92 -61
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/working-message.ts +75 -40
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +19 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +1080 -229
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +232 -259
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +42 -41
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +8 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +2 -7
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +277 -113
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/layout.ts +15 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +18 -72
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +61 -26
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/ansi-utils.ts +25 -57
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-component.ts +125 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-edit-render.ts +227 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-header.ts +136 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-highlight.ts +115 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-inline.ts +324 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-layout.ts +762 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-limits.ts +152 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-palette.ts +410 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-parse.ts +535 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-presentation.ts +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +21 -3219
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-text.ts +44 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-write-render.ts +568 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/index.ts +47 -10
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/line-width-safety.ts +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/write-execution.ts +1 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +145 -190
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/mcp-title.ts +39 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{message-display.ts → tool/message-display.ts} +24 -90
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/names.ts +426 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +90 -132
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{show-more-hint.ts → tool/show-more-hint.ts} +10 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/transcript-refresh.ts +8 -26
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/ansi-color.ts +44 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/ansi-text.ts +76 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/component-tree.ts +34 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/format.ts +28 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/fullscreen-detect.ts +51 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/patch-keys.ts +124 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/sgr-mouse.ts +70 -0
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +8 -8
- package/node_modules/pi-antigravity/README.md +29 -6
- package/node_modules/pi-antigravity/package.json +12 -7
- package/node_modules/pi-antigravity/src/auth/accounts.ts +327 -0
- package/node_modules/pi-antigravity/src/auth/index.ts +9 -0
- package/node_modules/pi-antigravity/src/client/client.ts +1 -1
- package/node_modules/pi-antigravity/src/image/image.ts +4 -4
- package/node_modules/pi-antigravity/src/index.ts +203 -13
- package/node_modules/pi-antigravity/src/search/index.ts +1 -0
- package/node_modules/pi-antigravity/src/search/search.ts +325 -0
- package/node_modules/pi-antigravity/src/stream/stream.ts +191 -14
- package/node_modules/pi-antigravity/src/types/enums.ts +45 -36
- package/node_modules/pi-antigravity/src/types/types.ts +5 -12
- package/node_modules/pi-antigravity/src/utils/http.ts +7 -2
- package/node_modules/pi-antigravity/src/utils/util.ts +31 -3
- package/node_modules/pi-typesafe/README.md +2 -0
- package/node_modules/pi-typesafe/package.json +2 -1
- package/node_modules/pi-web-access/CHANGELOG.md +72 -0
- package/node_modules/pi-web-access/README.md +34 -13
- package/node_modules/pi-web-access/chrome-cookies.ts +79 -25
- package/node_modules/pi-web-access/curator-page.ts +5 -1
- package/node_modules/pi-web-access/curator-server.ts +2 -0
- package/node_modules/pi-web-access/dist/index.js +1337 -1009
- package/node_modules/pi-web-access/exa.ts +20 -52
- package/node_modules/pi-web-access/extract.ts +41 -4
- package/node_modules/pi-web-access/gemini-search.ts +12 -4
- package/node_modules/pi-web-access/index.ts +29 -37
- package/node_modules/pi-web-access/openai-search.ts +35 -7
- package/node_modules/pi-web-access/package.json +6 -5
- package/node_modules/pi-web-access/page-query.ts +2 -2
- package/node_modules/pi-web-access/query-rewrite.ts +2 -3
- package/node_modules/pi-web-access/ssrf-protection.ts +27 -10
- package/node_modules/pi-web-access/storage.ts +48 -77
- package/node_modules/pi-web-access/summary-model-scope.ts +6 -77
- package/node_modules/pi-web-access/summary-review.ts +3 -4
- package/node_modules/pi-web-access/tavily.ts +67 -20
- package/node_modules/pi-web-access/tool-activation.ts +53 -33
- package/node_modules/pi-web-access/utils.ts +1 -1
- package/node_modules/pi-web-access/you.ts +164 -0
- package/node_modules/pi-web-access/zai.ts +161 -0
- package/package.json +7 -11
- package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/hero.en.svg +0 -101
- package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/hero.svg +0 -101
- package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/welcome.webp +0 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/render-utils.ts +0 -5
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/types.ts +0 -30
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/write-display-utils.ts +0 -12
- package/node_modules/pi-footer/CHANGELOG.md +0 -145
- package/node_modules/pi-footer/LICENSE +0 -21
- package/node_modules/pi-footer/README.md +0 -354
- package/node_modules/pi-footer/package.json +0 -68
- package/node_modules/pi-footer/src/cache.ts +0 -106
- package/node_modules/pi-footer/src/colors.ts +0 -237
- package/node_modules/pi-footer/src/config.ts +0 -202
- package/node_modules/pi-footer/src/event-widgets.ts +0 -41
- package/node_modules/pi-footer/src/extension-statuses.ts +0 -102
- package/node_modules/pi-footer/src/git.ts +0 -135
- package/node_modules/pi-footer/src/index.ts +0 -268
- package/node_modules/pi-footer/src/metrics.ts +0 -141
- package/node_modules/pi-footer/src/presets.ts +0 -383
- package/node_modules/pi-footer/src/render.ts +0 -114
- package/node_modules/pi-footer/src/separators.ts +0 -52
- package/node_modules/pi-footer/src/types.ts +0 -122
- package/node_modules/pi-footer/src/ui/color-level-confirm.ts +0 -22
- package/node_modules/pi-footer/src/ui/color-options.ts +0 -195
- package/node_modules/pi-footer/src/ui/config-lifecycle.ts +0 -61
- package/node_modules/pi-footer/src/ui/edit-colors.ts +0 -13
- package/node_modules/pi-footer/src/ui/events.ts +0 -35
- package/node_modules/pi-footer/src/ui/extension-status-picker.ts +0 -60
- package/node_modules/pi-footer/src/ui/extension-statuses.ts +0 -73
- package/node_modules/pi-footer/src/ui/fields.ts +0 -252
- package/node_modules/pi-footer/src/ui/global-menu.ts +0 -126
- package/node_modules/pi-footer/src/ui/helpers.ts +0 -44
- package/node_modules/pi-footer/src/ui/layout.ts +0 -17
- package/node_modules/pi-footer/src/ui/line-list.ts +0 -19
- package/node_modules/pi-footer/src/ui/model.ts +0 -64
- package/node_modules/pi-footer/src/ui/navigation.ts +0 -29
- package/node_modules/pi-footer/src/ui/option-edit.ts +0 -93
- package/node_modules/pi-footer/src/ui/overlay-render.ts +0 -103
- package/node_modules/pi-footer/src/ui/screen-context.ts +0 -18
- package/node_modules/pi-footer/src/ui/screen-controller.ts +0 -27
- package/node_modules/pi-footer/src/ui/screen-render.ts +0 -32
- package/node_modules/pi-footer/src/ui/screen-state.ts +0 -20
- package/node_modules/pi-footer/src/ui/screen.ts +0 -243
- package/node_modules/pi-footer/src/ui/screens/add-widget.ts +0 -92
- package/node_modules/pi-footer/src/ui/screens/confirm-exit.ts +0 -52
- package/node_modules/pi-footer/src/ui/screens/controller.ts +0 -12
- package/node_modules/pi-footer/src/ui/screens/edit-colors.ts +0 -68
- package/node_modules/pi-footer/src/ui/screens/edit-widget.ts +0 -115
- package/node_modules/pi-footer/src/ui/screens/extension-status-row.ts +0 -61
- package/node_modules/pi-footer/src/ui/screens/global.ts +0 -75
- package/node_modules/pi-footer/src/ui/screens/line-list.ts +0 -108
- package/node_modules/pi-footer/src/ui/screens/main.ts +0 -99
- package/node_modules/pi-footer/src/ui/screens/terminal.ts +0 -121
- package/node_modules/pi-footer/src/ui/screens/widget-list.ts +0 -154
- package/node_modules/pi-footer/src/ui/terminal-menu.ts +0 -32
- package/node_modules/pi-footer/src/ui/theme.ts +0 -48
- package/node_modules/pi-footer/src/ui/title-bar.ts +0 -100
- package/node_modules/pi-footer/src/ui/widget-actions.ts +0 -76
- package/node_modules/pi-footer/src/ui.ts +0 -66
- package/node_modules/pi-footer/src/widgets/context.ts +0 -28
- package/node_modules/pi-footer/src/widgets/core/active-tools.ts +0 -17
- package/node_modules/pi-footer/src/widgets/core/context-window.ts +0 -24
- package/node_modules/pi-footer/src/widgets/core/cwd-basename.ts +0 -19
- package/node_modules/pi-footer/src/widgets/core/cwd.ts +0 -108
- package/node_modules/pi-footer/src/widgets/core/event.ts +0 -34
- package/node_modules/pi-footer/src/widgets/core/external-status.ts +0 -102
- package/node_modules/pi-footer/src/widgets/core/model-provider.ts +0 -20
- package/node_modules/pi-footer/src/widgets/core/model.ts +0 -35
- package/node_modules/pi-footer/src/widgets/core/provider.ts +0 -17
- package/node_modules/pi-footer/src/widgets/core/session-name.ts +0 -17
- package/node_modules/pi-footer/src/widgets/core/text-verbosity.ts +0 -17
- package/node_modules/pi-footer/src/widgets/core/thinking-level.ts +0 -17
- package/node_modules/pi-footer/src/widgets/git/ahead-behind.ts +0 -17
- package/node_modules/pi-footer/src/widgets/git/branch.ts +0 -81
- package/node_modules/pi-footer/src/widgets/git/clean.ts +0 -19
- package/node_modules/pi-footer/src/widgets/git/deletions.ts +0 -17
- package/node_modules/pi-footer/src/widgets/git/diff.ts +0 -38
- package/node_modules/pi-footer/src/widgets/git/insertions.ts +0 -17
- package/node_modules/pi-footer/src/widgets/git/remote.ts +0 -17
- package/node_modules/pi-footer/src/widgets/git/root.ts +0 -17
- package/node_modules/pi-footer/src/widgets/git/sha.ts +0 -17
- package/node_modules/pi-footer/src/widgets/git/staged.ts +0 -17
- package/node_modules/pi-footer/src/widgets/git/status.ts +0 -19
- package/node_modules/pi-footer/src/widgets/git/unstaged.ts +0 -17
- package/node_modules/pi-footer/src/widgets/git/untracked.ts +0 -17
- package/node_modules/pi-footer/src/widgets/instance.ts +0 -118
- package/node_modules/pi-footer/src/widgets/layout/custom-text.ts +0 -18
- package/node_modules/pi-footer/src/widgets/layout/flex-separator.ts +0 -32
- package/node_modules/pi-footer/src/widgets/layout/separator.ts +0 -57
- package/node_modules/pi-footer/src/widgets/layout/spacer.ts +0 -31
- package/node_modules/pi-footer/src/widgets/options.ts +0 -122
- package/node_modules/pi-footer/src/widgets/project/runtime.ts +0 -361
- package/node_modules/pi-footer/src/widgets/registry.ts +0 -223
- package/node_modules/pi-footer/src/widgets/session/assistant-messages.ts +0 -17
- package/node_modules/pi-footer/src/widgets/session/compactions.ts +0 -17
- package/node_modules/pi-footer/src/widgets/session/elapsed.ts +0 -18
- package/node_modules/pi-footer/src/widgets/session/last-activity.ts +0 -18
- package/node_modules/pi-footer/src/widgets/session/messages.ts +0 -19
- package/node_modules/pi-footer/src/widgets/session/session-id.ts +0 -17
- package/node_modules/pi-footer/src/widgets/session/session-start.ts +0 -18
- package/node_modules/pi-footer/src/widgets/session/tool-results.ts +0 -17
- package/node_modules/pi-footer/src/widgets/session/total-messages.ts +0 -19
- package/node_modules/pi-footer/src/widgets/session/total-time.ts +0 -18
- package/node_modules/pi-footer/src/widgets/session/user-messages.ts +0 -17
- package/node_modules/pi-footer/src/widgets/store.ts +0 -26
- package/node_modules/pi-footer/src/widgets/tokens/cache-hit-rate.ts +0 -55
- package/node_modules/pi-footer/src/widgets/tokens/cache-read.ts +0 -18
- package/node_modules/pi-footer/src/widgets/tokens/cache-write.ts +0 -18
- package/node_modules/pi-footer/src/widgets/tokens/context-bar.ts +0 -108
- package/node_modules/pi-footer/src/widgets/tokens/context-length.ts +0 -26
- package/node_modules/pi-footer/src/widgets/tokens/context-remaining.ts +0 -29
- package/node_modules/pi-footer/src/widgets/tokens/context.ts +0 -29
- package/node_modules/pi-footer/src/widgets/tokens/cost.ts +0 -51
- package/node_modules/pi-footer/src/widgets/tokens/input-speed.ts +0 -25
- package/node_modules/pi-footer/src/widgets/tokens/input-tokens.ts +0 -18
- package/node_modules/pi-footer/src/widgets/tokens/output-speed.ts +0 -25
- package/node_modules/pi-footer/src/widgets/tokens/output-tokens.ts +0 -18
- package/node_modules/pi-footer/src/widgets/tokens/tokens.ts +0 -20
- package/node_modules/pi-footer/src/widgets/tokens/total-speed.ts +0 -25
- package/node_modules/pi-footer/src/widgets/tokens/total-tokens.ts +0 -18
- package/node_modules/pi-footer/src/widgets/types.ts +0 -181
- package/node_modules/pi-footer/src/widgets/utils/colors.ts +0 -11
- package/node_modules/pi-footer/src/widgets/utils/context.ts +0 -112
- package/node_modules/pi-footer/src/widgets/utils/session.ts +0 -19
- package/node_modules/pi-footer/src/widgets/utils/token-format.ts +0 -78
|
@@ -5,7 +5,7 @@ import { activityMonitor } from "./activity.ts";
|
|
|
5
5
|
import { normalizeDomain } from "./domain-filter-normalization.ts";
|
|
6
6
|
import type { SearchOptions, SearchResponse, SearchResult } from "./perplexity.ts";
|
|
7
7
|
import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
|
|
8
|
-
import { getWebSearchConfigPath } from "./utils.ts";
|
|
8
|
+
import { fetchWithCredentialRedirects, getWebSearchConfigPath } from "./utils.ts";
|
|
9
9
|
|
|
10
10
|
const OPENAI_RESPONSES_URL = "https://api.openai.com/v1/responses";
|
|
11
11
|
const CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
@@ -13,13 +13,16 @@ const CONFIG_PATH = getWebSearchConfigPath();
|
|
|
13
13
|
const SEARCH_TIMEOUT_MS = 60_000;
|
|
14
14
|
|
|
15
15
|
// The selected model runs the server-side web_search call and writes the cited summary.
|
|
16
|
-
// Prefer the newest mid-tier ("terra") model, then the newest bare mainline id
|
|
16
|
+
// Prefer the newest mid-tier ("terra") model, then the newest bare mainline id, then any
|
|
17
|
+
// other versioned GPT id (gateway providers such as opencode-go list only suffixed GPT
|
|
18
|
+
// ids next to non-OpenAI models; unversioned ids such as gpt-oss-* are skipped); price
|
|
17
19
|
// tiers ("pro"/"ultra" id segments) are excluded, and the numeric-aware sort keeps
|
|
18
20
|
// e.g. gpt-5.10 ahead of gpt-5.9.
|
|
19
21
|
const EXCLUDED_MODEL_SEGMENTS = new Set(["pro", "ultra"]);
|
|
20
22
|
const MODEL_PREFERENCE = [
|
|
21
23
|
(id: string) => id.includes("terra"),
|
|
22
24
|
(id: string) => /^gpt-\d+(\.\d+)?$/.test(id),
|
|
25
|
+
(id: string) => /^gpt-\d/.test(id),
|
|
23
26
|
];
|
|
24
27
|
const DEFAULT_SEARCH_PROVIDERS: readonly string[] = ["openai-codex", "openai"];
|
|
25
28
|
|
|
@@ -182,6 +185,9 @@ function resolveConfiguredResponsesUrl(value: unknown): string {
|
|
|
182
185
|
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
183
186
|
throw new Error(`openaiResponsesUrl in ${CONFIG_PATH} must use http or https`);
|
|
184
187
|
}
|
|
188
|
+
if (url.hostname === "opencode.ai" && url.protocol !== "https:") {
|
|
189
|
+
throw new Error(`openaiResponsesUrl in ${CONFIG_PATH} must use HTTPS for opencode.ai`);
|
|
190
|
+
}
|
|
185
191
|
return url.toString();
|
|
186
192
|
}
|
|
187
193
|
|
|
@@ -204,6 +210,9 @@ function resolveProviderResponsesUrl(baseUrl: unknown, useCodexEndpoint: boolean
|
|
|
204
210
|
} catch {
|
|
205
211
|
throw new CustomOpenAIBaseUrlError("OpenAI search configuration: openaiUseProviderBaseUrl requires an absolute http(s) provider baseUrl");
|
|
206
212
|
}
|
|
213
|
+
if (url.hostname === "opencode.ai" && url.protocol !== "https:") {
|
|
214
|
+
throw new CustomOpenAIBaseUrlError("OpenAI search configuration: opencode.ai provider baseUrl must use HTTPS");
|
|
215
|
+
}
|
|
207
216
|
const path = url.pathname.replace(/\/+$/u, "");
|
|
208
217
|
if (path.endsWith("/responses")) {
|
|
209
218
|
url.pathname = path;
|
|
@@ -239,6 +248,19 @@ function toRequestHeaders(headers: ProviderHeaders): Record<string, string> {
|
|
|
239
248
|
return requestHeaders;
|
|
240
249
|
}
|
|
241
250
|
|
|
251
|
+
function applyOpenCodeDestinationHeaders(headers: HeadersInit, requestUrl: string, ctx?: Pick<ExtensionContext, "sessionManager">): Headers {
|
|
252
|
+
const requestHeaders = new Headers(headers);
|
|
253
|
+
requestHeaders.delete("x-opencode-session");
|
|
254
|
+
requestHeaders.delete("x-opencode-client");
|
|
255
|
+
const url = new URL(requestUrl);
|
|
256
|
+
const sessionId = ctx?.sessionManager?.getSessionId?.();
|
|
257
|
+
if (url.protocol === "https:" && url.hostname === "opencode.ai" && sessionId) {
|
|
258
|
+
requestHeaders.set("x-opencode-session", sessionId);
|
|
259
|
+
requestHeaders.set("x-opencode-client", "pi");
|
|
260
|
+
}
|
|
261
|
+
return requestHeaders;
|
|
262
|
+
}
|
|
263
|
+
|
|
242
264
|
async function resolvePiAuth(ctx: ExtensionContext, responsesUrl: string, providers: readonly string[], modelOverride?: string, hasExplicitResponsesUrl = false, useProviderBaseUrl = false): Promise<OpenAIAuth | undefined> {
|
|
243
265
|
let models: ReturnType<typeof ctx.modelRegistry.getAll>;
|
|
244
266
|
let invalidProviderUrlError: CustomOpenAIBaseUrlError | undefined;
|
|
@@ -643,6 +665,7 @@ async function runOpenAISearch(
|
|
|
643
665
|
query: string,
|
|
644
666
|
options: SearchOptions,
|
|
645
667
|
auth: OpenAIAuth,
|
|
668
|
+
ctx?: Pick<ExtensionContext, "sessionManager">,
|
|
646
669
|
): Promise<SearchResponse> {
|
|
647
670
|
const useAlphaSearch = isAlphaSearchEnabled();
|
|
648
671
|
if (useAlphaSearch) options.signal?.throwIfAborted();
|
|
@@ -672,16 +695,21 @@ async function runOpenAISearch(
|
|
|
672
695
|
if (accountId) headers["chatgpt-account-id"] = accountId;
|
|
673
696
|
headers.originator = "pi";
|
|
674
697
|
}
|
|
698
|
+
const requestHeaders = applyOpenCodeDestinationHeaders(
|
|
699
|
+
useAlphaSearch ? buildAlphaSearchHeaders(headers, auth.apiKey) : headers,
|
|
700
|
+
requestUrl,
|
|
701
|
+
ctx,
|
|
702
|
+
);
|
|
675
703
|
|
|
676
704
|
try {
|
|
677
|
-
const response = await
|
|
705
|
+
const response = await fetchWithCredentialRedirects(requestUrl, {
|
|
678
706
|
method: "POST",
|
|
679
|
-
headers:
|
|
707
|
+
headers: requestHeaders,
|
|
680
708
|
body: JSON.stringify(body),
|
|
681
709
|
signal: options.signal
|
|
682
710
|
? AbortSignal.any([AbortSignal.timeout(SEARCH_TIMEOUT_MS), options.signal])
|
|
683
711
|
: AbortSignal.timeout(SEARCH_TIMEOUT_MS),
|
|
684
|
-
});
|
|
712
|
+
}, ["Authorization", "x-opencode-session", "x-opencode-client", "chatgpt-account-id", ...Object.keys(auth.headers)]);
|
|
685
713
|
|
|
686
714
|
if (!response.ok) {
|
|
687
715
|
activityMonitor.logError(activityId, `HTTP ${response.status}`);
|
|
@@ -738,7 +766,7 @@ export async function searchWithOpenAI(
|
|
|
738
766
|
" 3. Set OPENAI_API_KEY environment variable",
|
|
739
767
|
);
|
|
740
768
|
}
|
|
741
|
-
return runOpenAISearch(query, options, auth);
|
|
769
|
+
return runOpenAISearch(query, options, auth, ctx);
|
|
742
770
|
}
|
|
743
771
|
|
|
744
772
|
export async function searchWithCurrentModelOpenAI(
|
|
@@ -748,5 +776,5 @@ export async function searchWithCurrentModelOpenAI(
|
|
|
748
776
|
): Promise<SearchResponse> {
|
|
749
777
|
if (!ctx) throw new Error("OpenAI current-model search requires an extension context");
|
|
750
778
|
const auth = await resolveCurrentModelAuth(ctx, options.signal);
|
|
751
|
-
return runOpenAISearch(query, options, auth);
|
|
779
|
+
return runOpenAISearch(query, options, auth, ctx);
|
|
752
780
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-web-access",
|
|
3
|
-
"version": "0.
|
|
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.",
|
|
3
|
+
"version": "0.34.0",
|
|
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, You.com, SearXNG, Exa, Perplexity, Gemini, Kimi, and Mistral.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "node --test",
|
|
7
|
+
"test": "node --import ./test/isolate-env.mjs --test",
|
|
8
8
|
"typecheck": "tsc",
|
|
9
9
|
"audit:runtime": "npm audit --omit=dev --omit=peer",
|
|
10
10
|
"build": "node scripts/build.js",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"@earendil-works/pi-tui": "0.86.1",
|
|
51
51
|
"@types/turndown": "^5.0.6",
|
|
52
52
|
"esbuild": "^0.28.1",
|
|
53
|
+
"typebox": "1.3.27",
|
|
53
54
|
"typescript": "^7.0.2"
|
|
54
55
|
},
|
|
55
56
|
"dependencies": {
|
|
@@ -60,8 +61,8 @@
|
|
|
60
61
|
"p-limit": "^6.1.0",
|
|
61
62
|
"promise.try": "^2.0.1",
|
|
62
63
|
"turndown": "^7.2.0",
|
|
63
|
-
"
|
|
64
|
-
"
|
|
64
|
+
"undici": "^8.11.2",
|
|
65
|
+
"unpdf": "^1.6.2"
|
|
65
66
|
},
|
|
66
67
|
"peerDependencies": {
|
|
67
68
|
"@earendil-works/pi-ai": "*",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { complete, Api, Message, Model } from "@earendil-works/pi-ai/compat";
|
|
2
2
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
-
import { findModelWithProviderRouting,
|
|
4
|
+
import { findModelWithProviderRouting, isModelInScope } from "./summary-model-scope.ts";
|
|
5
5
|
import { getWebSearchConfigPath } from "./utils.ts";
|
|
6
6
|
import { awaitWithAbort } from "./abortable.ts";
|
|
7
7
|
import { openCodeSessionHeaders } from "./opencode-session-headers.ts";
|
|
@@ -84,7 +84,7 @@ function resolveModel(ctx: ExtensionContext, override?: string, configured?: Ans
|
|
|
84
84
|
throw new Error("No current model available for page answering");
|
|
85
85
|
}
|
|
86
86
|
if (!model.input.includes("text")) throw new Error(`Answer model does not support text input: ${model.provider}/${model.id}`);
|
|
87
|
-
if (!
|
|
87
|
+
if (!isModelInScope(model, ctx.scopedModels)) {
|
|
88
88
|
throw new Error(`Answer model is not enabled: ${model.provider}/${model.id}`);
|
|
89
89
|
}
|
|
90
90
|
return model;
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import type { complete, Api, Model, ProviderHeaders } from "@earendil-works/pi-ai/compat";
|
|
2
2
|
import type { SummaryGenerationContext } from "./summary-review.ts";
|
|
3
3
|
import { awaitWithAbort } from "./abortable.ts";
|
|
4
|
-
import { findModelWithProviderRouting,
|
|
4
|
+
import { findModelWithProviderRouting, isModelInScope } from "./summary-model-scope.ts";
|
|
5
5
|
|
|
6
6
|
async function resolveFirstAvailableModel(
|
|
7
7
|
ctx: SummaryGenerationContext,
|
|
8
8
|
candidates: Array<{ provider: string; id: string }>,
|
|
9
9
|
): Promise<{ model: Model<Api>; apiKey: string; headers?: ProviderHeaders }> {
|
|
10
|
-
const enabledModelPatterns = loadEnabledModelPatterns(ctx);
|
|
11
10
|
for (const { provider, id } of candidates) {
|
|
12
11
|
const model = findModelWithProviderRouting(ctx.modelRegistry, provider, id);
|
|
13
|
-
if (!model || !
|
|
12
|
+
if (!model || !isModelInScope(model, ctx.scopedModels)) continue;
|
|
14
13
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
15
14
|
if (auth.ok && auth.apiKey) return { model, apiKey: auth.apiKey, headers: auth.headers };
|
|
16
15
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { lookup as dnsLookup } from "node:dns/promises";
|
|
2
2
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import net from "node:net";
|
|
4
|
-
import { getActiveProxy, getWebSearchConfigPath, hasScopedProxyDecision, isProxyBypassedUrl } from "./utils.ts";
|
|
4
|
+
import { getActiveProxy, getWebSearchConfigPath, hasScopedProxyDecision, isProxyBypassedUrl, loadConfiguredProxy, normalizeProxyUrl } from "./utils.ts";
|
|
5
5
|
|
|
6
6
|
const DEFAULT_MAX_REDIRECTS = 5;
|
|
7
7
|
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
8
8
|
const LOOPBACK_ALLOW_RANGES = ["127.0.0.0/8", "::1", "::ffff:127.0.0.0/104"];
|
|
9
|
+
// Proxy schemes where the proxy, not curl, resolves the target hostname.
|
|
10
|
+
const REMOTE_DNS_PROXY_PROTOCOLS = new Set(["http:", "https:", "socks4a:", "socks5h:"]);
|
|
9
11
|
|
|
10
12
|
export type LookupAddress = { address: string; family: number };
|
|
11
13
|
export type Lookup = (hostname: string) => Promise<LookupAddress[]>;
|
|
@@ -147,12 +149,15 @@ interface ValidationOptions {
|
|
|
147
149
|
*/
|
|
148
150
|
allowRanges?: string[];
|
|
149
151
|
/**
|
|
150
|
-
* When true,
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
152
|
+
* When true, let the proxy carrying the request resolve hostnames instead of
|
|
153
|
+
* performing local DNS lookups: an HTTP(S) proxy from the environment, or the
|
|
154
|
+
* `proxy` configured in web-search.json. A different per-call proxy never
|
|
155
|
+
* qualifies. Literal IPs and localhost remain blocked, and NO_PROXY hosts still
|
|
156
|
+
* use the local SSRF preflight. This does not configure proxy transport.
|
|
154
157
|
*/
|
|
155
158
|
trustEnvProxy?: boolean;
|
|
159
|
+
/** Proxy pinned on the request via `__proxy`; defaults to the scoped proxy, like the transport. */
|
|
160
|
+
proxy?: string;
|
|
156
161
|
/** Allow loopback URLs for explicit provider base endpoints, not fetched targets. */
|
|
157
162
|
allowLoopback?: boolean;
|
|
158
163
|
}
|
|
@@ -207,14 +212,21 @@ export async function validateRemoteUrl(rawUrl: string | URL, options: Validatio
|
|
|
207
212
|
return url;
|
|
208
213
|
}
|
|
209
214
|
|
|
210
|
-
|
|
215
|
+
const trustProxy = options.trustEnvProxy === true;
|
|
216
|
+
const proxy = options.proxy !== undefined
|
|
217
|
+
? normalizeProxyUrl(options.proxy, "proxy")
|
|
218
|
+
: hasScopedProxyDecision() ? getActiveProxy() : undefined;
|
|
219
|
+
if (proxy === undefined ? shouldTrustEnvProxy(url, trustProxy) : trustProxy && isConfiguredRemoteDnsProxy(url, proxy)) return url;
|
|
211
220
|
|
|
212
221
|
let addresses: LookupAddress[];
|
|
213
222
|
try {
|
|
214
223
|
addresses = await (options.lookup ?? defaultLookup)(hostname);
|
|
215
224
|
} catch (err) {
|
|
216
225
|
const message = err instanceof Error ? err.message : String(err);
|
|
217
|
-
|
|
226
|
+
const hint = !trustProxy && proxy && isConfiguredRemoteDnsProxy(url, proxy)
|
|
227
|
+
? ". If your configured proxy resolves hostnames, set ssrf.trustEnvProxy to true in web-search.json."
|
|
228
|
+
: "";
|
|
229
|
+
throw new Error(`Failed to resolve ${hostname}: ${message}${hint}`);
|
|
218
230
|
}
|
|
219
231
|
|
|
220
232
|
if (addresses.length === 0) throw new Error(`Failed to resolve ${hostname}: no addresses returned`);
|
|
@@ -333,15 +345,20 @@ function hostnameMatchesNoProxy(hostname: string, port: string, entry: string):
|
|
|
333
345
|
|
|
334
346
|
function shouldTrustEnvProxy(url: URL, enabled: boolean): boolean {
|
|
335
347
|
if (!enabled || !getProxyForProtocol(url.protocol)) return false;
|
|
336
|
-
if (hasScopedProxyDecision()) return false;
|
|
337
|
-
const activeProxy = getActiveProxy();
|
|
338
|
-
if (activeProxy && !isProxyBypassedUrl(url)) return false;
|
|
339
348
|
const hostname = normalizeHostname(url.hostname);
|
|
340
349
|
const port = url.port || (url.protocol === "https:" ? "443" : "80");
|
|
341
350
|
const noProxy = process.env.NO_PROXY || process.env.no_proxy || "";
|
|
342
351
|
return !noProxy.split(",").some(entry => hostnameMatchesNoProxy(hostname, port, entry));
|
|
343
352
|
}
|
|
344
353
|
|
|
354
|
+
/** Only the operator's configured proxy qualifies: a model-supplied per-call proxy must not skip DNS validation. */
|
|
355
|
+
function isConfiguredRemoteDnsProxy(url: URL, proxy: string | null): boolean {
|
|
356
|
+
return proxy !== null &&
|
|
357
|
+
REMOTE_DNS_PROXY_PROTOCOLS.has(new URL(proxy).protocol) &&
|
|
358
|
+
!isProxyBypassedUrl(url) &&
|
|
359
|
+
proxy === loadConfiguredProxy();
|
|
360
|
+
}
|
|
361
|
+
|
|
345
362
|
function assertPublicAddress(address: string, hostname: string, allowRanges: ParsedCidr[] = []): void {
|
|
346
363
|
const normalized = normalizeHostname(address);
|
|
347
364
|
const ipVersion = net.isIP(normalized);
|
|
@@ -74,7 +74,9 @@ export function generateId(): string {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
export function getFetchCacheDir(): string {
|
|
77
|
-
|
|
77
|
+
// Always append our own folder: pruning deletes stale *.json files, so the override must
|
|
78
|
+
// never point pruning at a directory that holds user files.
|
|
79
|
+
return join(process.env.PI_WEB_ACCESS_CACHE_ROOT || getWebSearchConfigDir(), FETCH_CACHE_DIR);
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
function fetchCachePath(key: string): string | null {
|
|
@@ -151,17 +153,9 @@ function cacheLimits(limits?: Partial<FetchCacheLimits>): FetchCacheLimits {
|
|
|
151
153
|
return resolved;
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
function
|
|
156
|
+
function enforceMode(fd: number, mode: number): void {
|
|
155
157
|
try {
|
|
156
|
-
fchmodSync(fd,
|
|
157
|
-
} catch (err) {
|
|
158
|
-
if (process.platform !== "win32") throw err;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function enforceFileMode(fd: number): void {
|
|
163
|
-
try {
|
|
164
|
-
fchmodSync(fd, 0o600);
|
|
158
|
+
fchmodSync(fd, mode);
|
|
165
159
|
} catch (err) {
|
|
166
160
|
if (process.platform !== "win32") throw err;
|
|
167
161
|
}
|
|
@@ -182,31 +176,23 @@ function safeFetchCacheDir(create: boolean): string | null {
|
|
|
182
176
|
if (before.isSymbolicLink() || !before.isDirectory()) {
|
|
183
177
|
throw new Error("Fetched content cache path is not a safe directory");
|
|
184
178
|
}
|
|
185
|
-
if (process.platform
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
179
|
+
if (process.platform !== "win32") {
|
|
180
|
+
const fd = openSync(dir, constants.O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
|
|
181
|
+
try {
|
|
182
|
+
const opened = fstatSync(fd);
|
|
183
|
+
if (!opened.isDirectory() || opened.dev !== before.dev || opened.ino !== before.ino) {
|
|
184
|
+
throw new Error("Fetched content cache directory changed while opening");
|
|
185
|
+
}
|
|
186
|
+
enforceMode(fd, 0o700);
|
|
187
|
+
} finally {
|
|
188
|
+
closeSync(fd);
|
|
189
189
|
}
|
|
190
|
-
return dir;
|
|
191
190
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const opened = fstatSync(fd);
|
|
196
|
-
if (!opened.isDirectory() || opened.dev !== before.dev || opened.ino !== before.ino) {
|
|
197
|
-
throw new Error("Fetched content cache directory changed while opening");
|
|
198
|
-
}
|
|
199
|
-
enforceDirectoryMode(fd);
|
|
200
|
-
closeSync(fd);
|
|
201
|
-
fd = null;
|
|
202
|
-
const after = lstatSync(dir);
|
|
203
|
-
if (after.isSymbolicLink() || !after.isDirectory() || after.dev !== before.dev || after.ino !== before.ino) {
|
|
204
|
-
throw new Error("Fetched content cache directory changed while securing it");
|
|
205
|
-
}
|
|
206
|
-
return dir;
|
|
207
|
-
} finally {
|
|
208
|
-
if (fd !== null) try { closeSync(fd); } catch {}
|
|
191
|
+
const after = lstatSync(dir);
|
|
192
|
+
if (after.isSymbolicLink() || !after.isDirectory() || after.dev !== before.dev || after.ino !== before.ino) {
|
|
193
|
+
throw new Error("Fetched content cache directory changed while securing it");
|
|
209
194
|
}
|
|
195
|
+
return dir;
|
|
210
196
|
}
|
|
211
197
|
|
|
212
198
|
function openRegularFile(path: string): { fd: number; info: Stats } {
|
|
@@ -225,28 +211,22 @@ function openRegularFile(path: string): { fd: number; info: Stats } {
|
|
|
225
211
|
}
|
|
226
212
|
}
|
|
227
213
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
function unlinkCacheFile(dir: string, file: CacheFile): CacheUnlinkResult {
|
|
214
|
+
// True once the entry is gone; false when the directory or entry changed underneath us or could not be removed.
|
|
215
|
+
function unlinkCacheFile(dir: string, file: CacheFile): boolean {
|
|
231
216
|
try {
|
|
232
217
|
const root = lstatSync(dir);
|
|
233
|
-
if (root.isSymbolicLink() || !root.isDirectory()) return
|
|
234
|
-
const path = join(dir, file.name);
|
|
235
|
-
let current: Stats;
|
|
236
|
-
try {
|
|
237
|
-
current = lstatSync(path);
|
|
238
|
-
} catch (err) {
|
|
239
|
-
return (err as NodeJS.ErrnoException).code === "ENOENT" ? "missing" : "error";
|
|
240
|
-
}
|
|
241
|
-
if (current.isSymbolicLink() || !current.isFile() || current.dev !== file.dev || current.ino !== file.ino) return "changed";
|
|
242
|
-
try {
|
|
243
|
-
unlinkSync(path);
|
|
244
|
-
return "removed";
|
|
245
|
-
} catch (err) {
|
|
246
|
-
return (err as NodeJS.ErrnoException).code === "ENOENT" ? "missing" : "error";
|
|
247
|
-
}
|
|
218
|
+
if (root.isSymbolicLink() || !root.isDirectory()) return false;
|
|
248
219
|
} catch {
|
|
249
|
-
return
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const path = join(dir, file.name);
|
|
224
|
+
const current = lstatSync(path);
|
|
225
|
+
if (current.isSymbolicLink() || !current.isFile() || current.dev !== file.dev || current.ino !== file.ino) return false;
|
|
226
|
+
unlinkSync(path);
|
|
227
|
+
return true;
|
|
228
|
+
} catch (err) {
|
|
229
|
+
return (err as NodeJS.ErrnoException).code === "ENOENT";
|
|
250
230
|
}
|
|
251
231
|
}
|
|
252
232
|
|
|
@@ -269,24 +249,21 @@ function pruneFetchCache(now: number, limits: FetchCacheLimits, preferredKey?: s
|
|
|
269
249
|
const files: CacheFile[] = [];
|
|
270
250
|
for (const entry of entries) {
|
|
271
251
|
if (!CACHE_KEY_PATTERN.test(entry) && !CACHE_TMP_PATTERN.test(entry)) continue;
|
|
272
|
-
|
|
273
|
-
let opened: ReturnType<typeof openRegularFile>;
|
|
274
|
-
try {
|
|
275
|
-
opened = openRegularFile(path);
|
|
276
|
-
} catch {
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
252
|
+
let info: Stats;
|
|
279
253
|
try {
|
|
280
|
-
|
|
254
|
+
const opened = openRegularFile(join(dir, entry));
|
|
255
|
+
info = opened.info;
|
|
256
|
+
try {
|
|
257
|
+
enforceMode(opened.fd, 0o600);
|
|
258
|
+
} finally {
|
|
259
|
+
closeSync(opened.fd);
|
|
260
|
+
}
|
|
281
261
|
} catch {
|
|
282
|
-
closeSync(opened.fd);
|
|
283
262
|
continue;
|
|
284
263
|
}
|
|
285
|
-
|
|
286
|
-
const file = { name: entry, size: opened.info.size, mtimeMs: opened.info.mtimeMs, dev: opened.info.dev, ino: opened.info.ino };
|
|
264
|
+
const file = { name: entry, size: info.size, mtimeMs: info.mtimeMs, dev: info.dev, ino: info.ino };
|
|
287
265
|
if (now - file.mtimeMs >= CACHE_TTL_MS) {
|
|
288
|
-
|
|
289
|
-
if (removed !== "removed" && removed !== "missing") return false;
|
|
266
|
+
if (!unlinkCacheFile(dir, file)) return false;
|
|
290
267
|
continue;
|
|
291
268
|
}
|
|
292
269
|
if (CACHE_KEY_PATTERN.test(entry)) files.push(file);
|
|
@@ -307,8 +284,7 @@ function pruneFetchCache(now: number, limits: FetchCacheLimits, preferredKey?: s
|
|
|
307
284
|
if (index < 0) break;
|
|
308
285
|
const file = files[index];
|
|
309
286
|
attempted.add(file.name);
|
|
310
|
-
|
|
311
|
-
if (removed === "removed" || removed === "missing") files.splice(index, 1);
|
|
287
|
+
if (unlinkCacheFile(dir, file)) files.splice(index, 1);
|
|
312
288
|
usage = projectedUsage();
|
|
313
289
|
}
|
|
314
290
|
return usage.entries <= limits.maxEntries && usage.bytes <= limits.maxBytes;
|
|
@@ -335,7 +311,7 @@ function writeFetchCache(data: StoredSearchData & { urls: ExtractedContent[] }):
|
|
|
335
311
|
fd = openSync(tmpPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | O_NOFOLLOW, 0o600);
|
|
336
312
|
const tmpInfo = fstatSync(fd);
|
|
337
313
|
tmpFile = { name: tmpName, size: tmpInfo.size, mtimeMs: tmpInfo.mtimeMs, dev: tmpInfo.dev, ino: tmpInfo.ino };
|
|
338
|
-
|
|
314
|
+
enforceMode(fd, 0o600);
|
|
339
315
|
writeFileSync(fd, serialized, "utf8");
|
|
340
316
|
fsyncSync(fd);
|
|
341
317
|
closeSync(fd);
|
|
@@ -358,11 +334,6 @@ function writeFetchCache(data: StoredSearchData & { urls: ExtractedContent[] }):
|
|
|
358
334
|
return { version: FETCH_CACHE_VERSION, key, storedAt: Date.now() };
|
|
359
335
|
}
|
|
360
336
|
|
|
361
|
-
function cacheWriteError(err: unknown): string {
|
|
362
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
363
|
-
return `Failed to write fetched content cache: ${message}`;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
337
|
function createFetchSessionData(data: StoredSearchData & { urls: ExtractedContent[] }, ref: FetchCacheRef | null, cacheError?: string): StoredSearchData {
|
|
367
338
|
return {
|
|
368
339
|
id: data.id,
|
|
@@ -394,9 +365,9 @@ function unavailableFetchData(data: StoredSearchData, reason: string): StoredSea
|
|
|
394
365
|
};
|
|
395
366
|
}
|
|
396
367
|
|
|
397
|
-
function readCachedFetchData(data: StoredSearchData
|
|
368
|
+
function readCachedFetchData(data: StoredSearchData): StoredSearchData {
|
|
398
369
|
if (data.type !== "fetch") return data;
|
|
399
|
-
if (now - data.timestamp >= CACHE_TTL_MS) {
|
|
370
|
+
if (Date.now() - data.timestamp >= CACHE_TTL_MS) {
|
|
400
371
|
return unavailableFetchData(data, "Cached fetched content is missing or expired");
|
|
401
372
|
}
|
|
402
373
|
if (isInlineFetchData(data)) return data;
|
|
@@ -410,7 +381,7 @@ function readCachedFetchData(data: StoredSearchData, now = Date.now()): StoredSe
|
|
|
410
381
|
if (!safeFetchCacheDir(false)) return unavailableFetchData(data, "Cached fetched content is missing or expired");
|
|
411
382
|
const opened = openRegularFile(path);
|
|
412
383
|
fd = opened.fd;
|
|
413
|
-
|
|
384
|
+
enforceMode(fd, 0o600);
|
|
414
385
|
const parsed: unknown = JSON.parse(readFileSync(fd, "utf8"));
|
|
415
386
|
if (!isValidStoredData(parsed) || parsed.type !== "fetch" || parsed.id !== data.id || !isInlineFetchData(parsed)) {
|
|
416
387
|
return unavailableFetchData(data, "Cached fetched content is invalid");
|
|
@@ -452,7 +423,7 @@ export function storeFetchedContentResult(id: string, data: StoredSearchData & {
|
|
|
452
423
|
try {
|
|
453
424
|
ref = writeFetchCache(data);
|
|
454
425
|
} catch (err) {
|
|
455
|
-
cacheError =
|
|
426
|
+
cacheError = `Failed to write fetched content cache: ${err instanceof Error ? err.message : String(err)}`;
|
|
456
427
|
}
|
|
457
428
|
storedResults.set(id, ref ? { ...data, fetchCache: ref, urlMetadata: metadataForUrls(data.urls) } : { ...data, fetchCacheError: cacheError });
|
|
458
429
|
return createFetchSessionData(data, ref, cacheError);
|
|
@@ -1,16 +1,7 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
|
|
5
1
|
const THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
6
2
|
|
|
7
3
|
export type SummaryThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
8
4
|
|
|
9
|
-
interface SummaryModelScopeContext {
|
|
10
|
-
cwd: string;
|
|
11
|
-
isProjectTrusted(): boolean;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
5
|
export interface ModelLike {
|
|
15
6
|
provider: string;
|
|
16
7
|
id: string;
|
|
@@ -45,39 +36,12 @@ export function findModelWithProviderRouting<T extends ModelLike>(
|
|
|
45
36
|
return routed ?? registry.find(provider, id);
|
|
46
37
|
}
|
|
47
38
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
const raw = readFileSync(path, "utf8");
|
|
55
|
-
try {
|
|
56
|
-
return JSON.parse(raw) as Record<string, unknown>;
|
|
57
|
-
} catch (err) {
|
|
58
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
59
|
-
throw new Error(`Failed to parse ${path}: ${message}`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function loadEnabledModelPatterns(ctx: SummaryModelScopeContext): string[] | null {
|
|
64
|
-
const globalSettings = readSettings(join(getAgentDir(), "settings.json"));
|
|
65
|
-
const projectSettings = ctx.isProjectTrusted()
|
|
66
|
-
? readSettings(join(ctx.cwd, ".pi", "settings.json"))
|
|
67
|
-
: {};
|
|
68
|
-
const value = Object.hasOwn(projectSettings, "enabledModels")
|
|
69
|
-
? projectSettings.enabledModels
|
|
70
|
-
: globalSettings.enabledModels;
|
|
71
|
-
if (value === undefined) return null;
|
|
72
|
-
if (!Array.isArray(value)) throw new Error("enabledModels must be an array");
|
|
73
|
-
return value
|
|
74
|
-
.filter((item): item is string => typeof item === "string")
|
|
75
|
-
.map(item => item.trim())
|
|
76
|
-
.filter(Boolean);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function summaryModelValue(model: ModelLike): string {
|
|
80
|
-
return `${model.provider}/${model.id}`;
|
|
39
|
+
/**
|
|
40
|
+
* Pi resolves `--models` and `enabledModels` against the available catalogue and exposes the
|
|
41
|
+
* result as `ctx.scopedModels`; an empty scope means every available model is allowed.
|
|
42
|
+
*/
|
|
43
|
+
export function isModelInScope(model: ModelLike, scopedModels: ReadonlyArray<{ model: ModelLike }>): boolean {
|
|
44
|
+
return scopedModels.length === 0 || scopedModels.some(scoped => scoped.model.provider === model.provider && scoped.model.id === model.id);
|
|
81
45
|
}
|
|
82
46
|
|
|
83
47
|
export function splitThinkingSuffix(value: string): { value: string; thinkingLevel?: SummaryThinkingLevel } {
|
|
@@ -88,38 +52,3 @@ export function splitThinkingSuffix(value: string): { value: string; thinkingLev
|
|
|
88
52
|
? { value: value.slice(0, index), thinkingLevel: suffix as SummaryThinkingLevel }
|
|
89
53
|
: { value };
|
|
90
54
|
}
|
|
91
|
-
|
|
92
|
-
function stripThinkingSuffix(pattern: string): string {
|
|
93
|
-
return splitThinkingSuffix(pattern).value;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function globToRegExp(pattern: string): RegExp {
|
|
97
|
-
let source = "^";
|
|
98
|
-
for (const char of pattern) {
|
|
99
|
-
if (char === "*") {
|
|
100
|
-
source += ".*";
|
|
101
|
-
} else if (char === "?") {
|
|
102
|
-
source += ".";
|
|
103
|
-
} else {
|
|
104
|
-
source += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return new RegExp(`${source}$`, "i");
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function modelMatchesEnabledPatterns(model: ModelLike, patterns: string[] | null): boolean {
|
|
111
|
-
if (patterns === null) return true;
|
|
112
|
-
const value = summaryModelValue(model).toLowerCase();
|
|
113
|
-
const id = model.id.toLowerCase();
|
|
114
|
-
for (const rawPattern of patterns) {
|
|
115
|
-
const pattern = stripThinkingSuffix(rawPattern.trim()).toLowerCase();
|
|
116
|
-
if (!pattern) continue;
|
|
117
|
-
if (pattern.includes("*") || pattern.includes("?")) {
|
|
118
|
-
const regex = globToRegExp(pattern);
|
|
119
|
-
if (regex.test(value) || regex.test(id)) return true;
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
if (pattern === value || pattern === id) return true;
|
|
123
|
-
}
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
@@ -2,7 +2,7 @@ import type { ModelThinkingLevel, ThinkingLevel } from "@earendil-works/pi-ai";
|
|
|
2
2
|
import type { complete, Api, Message, Model } from "@earendil-works/pi-ai/compat";
|
|
3
3
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { openCodeSessionHeaders } from "./opencode-session-headers.ts";
|
|
5
|
-
import { findModelWithProviderRouting,
|
|
5
|
+
import { findModelWithProviderRouting, isModelInScope, splitThinkingSuffix, type SummaryThinkingLevel } from "./summary-model-scope.ts";
|
|
6
6
|
import type { QueryResultData } from "./storage.ts";
|
|
7
7
|
|
|
8
8
|
type ProviderHeaders = Record<string, string | null>;
|
|
@@ -30,7 +30,7 @@ export interface SummaryMeta {
|
|
|
30
30
|
edited?: boolean;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export type SummaryGenerationContext = Pick<ExtensionContext, "model" | "modelRegistry" | "sessionManager" | "
|
|
33
|
+
export type SummaryGenerationContext = Pick<ExtensionContext, "model" | "modelRegistry" | "sessionManager" | "scopedModels">;
|
|
34
34
|
|
|
35
35
|
function estimateTokens(text: string): number {
|
|
36
36
|
const trimmed = text.trim();
|
|
@@ -215,7 +215,6 @@ async function resolveSummaryModelCandidates(
|
|
|
215
215
|
ctx: SummaryGenerationContext,
|
|
216
216
|
modelOverride?: string,
|
|
217
217
|
): Promise<{ candidates: Array<{ model: Model<Api>; apiKey?: string; headers?: ProviderHeaders; thinkingLevel?: SummaryThinkingLevel }>; errors: string[] }> {
|
|
218
|
-
const enabledModelPatterns = loadEnabledModelPatterns(ctx);
|
|
219
218
|
const specs: Array<{ provider: string; id: string; thinkingLevel?: SummaryThinkingLevel }> = [];
|
|
220
219
|
const normalizedOverride = typeof modelOverride === "string" ? modelOverride.trim() : "";
|
|
221
220
|
if (normalizedOverride.length > 0) specs.push(parseModelSelector(normalizedOverride));
|
|
@@ -234,7 +233,7 @@ async function resolveSummaryModelCandidates(
|
|
|
234
233
|
errors.push(`Summary model not found: ${value}`);
|
|
235
234
|
continue;
|
|
236
235
|
}
|
|
237
|
-
if (!
|
|
236
|
+
if (!isModelInScope(model, ctx.scopedModels)) {
|
|
238
237
|
errors.push(`Summary model is not enabled: ${value}`);
|
|
239
238
|
continue;
|
|
240
239
|
}
|