@herbertgao/pi-extensions 2026.8.14 → 2026.8.15
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 +16 -13
- package/THIRD_PARTY_NOTICES.md +0 -1
- package/node_modules/@czottmann/pi-automode/CHANGELOG.md +8 -1
- package/node_modules/@czottmann/pi-automode/README.md +4 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +71 -9
- package/node_modules/@czottmann/pi-automode/package.json +1 -1
- package/node_modules/@herbertgao/pi-bark/LICENSE +21 -0
- package/node_modules/@herbertgao/pi-bark/README.md +44 -0
- package/node_modules/@herbertgao/pi-bark/assets/pi-icon.png +0 -0
- package/node_modules/@herbertgao/pi-bark/package.json +53 -0
- package/node_modules/@herbertgao/pi-bark/src/index.ts +178 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +125 -88
- package/node_modules/@narumitw/pi-btw/dist/index.ts +140 -45
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +2 -2
- package/node_modules/@narumitw/pi-btw/package.json +5 -8
- package/node_modules/@narumitw/pi-btw/src/btw.ts +31 -26
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +106 -17
- package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
- package/node_modules/@narumitw/pi-btw/src/settings.ts +16 -0
- package/node_modules/@tifan/pi-handoff/README.md +36 -13
- package/node_modules/@tifan/pi-handoff/package.json +2 -2
- package/node_modules/@tifan/pi-handoff/src/index.ts +160 -4
- package/node_modules/@tifan/pi-recap/README.md +1 -1
- package/node_modules/@tifan/pi-recap/package.json +2 -2
- package/node_modules/@tifan/pi-recap/src/index.ts +15 -0
- package/node_modules/@tifan/pi-rename/README.md +42 -21
- package/node_modules/@tifan/pi-rename/package.json +1 -1
- package/node_modules/@tifan/pi-rename/src/index.ts +62 -11
- package/node_modules/@tifan/pi-rename/src/language.ts +18 -0
- package/node_modules/@tifan/pi-rename/src/models.ts +69 -13
- package/node_modules/@tifan/pi-rename/src/naming.ts +36 -7
- package/node_modules/@tifan/pi-rename/src/sanitize.ts +22 -14
- package/node_modules/pi-lens/CHANGELOG.md +320 -0
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +79 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +16 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +13 -6
- package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +12 -0
- package/node_modules/pi-lens/dist/clients/biome-client.js +7 -0
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +20 -0
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +56 -19
- package/node_modules/pi-lens/dist/clients/build-identity.js +95 -0
- package/node_modules/pi-lens/dist/clients/bus-publish.js +3 -0
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +2 -1
- package/node_modules/pi-lens/dist/clients/cache-observability.js +2 -1
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +15 -1
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +14 -11
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +56 -4
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +14 -0
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +20 -3
- package/node_modules/pi-lens/dist/clients/deps/minimatch.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +39 -0
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +150 -9
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +231 -9
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +205 -100
- package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +87 -0
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +21 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +174 -55
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +5 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +16 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +54 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +1 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +37 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +11 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +8 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +76 -25
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +19 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +7 -13
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +51 -17
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +66 -1
- package/node_modules/pi-lens/dist/clients/file-utils.js +157 -7
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +5 -0
- package/node_modules/pi-lens/dist/clients/formatters.js +28 -3
- package/node_modules/pi-lens/dist/clients/git-guard.js +197 -47
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +32 -1
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +5 -1
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +7 -0
- package/node_modules/pi-lens/dist/clients/installer/index.js +131 -34
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +3 -1
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +4 -13
- package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +145 -0
- package/node_modules/pi-lens/dist/clients/instance-registry.js +432 -122
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +6 -1
- package/node_modules/pi-lens/dist/clients/language-profile.js +4 -2
- package/node_modules/pi-lens/dist/clients/latency-logger.js +19 -1
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/lens-map.js +3 -1
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +4 -0
- package/node_modules/pi-lens/dist/clients/lsp/client.js +516 -31
- package/node_modules/pi-lens/dist/clients/lsp/config.js +25 -2
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +9 -4
- package/node_modules/pi-lens/dist/clients/lsp/index.js +509 -182
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +5 -0
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +36 -8
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +116 -4
- package/node_modules/pi-lens/dist/clients/lsp/server.js +142 -13
- package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +99 -0
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +16 -6
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +32 -23
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +16 -2
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +12 -6
- package/node_modules/pi-lens/dist/clients/mcp/session.js +14 -1
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +23 -1
- package/node_modules/pi-lens/dist/clients/message-end-attribution.js +30 -0
- package/node_modules/pi-lens/dist/clients/metrics-history.js +21 -4
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +232 -18
- package/node_modules/pi-lens/dist/clients/package-manager.js +6 -18
- package/node_modules/pi-lens/dist/clients/path-keyed-map.js +14 -1
- package/node_modules/pi-lens/dist/clients/path-utils.js +32 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +0 -8
- package/node_modules/pi-lens/dist/clients/process-singletons.js +210 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +10 -4
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +33 -11
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +24 -2
- package/node_modules/pi-lens/dist/clients/read-guard.js +74 -7
- package/node_modules/pi-lens/dist/clients/recent-touches.js +17 -5
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +639 -62
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +5 -1
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +21 -1
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +68 -0
- package/node_modules/pi-lens/dist/clients/runtime-session.js +30 -12
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +15 -0
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +106 -7
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +242 -27
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +158 -18
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +48 -17
- package/node_modules/pi-lens/dist/clients/session-lifecycle.js +238 -42
- package/node_modules/pi-lens/dist/clients/sg-runner.js +27 -15
- package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +338 -0
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +37 -0
- package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +15 -1
- package/node_modules/pi-lens/dist/clients/startup-scan.js +6 -7
- package/node_modules/pi-lens/dist/clients/startup-timing.js +69 -4
- package/node_modules/pi-lens/dist/clients/string-utils.js +20 -0
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +419 -88
- package/node_modules/pi-lens/dist/clients/tool-policy.js +1 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +11 -5
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +17 -1
- package/node_modules/pi-lens/dist/clients/warm-attach.js +8 -10
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +13 -1
- package/node_modules/pi-lens/dist/clients/word-index-store.js +448 -0
- package/node_modules/pi-lens/dist/clients/word-index.js +582 -104
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +17 -0
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -6
- package/node_modules/pi-lens/dist/index.js +11243 -6651
- package/node_modules/pi-lens/dist/mcp/build-staleness.js +8 -7
- package/node_modules/pi-lens/dist/mcp/cli.js +3 -1
- package/node_modules/pi-lens/dist/mcp/server.js +6 -1
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +4 -0
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +14 -5
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +3 -1
- package/node_modules/pi-lens/docs/agent-guide.md +4 -2
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +41 -10
- package/node_modules/pi-lens/docs/astplayground.md +26 -17
- package/node_modules/pi-lens/package.json +5 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-important.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +15 -0
- package/node_modules/pi-mcp-adapter/README.md +2 -2
- package/node_modules/pi-mcp-adapter/commands.ts +1 -1
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +1 -1
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +105 -32
- package/node_modules/pi-mcp-adapter/package.json +1 -1
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +22 -12
- package/node_modules/pi-mcp-adapter/server-manager.ts +6 -0
- package/node_modules/pi-mcp-adapter/types.ts +1 -1
- package/node_modules/pi-web-access/CHANGELOG.md +20 -0
- package/node_modules/pi-web-access/README.md +15 -2
- package/node_modules/pi-web-access/extract.ts +57 -12
- package/node_modules/pi-web-access/github-extract.ts +47 -3
- package/node_modules/pi-web-access/index.ts +1 -1
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/page-query.ts +61 -8
- package/package.json +16 -16
- package/node_modules/@tifan/pi-titlebar-spinner/README.md +0 -24
- package/node_modules/@tifan/pi-titlebar-spinner/package.json +0 -39
- package/node_modules/@tifan/pi-titlebar-spinner/src/index.ts +0 -49
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.31.0] - 2026-08-28
|
|
11
|
+
|
|
12
|
+
### Highlights
|
|
13
|
+
- MCP Apps-aware servers can now recognize Pi as a UI-capable host and expose interactive resources.
|
|
14
|
+
- UI capability advertising works consistently across legacy and modern MCP protocol negotiation.
|
|
15
|
+
- Manual OAuth callback completion now supports HTTPS redirect URLs for pre-registered clients.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Pi now advertises MCP Apps UI support with the `io.modelcontextprotocol/ui` extension capability, so compatible servers can expose UI resources. Thanks to [@VikashLoomba](https://github.com/VikashLoomba) for #465.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Pre-registered OAuth clients can use HTTPS callback URLs through manual callback completion instead of being rejected as non-local redirects. Thanks to [@jluisrojas](https://github.com/jluisrojas) for PR #464.
|
|
24
|
+
|
|
10
25
|
## [2.30.0] - 2026-08-28
|
|
11
26
|
|
|
12
27
|
### Highlights
|
|
@@ -272,7 +272,7 @@ In the configuration examples below, `30000` is illustrative only. If `requestTi
|
|
|
272
272
|
| `oauth.clientId` | Pre-registered OAuth client ID. MCP 2026 prefers pre-registered clients or Client ID Metadata Documents; this adapter falls back to Dynamic Client Registration when the ID is omitted and the server supports it. |
|
|
273
273
|
| `oauth.clientSecret` | OAuth client secret for confidential clients; a value beginning with `!` runs a command when OAuth authenticates, while `!!` escapes a literal leading `!` |
|
|
274
274
|
| `oauth.scope` | Requested OAuth scopes |
|
|
275
|
-
| `oauth.redirectUri` | Exact
|
|
275
|
+
| `oauth.redirectUri` | Exact redirect URI for browser OAuth. Local `http://` loopback URIs need an explicit port. Pre-registered `https://` callbacks use manual completion by pasting the full callback URL. |
|
|
276
276
|
| `oauth.clientName` | Client display name advertised during Dynamic Client Registration fallback |
|
|
277
277
|
| `oauth.clientUri` | Client homepage URI advertised during Dynamic Client Registration fallback. Defaults to `piConfig.clientUri` from the host's manifest when set, and is omitted rather than guessed under a rebranded host |
|
|
278
278
|
| `oauth.logoUri` | Client logo URL advertised during Dynamic Client Registration fallback (RFC 7591 `logo_uri`). Must be an absolute `http(s)` URL — consent screens fetch it server-side, so local paths render nothing. Omitted from the registration request when unset |
|
|
@@ -304,7 +304,7 @@ Use `"2026-07-28"` to pin that revision. Pinning has no legacy or SSE fallback a
|
|
|
304
304
|
|
|
305
305
|
The stable SDK handles era-specific request envelopes, result decoding, list-changed subscriptions, cancellation, and multi-round-trip sampling/elicitation. The adapter keeps strict OAuth issuer validation in every mode. Adapter-level roots support, standard MCP logging presentation, and configuration/UI for protocol cache hints are not yet implemented.
|
|
306
306
|
|
|
307
|
-
For pre-registered browser OAuth clients, set `oauth.redirectUri` to the exact callback registered with the provider, for example `"http://localhost:3118/callback"`. Dynamic clients normally omit it and use a lazy OS-assigned localhost callback port.
|
|
307
|
+
For pre-registered browser OAuth clients, set `oauth.redirectUri` to the exact callback registered with the provider, for example `"http://localhost:3118/callback"`. Dynamic clients normally omit it and use a lazy OS-assigned localhost callback port. A configured `https://` callback runs in manual mode because the adapter cannot receive a callback on another host. After authorization, copy the full callback URL from the browser address bar and paste it into `/mcp-auth` or `mcp({ action: "auth-complete", ... })`.
|
|
308
308
|
|
|
309
309
|
If an internal authorization server publishes mismatched OAuth metadata and cannot be fixed immediately, set `oauth.skipIssuerMetadataValidation: true` on that server only. This is security-weakening. It disables the RFC 8414 issuer echo check and should not be used for public or untrusted servers.
|
|
310
310
|
|
|
@@ -297,7 +297,7 @@ export async function authenticateServer(
|
|
|
297
297
|
return ui.input(
|
|
298
298
|
`Complete ${serverName} OAuth\n\n` +
|
|
299
299
|
`${terminalHyperlink("Open authorization page", authorizationUrl)}\n${authorizationUrl}\n\n` +
|
|
300
|
-
"Approve access, then paste the full
|
|
300
|
+
"Approve access, then paste the full callback URL from the browser address bar below.",
|
|
301
301
|
undefined,
|
|
302
302
|
{ signal: inputSignal },
|
|
303
303
|
);
|
|
@@ -228,7 +228,7 @@ export interface OAuthConfig {
|
|
|
228
228
|
scope?: string;
|
|
229
229
|
/** Extra authorization URL parameters for provider-specific extensions. Flow-owned parameters cannot be overridden. */
|
|
230
230
|
authorizationParams?: Record<string, string>;
|
|
231
|
-
/** Exact authorization-code redirect URI for pre-registered clients */
|
|
231
|
+
/** Exact authorization-code redirect URI for pre-registered clients. HTTPS redirects use manual callback URL completion. */
|
|
232
232
|
redirectUri?: string;
|
|
233
233
|
/** Client display name for dynamic registration */
|
|
234
234
|
clientName?: string;
|
|
@@ -75,6 +75,8 @@ type PendingAuth = {
|
|
|
75
75
|
authProvider: McpOAuthProvider
|
|
76
76
|
serverUrl: string
|
|
77
77
|
authorizationUrl: string
|
|
78
|
+
manualRedirect: boolean
|
|
79
|
+
manualCompletionController?: AbortController
|
|
78
80
|
discovery: AuthDiscovery
|
|
79
81
|
authStorageOptions: AuthStorageOptions
|
|
80
82
|
}
|
|
@@ -307,7 +309,11 @@ async function probeAuthDiscovery(serverUrl: string, definition?: ServerEntry, s
|
|
|
307
309
|
}
|
|
308
310
|
}
|
|
309
311
|
|
|
310
|
-
|
|
312
|
+
type OAuthRedirectTarget =
|
|
313
|
+
| { mode: "local"; port: number; callbackHost: string; callbackPath: string }
|
|
314
|
+
| { mode: "manual" }
|
|
315
|
+
|
|
316
|
+
function parseOAuthRedirectUri(redirectUri: string): OAuthRedirectTarget {
|
|
311
317
|
let url: URL
|
|
312
318
|
try {
|
|
313
319
|
url = new URL(redirectUri)
|
|
@@ -315,12 +321,6 @@ function parseOAuthRedirectUri(redirectUri: string): { port: number; callbackHos
|
|
|
315
321
|
throw new Error(`Invalid OAuth redirectUri: ${redirectUri}`, { cause: error })
|
|
316
322
|
}
|
|
317
323
|
|
|
318
|
-
const hostname = url.hostname.toLowerCase()
|
|
319
|
-
const isLocalhost = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1"
|
|
320
|
-
if (url.protocol !== "http:" || !isLocalhost) {
|
|
321
|
-
throw new Error("OAuth redirectUri must be an http:// localhost or loopback URI")
|
|
322
|
-
}
|
|
323
|
-
|
|
324
324
|
if (url.username || url.password) {
|
|
325
325
|
throw new Error("OAuth redirectUri must not include username or password")
|
|
326
326
|
}
|
|
@@ -329,17 +329,32 @@ function parseOAuthRedirectUri(redirectUri: string): { port: number; callbackHos
|
|
|
329
329
|
throw new Error("OAuth redirectUri must not include a fragment")
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
+
const hostname = url.hostname.toLowerCase()
|
|
333
|
+
const isLocalhost = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1"
|
|
334
|
+
if (url.port) {
|
|
335
|
+
const parsedPort = Number.parseInt(url.port, 10)
|
|
336
|
+
if (!Number.isInteger(parsedPort) || parsedPort <= 0 || parsedPort > 65535) {
|
|
337
|
+
throw new Error("OAuth redirectUri port must be a positive numeric port")
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (url.protocol === "https:" && !isLocalhost) {
|
|
341
|
+
return { mode: "manual" }
|
|
342
|
+
}
|
|
343
|
+
if (url.protocol !== "http:" || !isLocalhost) {
|
|
344
|
+
throw new Error("OAuth redirectUri must be an https:// URI or an http:// localhost or loopback URI")
|
|
345
|
+
}
|
|
346
|
+
|
|
332
347
|
if (!url.port) {
|
|
333
|
-
throw new Error("OAuth redirectUri must include an explicit numeric port")
|
|
348
|
+
throw new Error("OAuth localhost redirectUri must include an explicit numeric port")
|
|
334
349
|
}
|
|
335
350
|
|
|
336
351
|
const port = Number.parseInt(url.port, 10)
|
|
337
352
|
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
338
|
-
throw new Error("OAuth redirectUri must include an explicit numeric port")
|
|
353
|
+
throw new Error("OAuth localhost redirectUri must include an explicit numeric port")
|
|
339
354
|
}
|
|
340
355
|
|
|
341
356
|
const callbackHost = hostname === "[::1]" ? "::1" : hostname
|
|
342
|
-
return { port, callbackHost, callbackPath: url.pathname }
|
|
357
|
+
return { mode: "local", port, callbackHost, callbackPath: url.pathname }
|
|
343
358
|
}
|
|
344
359
|
|
|
345
360
|
/**
|
|
@@ -393,25 +408,30 @@ export async function startAuth(
|
|
|
393
408
|
return { authorizationUrl: existingPendingAuth.authorizationUrl }
|
|
394
409
|
}
|
|
395
410
|
|
|
396
|
-
const
|
|
411
|
+
const redirectTarget = config.redirectUri !== undefined ? parseOAuthRedirectUri(config.redirectUri) : undefined
|
|
412
|
+
const manualRedirect = redirectTarget?.mode === "manual"
|
|
397
413
|
const oauthState = generateState()
|
|
398
414
|
|
|
399
|
-
|
|
400
|
-
await ensureCallbackServer({
|
|
401
|
-
strictPort: Boolean(config.clientId) || config.redirectUri !== undefined,
|
|
402
|
-
oauthState,
|
|
403
|
-
reserveState: true,
|
|
404
|
-
...(redirectCallback ? { port: redirectCallback.port, callbackHost: redirectCallback.callbackHost, callbackPath: redirectCallback.callbackPath } : {}),
|
|
405
|
-
})
|
|
406
|
-
throwIfAborted(signal)
|
|
407
|
-
} catch (error) {
|
|
408
|
-
releaseCallbackServer(oauthState)
|
|
415
|
+
if (!manualRedirect) {
|
|
409
416
|
try {
|
|
410
|
-
await
|
|
411
|
-
|
|
412
|
-
|
|
417
|
+
await ensureCallbackServer({
|
|
418
|
+
strictPort: Boolean(config.clientId) || config.redirectUri !== undefined,
|
|
419
|
+
oauthState,
|
|
420
|
+
reserveState: true,
|
|
421
|
+
...(redirectTarget?.mode === "local"
|
|
422
|
+
? { port: redirectTarget.port, callbackHost: redirectTarget.callbackHost, callbackPath: redirectTarget.callbackPath }
|
|
423
|
+
: {}),
|
|
424
|
+
})
|
|
425
|
+
throwIfAborted(signal)
|
|
426
|
+
} catch (error) {
|
|
427
|
+
releaseCallbackServer(oauthState)
|
|
428
|
+
try {
|
|
429
|
+
await cleanupAndReleaseCallbackServerIfIdle(() => clearOAuthState(serverName, authStorageOptions))
|
|
430
|
+
} catch (cleanupError) {
|
|
431
|
+
throw new AggregateError([error, cleanupError], "OAuth startup cleanup failed")
|
|
432
|
+
}
|
|
433
|
+
throw error
|
|
413
434
|
}
|
|
414
|
-
throw error
|
|
415
435
|
}
|
|
416
436
|
|
|
417
437
|
let capturedUrl: URL | undefined
|
|
@@ -455,7 +475,16 @@ export async function startAuth(
|
|
|
455
475
|
if (!capturedUrl) {
|
|
456
476
|
throw new UnauthorizedError("OAuth authorization URL was not provided")
|
|
457
477
|
}
|
|
458
|
-
await setPendingAuth(runtime, serverName, {
|
|
478
|
+
await setPendingAuth(runtime, serverName, {
|
|
479
|
+
serverName,
|
|
480
|
+
authProvider,
|
|
481
|
+
serverUrl,
|
|
482
|
+
authorizationUrl: capturedUrl.toString(),
|
|
483
|
+
manualRedirect,
|
|
484
|
+
...(manualRedirect ? { manualCompletionController: new AbortController() } : {}),
|
|
485
|
+
discovery,
|
|
486
|
+
authStorageOptions,
|
|
487
|
+
}, oauthState, signal, generation)
|
|
459
488
|
return { authorizationUrl: capturedUrl.toString() }
|
|
460
489
|
} catch (error) {
|
|
461
490
|
authProvider.deactivate()
|
|
@@ -484,7 +513,13 @@ async function setPendingAuth(
|
|
|
484
513
|
state.pendingAuths.set(key, pendingAuth)
|
|
485
514
|
state.pendingAuthStates.set(key, oauthState)
|
|
486
515
|
const cleanupTimer = setTimeout(() => {
|
|
487
|
-
void clearPendingAuthAndReleaseIfIdle(
|
|
516
|
+
void clearPendingAuthAndReleaseIfIdle(
|
|
517
|
+
runtime,
|
|
518
|
+
serverName,
|
|
519
|
+
oauthState,
|
|
520
|
+
pendingAuth.authStorageOptions,
|
|
521
|
+
new Error("OAuth authorization timeout - authorization took too long"),
|
|
522
|
+
).catch(error => {
|
|
488
523
|
console.error(`MCP Auth: Timed-out flow cleanup failed: ${formatTerminalError(error)}`)
|
|
489
524
|
})
|
|
490
525
|
}, MANUAL_AUTH_TIMEOUT_MS)
|
|
@@ -492,7 +527,13 @@ async function setPendingAuth(
|
|
|
492
527
|
state.pendingAuthCleanupTimers.set(key, cleanupTimer)
|
|
493
528
|
}
|
|
494
529
|
|
|
495
|
-
async function clearPendingAuth(
|
|
530
|
+
async function clearPendingAuth(
|
|
531
|
+
runtime: McpOAuthRuntime,
|
|
532
|
+
serverName: string,
|
|
533
|
+
oauthState?: string,
|
|
534
|
+
fallbackStorageOptions: AuthStorageOptions = {},
|
|
535
|
+
reason: Error = new Error("Authorization cancelled"),
|
|
536
|
+
): Promise<void> {
|
|
496
537
|
const state = getRuntimeState(runtime)
|
|
497
538
|
const key = getPendingAuthKey(serverName, fallbackStorageOptions)
|
|
498
539
|
const pendingAuth = state.pendingAuths.get(key)
|
|
@@ -506,6 +547,7 @@ async function clearPendingAuth(runtime: McpOAuthRuntime, serverName: string, oa
|
|
|
506
547
|
state.pendingAuthCleanupTimers.delete(key)
|
|
507
548
|
}
|
|
508
549
|
|
|
550
|
+
pendingAuth?.manualCompletionController?.abort(reason)
|
|
509
551
|
pendingAuth?.authProvider.deactivate()
|
|
510
552
|
state.pendingAuths.delete(key)
|
|
511
553
|
state.pendingAuthStates.delete(key)
|
|
@@ -524,9 +566,10 @@ async function clearPendingAuthAndReleaseIfIdle(
|
|
|
524
566
|
serverName: string,
|
|
525
567
|
oauthState: string | undefined,
|
|
526
568
|
fallbackStorageOptions: AuthStorageOptions = {},
|
|
569
|
+
reason?: Error,
|
|
527
570
|
): Promise<void> {
|
|
528
571
|
await cleanupAndReleaseCallbackServerIfIdle(
|
|
529
|
-
() => clearPendingAuth(runtime, serverName, oauthState, fallbackStorageOptions),
|
|
572
|
+
() => clearPendingAuth(runtime, serverName, oauthState, fallbackStorageOptions, reason),
|
|
530
573
|
)
|
|
531
574
|
}
|
|
532
575
|
|
|
@@ -629,6 +672,19 @@ type AuthorizationResponse = {
|
|
|
629
672
|
source: "callback" | "manual"
|
|
630
673
|
}
|
|
631
674
|
|
|
675
|
+
function waitForManualCompletionCancellation(signal: AbortSignal): Promise<AuthorizationCodeInput> {
|
|
676
|
+
return new Promise((_, reject) => {
|
|
677
|
+
const rejectFromSignal = () => {
|
|
678
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error("Authorization cancelled"))
|
|
679
|
+
}
|
|
680
|
+
if (signal.aborted) {
|
|
681
|
+
rejectFromSignal()
|
|
682
|
+
return
|
|
683
|
+
}
|
|
684
|
+
signal.addEventListener("abort", rejectFromSignal, { once: true })
|
|
685
|
+
})
|
|
686
|
+
}
|
|
687
|
+
|
|
632
688
|
/**
|
|
633
689
|
* Wait for either the localhost callback or a manually pasted redirect URL.
|
|
634
690
|
* The manual input prompt is dismissed as soon as either path finishes.
|
|
@@ -686,7 +742,11 @@ export async function completeAuthFromInput(
|
|
|
686
742
|
throwIfAborted(signal)
|
|
687
743
|
const key = getPendingAuthKey(serverName, fallbackAuthStorageOptions)
|
|
688
744
|
const oauthState = runtimeState.pendingAuthStates.get(key)
|
|
745
|
+
const pendingAuth = runtimeState.pendingAuths.get(key)
|
|
689
746
|
throwIfAborted(signal)
|
|
747
|
+
if (pendingAuth?.manualRedirect && !getSearchParamsFromInput(input.trim())) {
|
|
748
|
+
throw new Error("Paste the full OAuth callback URL, including its code and state parameters")
|
|
749
|
+
}
|
|
690
750
|
const parsed = parseAuthorizationRedirectInput(input, oauthState)
|
|
691
751
|
return completeAuth(serverName, parsed, options)
|
|
692
752
|
}
|
|
@@ -805,15 +865,28 @@ export async function authenticate(
|
|
|
805
865
|
try {
|
|
806
866
|
// Get the state that was already generated and stored in startAuth().
|
|
807
867
|
// Keep this lookup and its abort check inside the cleanup boundary because
|
|
808
|
-
// startAuth
|
|
868
|
+
// startAuth already owns the pending flow at this point.
|
|
809
869
|
oauthState = runtimeState.pendingAuthStates.get(getPendingAuthKey(serverName, authStorageOptions))
|
|
810
870
|
throwIfAborted(signal)
|
|
811
871
|
if (!oauthState) {
|
|
812
872
|
throw new Error("OAuth state not found - this should not happen")
|
|
813
873
|
}
|
|
814
874
|
|
|
815
|
-
|
|
816
|
-
|
|
875
|
+
const pendingAuth = runtimeState.pendingAuths.get(getPendingAuthKey(serverName, authStorageOptions))
|
|
876
|
+
if (!pendingAuth) {
|
|
877
|
+
throw new Error(`No pending OAuth flow for server: ${serverName}`)
|
|
878
|
+
}
|
|
879
|
+
if (pendingAuth.manualRedirect && !options.onAuthorizationInput) {
|
|
880
|
+
throw new Error(
|
|
881
|
+
`OAuth for ${serverName} uses a remote redirect URI. Complete it with auth-start/auth-complete or /mcp-auth.`,
|
|
882
|
+
)
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Register the localhost callback before opening the browser. Remote
|
|
886
|
+
// pre-registered callbacks are completed by pasting their full URL.
|
|
887
|
+
const callbackPromise: Promise<AuthorizationCodeInput> = pendingAuth.manualRedirect
|
|
888
|
+
? waitForManualCompletionCancellation(pendingAuth.manualCompletionController!.signal)
|
|
889
|
+
: waitForCallback(oauthState)
|
|
817
890
|
void callbackPromise.catch(() => {})
|
|
818
891
|
|
|
819
892
|
// Open browser. Always surface the URL first so remote/headless users can copy it
|
|
@@ -143,22 +143,30 @@ function getAuthFailedMessage(state: McpExtensionState, serverName: string, mess
|
|
|
143
143
|
return `OAuth authentication failed for "${serverName}": ${message}. Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
function
|
|
146
|
+
function getRedirectDetails(authorizationUrl: string): { port?: number; remote: boolean } {
|
|
147
147
|
try {
|
|
148
148
|
const redirectUri = new URL(authorizationUrl).searchParams.get("redirect_uri");
|
|
149
|
-
if (!redirectUri) return
|
|
150
|
-
const
|
|
151
|
-
|
|
149
|
+
if (!redirectUri) return { remote: false };
|
|
150
|
+
const redirect = new URL(redirectUri);
|
|
151
|
+
const hostname = redirect.hostname.toLowerCase();
|
|
152
|
+
const local = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1";
|
|
153
|
+
const port = Number.parseInt(redirect.port, 10);
|
|
154
|
+
return {
|
|
155
|
+
...(Number.isInteger(port) ? { port } : {}),
|
|
156
|
+
remote: !local,
|
|
157
|
+
};
|
|
152
158
|
} catch {
|
|
153
|
-
return
|
|
159
|
+
return { remote: false };
|
|
154
160
|
}
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
function formatManualAuthInstructions(serverName: string, authorizationUrl: string): string {
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
?
|
|
161
|
-
:
|
|
164
|
+
const redirect = getRedirectDetails(authorizationUrl);
|
|
165
|
+
const redirectNote = redirect.remote
|
|
166
|
+
? "The provider uses a pre-registered HTTPS callback. Copy its full URL from the browser address bar, even if the destination page reports an error."
|
|
167
|
+
: redirect.port
|
|
168
|
+
? `The redirect URL will use local port ${redirect.port}. On a remote server it is expected for that localhost page to fail locally; copy the address bar URL anyway.`
|
|
169
|
+
: "";
|
|
162
170
|
|
|
163
171
|
return [
|
|
164
172
|
`MCP OAuth required for "${serverName}".`,
|
|
@@ -167,11 +175,13 @@ function formatManualAuthInstructions(serverName: string, authorizationUrl: stri
|
|
|
167
175
|
"",
|
|
168
176
|
authorizationUrl,
|
|
169
177
|
"",
|
|
170
|
-
"After approving, copy the full
|
|
178
|
+
"After approving, copy the full callback URL from your browser address bar and send it back with:",
|
|
171
179
|
`mcp({ action: "auth-complete", server: "${serverName}", args: { redirectUrl: "PASTE_REDIRECT_URL_HERE" } })`,
|
|
172
180
|
"",
|
|
173
|
-
|
|
174
|
-
|
|
181
|
+
redirect.remote
|
|
182
|
+
? "Remote HTTPS callbacks must include the full callback URL so the OAuth state can be checked. JSON-string args remain supported."
|
|
183
|
+
: 'You can also pass just the `code` query parameter as `args: { code: "PASTE_CODE_HERE" }`. JSON-string args remain supported.',
|
|
184
|
+
redirectNote,
|
|
175
185
|
].filter(Boolean).join("\n");
|
|
176
186
|
}
|
|
177
187
|
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import { resolveNpxBinary } from "./npx-resolver.ts";
|
|
35
35
|
import { createJsonSchemaValidator } from "./json-schema-validator.ts";
|
|
36
36
|
import { logger } from "./logger.ts";
|
|
37
|
+
import { RESOURCE_MIME_TYPE } from "./ui-app-bridge-helpers.ts";
|
|
37
38
|
import { McpOAuthProvider } from "./mcp-oauth-provider.ts";
|
|
38
39
|
import { extractOAuthConfig, supportsOAuth, type McpOAuthRuntime } from "./mcp-auth-flow.ts";
|
|
39
40
|
import { invalidateAuthEntryCache, type AuthStorageOptions } from "./mcp-auth.ts";
|
|
@@ -725,6 +726,11 @@ export class McpServerManager {
|
|
|
725
726
|
|
|
726
727
|
private buildClientCapabilities() {
|
|
727
728
|
return {
|
|
729
|
+
extensions: {
|
|
730
|
+
"io.modelcontextprotocol/ui": {
|
|
731
|
+
mimeTypes: [RESOURCE_MIME_TYPE],
|
|
732
|
+
},
|
|
733
|
+
},
|
|
728
734
|
...(this.samplingConfig ? { sampling: {} } : {}),
|
|
729
735
|
...(this.elicitationConfig
|
|
730
736
|
? {
|
|
@@ -378,7 +378,7 @@ export interface OAuthConfig {
|
|
|
378
378
|
scope?: string;
|
|
379
379
|
/** Extra authorization URL parameters for provider-specific extensions. Flow-owned parameters cannot be overridden. */
|
|
380
380
|
authorizationParams?: Record<string, string>;
|
|
381
|
-
/** Exact authorization-code redirect URI for pre-registered clients */
|
|
381
|
+
/** Exact authorization-code redirect URI for pre-registered clients. HTTPS redirects use manual callback URL completion. */
|
|
382
382
|
redirectUri?: string;
|
|
383
383
|
/** Client display name for dynamic registration */
|
|
384
384
|
clientName?: string;
|
|
@@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.27.0] - 2026-08-28
|
|
8
|
+
|
|
9
|
+
### Highlights
|
|
10
|
+
|
|
11
|
+
- `fetch_content` now lets you tune direct HTTP and Jina Reader timeouts from config.
|
|
12
|
+
- Answer mode can use a configured default model while still allowing per-call overrides.
|
|
13
|
+
- HTML fallback parsing is quieter for pages with relative canonical links.
|
|
14
|
+
- GitHub repository fetching is safer when more than one Pi process is running.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Added opt-in `fetch.timeout` configuration in seconds for the direct HTTP and Jina Reader `fetch_content` paths, with per-call timeout overrides taking precedence. Thanks to [@linuxtextadventurer](https://github.com/linuxtextadventurer) for PR #327.
|
|
19
|
+
- Added opt-in `fetch.answerProvider` and `fetch.answerModel` defaults for `fetch_content` answer mode, with per-call `answerModel` overrides taking precedence. Thanks to [@linuxtextadventurer](https://github.com/linuxtextadventurer) for PR #328.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Set the Defuddle fallback document URL before parsing pages with relative canonical links, preventing `ERR_INVALID_URL` warnings. Thanks to [@bin115885](https://github.com/bin115885) for issue #322.
|
|
24
|
+
- Isolated GitHub clone workdirs per extension runtime so cleanup in one process cannot delete another process's clone. Thanks to [@MDGChamomile](https://github.com/MDGChamomile) for PR #323.
|
|
25
|
+
|
|
26
|
+
|
|
7
27
|
## [0.26.0] - 2026-08-28
|
|
8
28
|
|
|
9
29
|
### Highlights
|
|
@@ -159,11 +159,15 @@ fetch_content({ url: "https://example.com/diagram.png" })
|
|
|
159
159
|
| `url` / `urls` | Single URL/path or multiple URLs |
|
|
160
160
|
| `prompt` | Question for video analysis, or the page-local question required by `mode: "answer"` |
|
|
161
161
|
| `mode` | `readable` (default), `raw` for exact textual HTTP bodies, or `answer` for a grounded answer from fetched content |
|
|
162
|
-
| `answerModel` | Optional `provider/model-id` override for answer mode; defaults to the current enabled Pi model |
|
|
162
|
+
| `answerModel` | Optional `provider/model-id` override for answer mode; defaults to the configured `fetch.answerProvider` + `fetch.answerModel` pair, or the current enabled Pi model when no pair is configured |
|
|
163
163
|
| `timestamp` | Extract frame(s) — single (`"23:41"`), range (`"23:41-25:00"`), or seconds (`"85"`) |
|
|
164
164
|
| `frames` | Number of frames to extract (max 12) |
|
|
165
165
|
| `forceClone` | Clone GitHub repos that exceed the 350MB size threshold |
|
|
166
166
|
|
|
167
|
+
For a standing answer-mode model, set both `fetch.answerProvider` and `fetch.answerModel` in `web-search.json`; a per-call `answerModel` takes precedence. Configured answer defaults are opt-in and can send fetched page text to a different provider/model, which may change privacy and cost behavior.
|
|
168
|
+
|
|
169
|
+
Thanks to [@linuxtextadventurer](https://github.com/linuxtextadventurer) for PR #328.
|
|
170
|
+
|
|
167
171
|
### get_search_content
|
|
168
172
|
|
|
169
173
|
Retrieve stored content from previous searches or fetches. Fetched URL content is stored in full in a private `web-search-cache` directory under the Pi config directory, not in the session JSONL. This includes `fetch_content` answer mode, which stores the original page content. The cache has a one-hour lifetime and fixed limits of 128 entries and 128 MiB; when either limit is reached, the oldest entries are removed first. On macOS and Linux the cache directory and files are kept at permissions `0700` and `0600`, respectively. Use `findText` to locate bounded matching passages without paging through a large page, or use `offset` and `limit` to retrieve slices intentionally.
|
|
@@ -403,6 +407,11 @@ Config defaults to `~/.pi/web-search.json`, or `web-search.json` under `PI_CODIN
|
|
|
403
407
|
"providers": ["http", "firecrawl", "jina", "tinyfish", "search1api", "querit", "kagi", "ollama", "parallel", "brightdata", "gemini"],
|
|
404
408
|
"allowRemoteHostedProviders": false
|
|
405
409
|
},
|
|
410
|
+
"fetch": {
|
|
411
|
+
"timeout": 30,
|
|
412
|
+
"answerProvider": "openai",
|
|
413
|
+
"answerModel": "gpt-5.6"
|
|
414
|
+
},
|
|
406
415
|
"webSearch": {
|
|
407
416
|
"enabled": true
|
|
408
417
|
},
|
|
@@ -502,6 +511,10 @@ Set `braveBaseUrl`, `exaBaseUrl`, or `tavilyBaseUrl` to route those providers th
|
|
|
502
511
|
|
|
503
512
|
`fetchContent.domainPolicy` is an optional hostname allow/deny policy for `fetch_content` target URLs. It is off when omitted. Each bare hostname matches itself and its subdomains; `deny` wins when a hostname matches both lists. The policy is checked before HTTP(S) target handling and before each redirect followed by this extension's own fetch path. Local file paths and non-HTTP sources are not subject to this policy. It is an additional restriction: the existing SSRF guard still blocks private and internal destinations. Remote extraction services can still perform their own DNS, redirects, and egress after this extension preflights the submitted target URL, so third-party hosted HTTP(S) fallbacks stay disabled unless `fetchRouting.allowRemoteHostedProviders` is enabled for separately isolated provider deployments.
|
|
504
513
|
|
|
514
|
+
`fetch.timeout` is an optional positive finite number of seconds for direct HTTP fetches and the Jina Reader fallback. When omitted, both use a 30-second budget. Fractional values are supported and rounded up to at least 1 millisecond; values that cannot be converted to a finite safe integer delay from 1 through Node's 2,147,483,647 ms timer maximum are rejected. An invalid declared value fails closed with an error naming `web-search.json`. An internal/per-call `timeoutMs` override takes precedence over this setting. Other remote extraction fallbacks keep their own documented budgets.
|
|
515
|
+
|
|
516
|
+
`fetch.answerProvider` and `fetch.answerModel` are an optional pair that selects the model used by `fetch_content` answer mode when no per-call `answerModel` is supplied. Both values must be non-empty strings and must identify an enabled text-capable model available in Pi's model registry; invalid or partial configuration fails closed. This is opt-in: answering with the configured provider/model can send fetched page text outside the current session and may incur that provider's costs. A per-call `answerModel` override is resolved first and remains usable even when these configured defaults are malformed.
|
|
517
|
+
|
|
505
518
|
Set `searxngBaseUrl` or `SEARXNG_BASE_URL` to use a self-hosted SearXNG JSON API. A configured endpoint is preferred first in `auto` mode for local/private search. Its base URL and redirects remain subject to the SSRF guard; add only the narrowest self-hosted range to `ssrf.allowRanges` when it resolves to a private or synthetic range. Optional `searxngHeaders` merges extra HTTP headers into each SearXNG request (string values only; invalid header names are ignored), which is useful for reverse-proxy or Zero Trust auth such as Cloudflare Access service tokens (`CF-Access-Client-Id` / `CF-Access-Client-Secret`). Configured headers override the default `Accept: application/json` when the same name is supplied. Thanks to Marcos A. Núñez (@marnunez) for PR #107 and Avinash Kanaujiya (@avinashkanaujiya) for issue #105.
|
|
506
519
|
|
|
507
520
|
**DuckDuckGo.** DuckDuckGo HTML search is keyless and explicit-only. Select it with `provider: "duckduckgo"` or place it in `searchRouting`; it is never chosen by `auto` and never participates in `provider: "all"`. Domain filters are enforced locally after DuckDuckGo redirect URLs are decoded. `recencyFilter` is not guaranteed because the HTML endpoint has no documented stable time parameter. A 200 page with no parseable results is reported as an invalid response, so routing can continue when `fallbackOn` includes `"invalid-response"`.
|
|
@@ -868,7 +881,7 @@ Values use the same format as pi keybindings (e.g. `ctrl+s`, `ctrl+shift+s`, `al
|
|
|
868
881
|
|
|
869
882
|
Set `"enabled": false` under `tools`, `commands`, `image`, or `pdf` to disable that feature. Tool-specific settings override the legacy `webSearch.enabled` shorthand; without an override, it still disables `web_search` and `source_check`. `image.enabled: false` blocks direct image fetches and video frame extraction, and prevents video thumbnails. `pdf.enabled: false` blocks PDF extraction. For GitHub specifically, `githubClone.enabled: false` only skips clone/API specialization, and `githubPrIssue.enabled: false` only skips PR/issue specialization; neither setting unregisters `fetch_content` or blocks generic URL extraction. Pi restart is required for tool and command registration changes.
|
|
870
883
|
|
|
871
|
-
Rate limits: Perplexity is capped at 10 requests/minute (client-side). Jina Search, TinyFish, Search1API, and Searchinfinity apply the plan limits documented by their APIs. Querit Search and Contents subscriptions are independent. Content fetches run 3 concurrent
|
|
884
|
+
Rate limits: Perplexity is capped at 10 requests/minute (client-side). Jina Search, TinyFish, Search1API, and Searchinfinity apply the plan limits documented by their APIs. Querit Search and Contents subscriptions are independent. Content fetches run 3 concurrent; direct HTTP fetches and Jina Reader use a 30s timeout by default, configurable together with `fetch.timeout` in seconds. Remote extraction fallbacks carry their own budgets and are not covered by that setting: Firecrawl 60s, Kagi Extract 60s, Ollama Web Fetch 60s, Bright Data Web Unlocker 60s, TinyFish up to 150s, Gemini 120s, Datalab 120s (capped at 300s, rate-limited to 25 requests/minute on the free tier). `pdf.maxSizeMB` defaults to 20 and is capped at 50. `pdf.maxPages` defaults to 100 and limits every PDF provider to the first N pages.
|
|
872
885
|
|
|
873
886
|
## Limitations
|
|
874
887
|
|
|
@@ -31,12 +31,45 @@ import { getBrowserCookiesForHosts, getLastBrowserCookieDiagnostic } from "./chr
|
|
|
31
31
|
import { sanitizeInlineDataUris } from "./data-uri-sanitize.ts";
|
|
32
32
|
|
|
33
33
|
const DEFAULT_TIMEOUT_MS = 30000;
|
|
34
|
+
const MAX_CONFIGURED_TIMEOUT_MS = 2_147_483_647;
|
|
34
35
|
const CONCURRENT_LIMIT = 3;
|
|
36
|
+
const WEB_SEARCH_CONFIG_PATH = getWebSearchConfigPath();
|
|
37
|
+
|
|
38
|
+
function loadFetchTimeoutMs(): number {
|
|
39
|
+
if (!existsSync(WEB_SEARCH_CONFIG_PATH)) return DEFAULT_TIMEOUT_MS;
|
|
40
|
+
|
|
41
|
+
let raw: unknown;
|
|
42
|
+
try {
|
|
43
|
+
raw = JSON.parse(readFileSync(WEB_SEARCH_CONFIG_PATH, "utf-8"));
|
|
44
|
+
} catch (err) {
|
|
45
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
46
|
+
throw new Error(`Failed to parse ${WEB_SEARCH_CONFIG_PATH}: ${message}`);
|
|
47
|
+
}
|
|
48
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
49
|
+
throw new Error(`Invalid config in ${WEB_SEARCH_CONFIG_PATH}: expected a JSON object`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const fetchConfig = (raw as Record<string, unknown>).fetch;
|
|
53
|
+
if (fetchConfig === undefined) return DEFAULT_TIMEOUT_MS;
|
|
54
|
+
if (!fetchConfig || typeof fetchConfig !== "object" || Array.isArray(fetchConfig)) {
|
|
55
|
+
throw new Error(`fetch in ${WEB_SEARCH_CONFIG_PATH} must be an object`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const value = (fetchConfig as Record<string, unknown>).timeout;
|
|
59
|
+
if (value === undefined) return DEFAULT_TIMEOUT_MS;
|
|
60
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
61
|
+
throw new Error(`Invalid fetch.timeout in ${WEB_SEARCH_CONFIG_PATH}: expected a positive finite number of seconds, got ${JSON.stringify(value)}`);
|
|
62
|
+
}
|
|
63
|
+
const timeoutMs = Math.ceil(value * 1000);
|
|
64
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_CONFIGURED_TIMEOUT_MS) {
|
|
65
|
+
throw new Error(`Invalid fetch.timeout in ${WEB_SEARCH_CONFIG_PATH}: converted timeout must be a finite safe integer from 1 through ${MAX_CONFIGURED_TIMEOUT_MS} milliseconds`);
|
|
66
|
+
}
|
|
67
|
+
return Math.max(1, timeoutMs);
|
|
68
|
+
}
|
|
35
69
|
|
|
36
70
|
const NON_RECOVERABLE_ERRORS = ["Unsupported content type", "Response too large", "PDF extraction is disabled", "Image fetching is disabled"];
|
|
37
71
|
const MIN_USEFUL_CONTENT = 500;
|
|
38
72
|
const SUPPORTED_IMAGE_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
39
|
-
const WEB_SEARCH_CONFIG_PATH = getWebSearchConfigPath();
|
|
40
73
|
const FETCH_PROVIDERS = ["http", "firecrawl", "jina", "tinyfish", "search1api", "querit", "kagi", "ollama", "parallel", "parallel-mcp", "brightdata", "gemini"] as const;
|
|
41
74
|
type FetchProvider = typeof FETCH_PROVIDERS[number];
|
|
42
75
|
type FetchRouting = { providers: FetchProvider[]; allowRemoteHostedProviders: boolean };
|
|
@@ -51,6 +84,10 @@ function isDefuddleConsoleError(args: Parameters<typeof console.error>): boolean
|
|
|
51
84
|
async function extractWithDefuddle(text: string, url: string): Promise<{ title: string; content: string } | null> {
|
|
52
85
|
const { Defuddle } = await import("defuddle/node");
|
|
53
86
|
const { document } = parseHTML(text);
|
|
87
|
+
Object.defineProperty(document, "location", {
|
|
88
|
+
value: new URL(url),
|
|
89
|
+
configurable: true,
|
|
90
|
+
});
|
|
54
91
|
let processingError: unknown;
|
|
55
92
|
const originalConsoleError = console.error;
|
|
56
93
|
console.error = (...args) => {
|
|
@@ -298,11 +335,16 @@ export interface ExtractOptions {
|
|
|
298
335
|
lookup?: Lookup;
|
|
299
336
|
}
|
|
300
337
|
|
|
338
|
+
/** Resolve the direct HTTP/Jina fetch budget, with a per-call override taking precedence. */
|
|
339
|
+
export function resolveFetchTimeoutMs(options?: Pick<ExtractOptions, "timeoutMs">): number {
|
|
340
|
+
return options?.timeoutMs ?? loadFetchTimeoutMs();
|
|
341
|
+
}
|
|
342
|
+
|
|
301
343
|
const JINA_READER_BASE = "https://r.jina.ai/";
|
|
302
|
-
const JINA_TIMEOUT_MS = 30000;
|
|
303
344
|
|
|
304
345
|
async function extractWithJinaReader(
|
|
305
346
|
url: string,
|
|
347
|
+
timeoutMs: number,
|
|
306
348
|
signal?: AbortSignal,
|
|
307
349
|
lookup?: Lookup,
|
|
308
350
|
): Promise<ExtractedContent | null> {
|
|
@@ -325,7 +367,7 @@ async function extractWithJinaReader(
|
|
|
325
367
|
"X-No-Cache": "true",
|
|
326
368
|
},
|
|
327
369
|
signal: AbortSignal.any([
|
|
328
|
-
AbortSignal.timeout(
|
|
370
|
+
AbortSignal.timeout(timeoutMs),
|
|
329
371
|
...(signal ? [signal] : []),
|
|
330
372
|
]),
|
|
331
373
|
});
|
|
@@ -480,18 +522,14 @@ export async function extractContent(
|
|
|
480
522
|
}
|
|
481
523
|
}
|
|
482
524
|
|
|
483
|
-
if (options?.authFetchProfile) {
|
|
525
|
+
if (options?.authFetchProfile || options?.mode === "raw") {
|
|
484
526
|
try {
|
|
485
|
-
return await extractViaHttp(url, signal, options);
|
|
527
|
+
return await extractViaHttp(url, resolveFetchTimeoutMs(options), signal, options);
|
|
486
528
|
} catch (err) {
|
|
487
529
|
return { url, title: "", content: "", error: errorMessage(err) };
|
|
488
530
|
}
|
|
489
531
|
}
|
|
490
532
|
|
|
491
|
-
if (options?.mode === "raw") {
|
|
492
|
-
return extractViaHttp(url, signal, options);
|
|
493
|
-
}
|
|
494
|
-
|
|
495
533
|
if (options?.frames || options?.timestamp) {
|
|
496
534
|
const disabled = imageGateError();
|
|
497
535
|
if (disabled) return { url, title: "", content: "", error: disabled };
|
|
@@ -703,6 +741,13 @@ export async function extractContent(
|
|
|
703
741
|
|
|
704
742
|
if (signal?.aborted) return abortedResult(url);
|
|
705
743
|
|
|
744
|
+
let fetchTimeoutMs: number;
|
|
745
|
+
try {
|
|
746
|
+
fetchTimeoutMs = resolveFetchTimeoutMs(options);
|
|
747
|
+
} catch (err) {
|
|
748
|
+
return { url, title: "", content: "", error: errorMessage(err) };
|
|
749
|
+
}
|
|
750
|
+
|
|
706
751
|
let fetchRouting: FetchRouting;
|
|
707
752
|
try {
|
|
708
753
|
fetchRouting = loadFetchRouting();
|
|
@@ -731,7 +776,7 @@ export async function extractContent(
|
|
|
731
776
|
? { ...httpResult, error: message }
|
|
732
777
|
: { url, title: "", content: "", error: message };
|
|
733
778
|
const runHttpProvider = async (): Promise<ExtractedContent | null> => {
|
|
734
|
-
const { declaredLinks: discoveredLinks = [], ...result } = await extractViaHttp(url, signal, options);
|
|
779
|
+
const { declaredLinks: discoveredLinks = [], ...result } = await extractViaHttp(url, fetchTimeoutMs, signal, options);
|
|
735
780
|
httpResult = result;
|
|
736
781
|
declaredLinks = discoveredLinks;
|
|
737
782
|
if (signal?.aborted) return abortedResult(url);
|
|
@@ -786,7 +831,7 @@ export async function extractContent(
|
|
|
786
831
|
}
|
|
787
832
|
|
|
788
833
|
if (provider === "jina") {
|
|
789
|
-
const jinaResult = await extractWithJinaReader(url, signal, options?.lookup);
|
|
834
|
+
const jinaResult = await extractWithJinaReader(url, fetchTimeoutMs, signal, options?.lookup);
|
|
790
835
|
if (jinaResult) return withDeclaredLinks(jinaResult);
|
|
791
836
|
continue;
|
|
792
837
|
}
|
|
@@ -1066,10 +1111,10 @@ function responseSizeLimitError(maxBytes: number): Error {
|
|
|
1066
1111
|
|
|
1067
1112
|
async function extractViaHttp(
|
|
1068
1113
|
url: string,
|
|
1114
|
+
timeoutMs: number,
|
|
1069
1115
|
signal?: AbortSignal,
|
|
1070
1116
|
options?: ExtractOptions,
|
|
1071
1117
|
): Promise<HttpExtractedContent> {
|
|
1072
|
-
const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1073
1118
|
const activityId = activityMonitor.logStart({ type: "fetch", url });
|
|
1074
1119
|
|
|
1075
1120
|
const controller = new AbortController();
|