@herbertgao/pi-extensions 2026.8.14 → 2026.9.0
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 +19 -13
- package/THIRD_PARTY_NOTICES.md +26 -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/@herbertgao/pi-cc-extensions/extensions/config/config.ts +20 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +46 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +55 -17
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +6 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +5 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +34 -30
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +129 -88
- package/node_modules/@narumitw/pi-btw/dist/index.ts +283 -59
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
- 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 +264 -24
- 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/@narumitw/pi-btw/src/text.ts +18 -0
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +5 -16
- package/node_modules/@narumitw/pi-caffeinate/LICENSE +21 -0
- package/node_modules/@narumitw/pi-caffeinate/README.md +224 -0
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts +1112 -0
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts.map +7 -0
- package/node_modules/@narumitw/pi-caffeinate/package.json +53 -0
- package/node_modules/@narumitw/pi-caffeinate/src/caffeinate.ts +811 -0
- package/node_modules/@narumitw/pi-caffeinate/src/dbus-inhibit.ts +153 -0
- package/node_modules/@narumitw/pi-caffeinate/src/index.ts +1 -0
- package/node_modules/@narumitw/pi-caffeinate/src/inhibitor-process.ts +43 -0
- package/node_modules/@narumitw/pi-caffeinate/src/inhibitors.ts +167 -0
- package/node_modules/@narumitw/pi-caffeinate/src/settings.ts +202 -0
- package/node_modules/@tifan/pi-handoff/README.md +39 -13
- package/node_modules/@tifan/pi-handoff/package.json +2 -2
- package/node_modules/@tifan/pi-handoff/src/index.ts +166 -5
- 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 +42 -0
- package/node_modules/pi-mcp-adapter/README.md +15 -15
- package/node_modules/pi-mcp-adapter/commands.ts +59 -22
- package/node_modules/pi-mcp-adapter/config.ts +18 -4
- package/node_modules/pi-mcp-adapter/direct-tools.ts +26 -6
- package/node_modules/pi-mcp-adapter/dist/config.d.ts +4 -0
- package/node_modules/pi-mcp-adapter/dist/config.js +13 -4
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +12 -4
- package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
- package/node_modules/pi-mcp-adapter/error-signal.ts +15 -4
- package/node_modules/pi-mcp-adapter/errors.ts +141 -0
- package/node_modules/pi-mcp-adapter/host-html-template.ts +58 -5
- package/node_modules/pi-mcp-adapter/index.ts +2 -3
- package/node_modules/pi-mcp-adapter/init.ts +5 -0
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +105 -32
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +30 -9
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +66 -28
- package/node_modules/pi-mcp-adapter/mcp-status.ts +2 -0
- package/node_modules/pi-mcp-adapter/package.json +2 -1
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +88 -25
- package/node_modules/pi-mcp-adapter/sandbox-proxy-template.ts +217 -0
- package/node_modules/pi-mcp-adapter/server-manager.ts +343 -6
- package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +1 -0
- package/node_modules/pi-mcp-adapter/types.ts +19 -4
- package/node_modules/pi-mcp-adapter/ui-resource-handler.ts +18 -2
- package/node_modules/pi-mcp-adapter/ui-server.ts +179 -54
- package/node_modules/pi-mcp-adapter/ui-session.ts +28 -8
- 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 +21 -17
- 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
|
@@ -12,6 +12,11 @@ import type { ConsentManager } from "./consent-manager.ts";
|
|
|
12
12
|
import { ServerError, wrapError } from "./errors.ts";
|
|
13
13
|
import { formatAuthRequiredMessage, normalizeToolArguments } from "./utils.ts";
|
|
14
14
|
import { buildHostHtmlTemplate, buildCspMetaContent } from "./host-html-template.ts";
|
|
15
|
+
import {
|
|
16
|
+
buildSandboxProxyCsp,
|
|
17
|
+
buildSandboxProxyHtml,
|
|
18
|
+
SANDBOX_PROXY_PATH,
|
|
19
|
+
} from "./sandbox-proxy-template.ts";
|
|
15
20
|
import { logger } from "./logger.ts";
|
|
16
21
|
import type { McpServerManager } from "./server-manager.ts";
|
|
17
22
|
import type { McpExtensionState } from "./state.ts";
|
|
@@ -99,6 +104,11 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
|
|
|
99
104
|
const eventLog: Array<{ id: number; name: string; payload: unknown }> = [];
|
|
100
105
|
let latestCheckpointEventId: number | undefined;
|
|
101
106
|
let streamSummary: UiStreamSummary | undefined;
|
|
107
|
+
let server: http.Server | null = null;
|
|
108
|
+
let sandboxProxyServer: http.Server | null = null;
|
|
109
|
+
let sandboxProxyUrl: string | null = null;
|
|
110
|
+
let closeTimer: NodeJS.Timeout | null = null;
|
|
111
|
+
let listenersClosed = false;
|
|
102
112
|
|
|
103
113
|
// Track messages from UI for retrieval
|
|
104
114
|
const sessionMessages: UiSessionMessages = {
|
|
@@ -266,6 +276,32 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
|
|
|
266
276
|
watchdog = null;
|
|
267
277
|
};
|
|
268
278
|
|
|
279
|
+
const closeListeners = () => {
|
|
280
|
+
if (listenersClosed) return;
|
|
281
|
+
listenersClosed = true;
|
|
282
|
+
stopWatchdog();
|
|
283
|
+
if (closeTimer) {
|
|
284
|
+
clearTimeout(closeTimer);
|
|
285
|
+
closeTimer = null;
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
server?.close();
|
|
289
|
+
} catch {}
|
|
290
|
+
try {
|
|
291
|
+
sandboxProxyServer?.close();
|
|
292
|
+
} catch {}
|
|
293
|
+
closeSse();
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const scheduleListenerClose = () => {
|
|
297
|
+
if (closeTimer || listenersClosed) return;
|
|
298
|
+
closeTimer = setTimeout(() => {
|
|
299
|
+
closeTimer = null;
|
|
300
|
+
closeListeners();
|
|
301
|
+
}, 20);
|
|
302
|
+
closeTimer.unref();
|
|
303
|
+
};
|
|
304
|
+
|
|
269
305
|
const markCompleted = (reason: string) => {
|
|
270
306
|
if (completed) return;
|
|
271
307
|
log.debug("Session completed", { reason });
|
|
@@ -273,9 +309,10 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
|
|
|
273
309
|
completed = true;
|
|
274
310
|
stopWatchdog();
|
|
275
311
|
options.onComplete?.(reason);
|
|
312
|
+
scheduleListenerClose();
|
|
276
313
|
};
|
|
277
314
|
|
|
278
|
-
const
|
|
315
|
+
const hostServer = http.createServer(async (req, res) => {
|
|
279
316
|
try {
|
|
280
317
|
const method = req.method || "GET";
|
|
281
318
|
const hostHeader = req.headers.host;
|
|
@@ -302,6 +339,10 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
|
|
|
302
339
|
}
|
|
303
340
|
if (!validateTokenQuery(url, sessionToken, res)) return;
|
|
304
341
|
touchHeartbeat();
|
|
342
|
+
if (!sandboxProxyUrl) {
|
|
343
|
+
sendText(res, 503, "Sandbox proxy is not ready");
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
305
346
|
|
|
306
347
|
const html = buildHostHtmlTemplate({
|
|
307
348
|
sessionToken,
|
|
@@ -314,6 +355,7 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
|
|
|
314
355
|
requireToolConsent: options.consentManager.requiresPrompt(options.serverName),
|
|
315
356
|
cacheToolConsent: options.consentManager.shouldCacheConsent(),
|
|
316
357
|
hostContext,
|
|
358
|
+
sandboxProxyUrl,
|
|
317
359
|
});
|
|
318
360
|
|
|
319
361
|
res.writeHead(200, {
|
|
@@ -498,9 +540,15 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
|
|
|
498
540
|
...(options.onNeedsAuth ? { onNeedsAuth: options.onNeedsAuth } : {}),
|
|
499
541
|
},
|
|
500
542
|
options.serverName,
|
|
501
|
-
(conn) =>
|
|
543
|
+
async (conn) => {
|
|
544
|
+
await options.manager.ensureListen?.(options.serverName, conn);
|
|
545
|
+
return conn.client.callTool(callArgs, options.manager.getRequestOptions?.(options.serverName));
|
|
546
|
+
},
|
|
502
547
|
)
|
|
503
|
-
: await
|
|
548
|
+
: await (async () => {
|
|
549
|
+
await options.manager.ensureListen?.(options.serverName, connection);
|
|
550
|
+
return connection.client.callTool(callArgs, options.manager.getRequestOptions?.(options.serverName));
|
|
551
|
+
})();
|
|
504
552
|
sendJson(res, 200, { ok: true, result });
|
|
505
553
|
} finally {
|
|
506
554
|
options.manager.decrementInFlight(options.serverName);
|
|
@@ -614,12 +662,6 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
|
|
|
614
662
|
: "done";
|
|
615
663
|
markCompleted(reason);
|
|
616
664
|
sendJson(res, 200, { ok: true, result: {} });
|
|
617
|
-
setTimeout(() => {
|
|
618
|
-
try {
|
|
619
|
-
server.close();
|
|
620
|
-
} catch {}
|
|
621
|
-
closeSse();
|
|
622
|
-
}, 20).unref();
|
|
623
665
|
return;
|
|
624
666
|
}
|
|
625
667
|
|
|
@@ -641,6 +683,7 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
|
|
|
641
683
|
sendJson(res, status, { ok: false, error: wrapped.message });
|
|
642
684
|
}
|
|
643
685
|
});
|
|
686
|
+
server = hostServer;
|
|
644
687
|
|
|
645
688
|
if (options.initialResultPromise) {
|
|
646
689
|
options.initialResultPromise.then(
|
|
@@ -656,10 +699,6 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
|
|
|
656
699
|
if (completed) return;
|
|
657
700
|
if (Date.now() - lastHeartbeatAt <= ABANDONED_GRACE_MS) return;
|
|
658
701
|
markCompleted("stale");
|
|
659
|
-
try {
|
|
660
|
-
server.close();
|
|
661
|
-
} catch {}
|
|
662
|
-
closeSse();
|
|
663
702
|
}, WATCHDOG_INTERVAL_MS);
|
|
664
703
|
watchdog.unref();
|
|
665
704
|
|
|
@@ -667,13 +706,79 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
|
|
|
667
706
|
const candidates = resolvePortCandidates(options.port);
|
|
668
707
|
let candidateIndex = 0;
|
|
669
708
|
|
|
709
|
+
const startSandboxProxy = (parentOrigin: string): Promise<number> => {
|
|
710
|
+
const proxy = http.createServer((req, res) => {
|
|
711
|
+
try {
|
|
712
|
+
const method = req.method || "GET";
|
|
713
|
+
const hostHeader = req.headers.host;
|
|
714
|
+
const url = new URL(req.url || "/", `http://${hostHeader || "127.0.0.1"}`);
|
|
715
|
+
if (hostHeader !== undefined && !isAllowedHost(url.hostname)) {
|
|
716
|
+
sendText(res, 403, "Invalid host");
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (method === "HEAD" && url.pathname === SANDBOX_PROXY_PATH) {
|
|
721
|
+
res.writeHead(200, {
|
|
722
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
723
|
+
"Cache-Control": "no-store",
|
|
724
|
+
"Content-Security-Policy": buildSandboxProxyCsp(),
|
|
725
|
+
});
|
|
726
|
+
res.end();
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (method === "GET" && url.pathname === SANDBOX_PROXY_PATH) {
|
|
731
|
+
res.writeHead(200, {
|
|
732
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
733
|
+
"Cache-Control": "no-store",
|
|
734
|
+
"Content-Security-Policy": buildSandboxProxyCsp(),
|
|
735
|
+
"Referrer-Policy": "no-referrer",
|
|
736
|
+
"X-Content-Type-Options": "nosniff",
|
|
737
|
+
});
|
|
738
|
+
res.end(buildSandboxProxyHtml({ parentOrigin }));
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
sendJson(res, 404, { ok: false, error: "Not found" });
|
|
743
|
+
} catch (error) {
|
|
744
|
+
sendJson(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
sandboxProxyServer = proxy;
|
|
748
|
+
|
|
749
|
+
return new Promise((resolveProxy, rejectProxy) => {
|
|
750
|
+
const onError = (error: NodeJS.ErrnoException) => {
|
|
751
|
+
proxy.off("listening", onListening);
|
|
752
|
+
rejectProxy(error);
|
|
753
|
+
};
|
|
754
|
+
const onListening = () => {
|
|
755
|
+
proxy.off("error", onError);
|
|
756
|
+
const address = proxy.address();
|
|
757
|
+
if (!address || typeof address === "string") {
|
|
758
|
+
rejectProxy(new ServerError("invalid sandbox proxy address"));
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
resolveProxy(address.port);
|
|
762
|
+
};
|
|
763
|
+
proxy.once("error", onError);
|
|
764
|
+
proxy.listen(0, "127.0.0.1", onListening);
|
|
765
|
+
});
|
|
766
|
+
};
|
|
767
|
+
|
|
670
768
|
const listen = () => {
|
|
671
|
-
|
|
672
|
-
|
|
769
|
+
const candidate = candidates[candidateIndex];
|
|
770
|
+
if (candidate === undefined) {
|
|
771
|
+
const error = new ServerError("no UI server port candidates available");
|
|
772
|
+
closeListeners();
|
|
773
|
+
reject(error);
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
hostServer.once("error", onError);
|
|
777
|
+
hostServer.listen(candidate, "127.0.0.1", onListening);
|
|
673
778
|
};
|
|
674
779
|
|
|
675
780
|
const onError = (error: NodeJS.ErrnoException) => {
|
|
676
|
-
|
|
781
|
+
hostServer.off("listening", onListening);
|
|
677
782
|
if (error.code === "EADDRINUSE" && candidateIndex < candidates.length - 1) {
|
|
678
783
|
candidateIndex += 1;
|
|
679
784
|
listen();
|
|
@@ -681,6 +786,7 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
|
|
|
681
786
|
}
|
|
682
787
|
log.error("Failed to start server", error);
|
|
683
788
|
const port = candidates[candidateIndex];
|
|
789
|
+
closeListeners();
|
|
684
790
|
reject(new ServerError(error.message, {
|
|
685
791
|
...(port !== undefined ? { port } : {}),
|
|
686
792
|
cause: error,
|
|
@@ -688,52 +794,71 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
|
|
|
688
794
|
};
|
|
689
795
|
|
|
690
796
|
const onListening = () => {
|
|
691
|
-
|
|
692
|
-
const address =
|
|
797
|
+
hostServer.off("error", onError);
|
|
798
|
+
const address = hostServer.address();
|
|
693
799
|
if (!address || typeof address === "string") {
|
|
694
800
|
const err = new ServerError("invalid address");
|
|
695
801
|
log.error("Invalid server address", err);
|
|
802
|
+
closeListeners();
|
|
696
803
|
reject(err);
|
|
697
804
|
return;
|
|
698
805
|
}
|
|
699
806
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
807
|
+
const parentOrigin = `http://localhost:${address.port}`;
|
|
808
|
+
void startSandboxProxy(parentOrigin).then((proxyPort) => {
|
|
809
|
+
if (completed || listenersClosed) {
|
|
810
|
+
closeListeners();
|
|
811
|
+
reject(new ServerError("UI session completed before sandbox proxy was ready"));
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
sandboxProxyUrl = `http://localhost:${proxyPort}${SANDBOX_PROXY_PATH}`;
|
|
815
|
+
log.debug("Servers started", { port: address.port, proxyPort });
|
|
816
|
+
rememberMoshiDiscoveryPort(address.port);
|
|
817
|
+
|
|
818
|
+
const handle: UiServerHandle = {
|
|
819
|
+
url: `http://localhost:${address.port}/?session=${sessionToken}`,
|
|
820
|
+
port: address.port,
|
|
821
|
+
proxyUrl: sandboxProxyUrl,
|
|
822
|
+
proxyPort,
|
|
823
|
+
sessionToken,
|
|
824
|
+
serverName: options.serverName,
|
|
825
|
+
toolName: options.toolName,
|
|
826
|
+
close: (reason?: string) => {
|
|
827
|
+
markCompleted(reason ?? "closed");
|
|
828
|
+
closeListeners();
|
|
829
|
+
},
|
|
830
|
+
sendToolInput: (args: Record<string, unknown>) => {
|
|
831
|
+
pushEvent("tool-input", { arguments: args });
|
|
832
|
+
},
|
|
833
|
+
sendToolResult: (result: CallToolResult) => {
|
|
834
|
+
pushEvent("tool-result", result);
|
|
835
|
+
},
|
|
836
|
+
sendResultPatch: (result: CallToolResult) => {
|
|
837
|
+
pushEvent("result-patch", result);
|
|
838
|
+
},
|
|
839
|
+
sendToolCancelled: (reason: string) => {
|
|
840
|
+
pushEvent("tool-cancelled", { reason });
|
|
841
|
+
},
|
|
842
|
+
sendResourceUpdated: (uri: string) => {
|
|
843
|
+
pushEvent("resource-updated", { uri });
|
|
844
|
+
},
|
|
845
|
+
sendHostContext: (context: UiHostContext) => {
|
|
846
|
+
Object.assign(hostContext, context);
|
|
847
|
+
pushEvent("host-context", context);
|
|
848
|
+
},
|
|
849
|
+
getSessionMessages: () => ({ ...sessionMessages }),
|
|
850
|
+
getStreamSummary: () => streamSummary ? { ...streamSummary, phases: [...streamSummary.phases] } : undefined,
|
|
851
|
+
};
|
|
735
852
|
|
|
736
|
-
|
|
853
|
+
resolve(handle);
|
|
854
|
+
}).catch((error) => {
|
|
855
|
+
log.error("Failed to start sandbox proxy", error instanceof Error ? error : undefined);
|
|
856
|
+
closeListeners();
|
|
857
|
+
const wrapped = error instanceof ServerError
|
|
858
|
+
? error
|
|
859
|
+
: new ServerError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
860
|
+
reject(wrapped);
|
|
861
|
+
});
|
|
737
862
|
};
|
|
738
863
|
|
|
739
864
|
listen();
|
|
@@ -21,6 +21,7 @@ import { isGlimpseAvailable, openGlimpseWindow } from "./glimpse-ui.ts";
|
|
|
21
21
|
import type { SessionRecoveryDeps } from "./session-recovery.ts";
|
|
22
22
|
import { combineAbortSignals, isAbortError } from "./runtime-owner.ts";
|
|
23
23
|
import { throwIfAborted } from "./abort.ts";
|
|
24
|
+
import { InputRequiredNeedsUiError } from "./errors.ts";
|
|
24
25
|
|
|
25
26
|
let activeGlimpseWindow: { close(): void } | null = null;
|
|
26
27
|
|
|
@@ -172,7 +173,7 @@ function probeMoshiGateway(): Promise<boolean> {
|
|
|
172
173
|
});
|
|
173
174
|
}
|
|
174
175
|
|
|
175
|
-
function remoteAccessHint(opts: { url: string; port: number; moshi: boolean; openError: string | null; openedOnHost?: boolean }): string {
|
|
176
|
+
function remoteAccessHint(opts: { url: string; port: number; proxyPort: number; moshi: boolean; openError: string | null; openedOnHost?: boolean }): string {
|
|
176
177
|
const lines = [
|
|
177
178
|
opts.openError !== null
|
|
178
179
|
? "Couldn't open MCP UI here. Open it from your own device:"
|
|
@@ -185,10 +186,12 @@ function remoteAccessHint(opts: { url: string; port: number; moshi: boolean; ope
|
|
|
185
186
|
lines.push(`Browser launch failed: ${opts.openError}`);
|
|
186
187
|
}
|
|
187
188
|
if (opts.moshi) {
|
|
188
|
-
lines.push(
|
|
189
|
+
lines.push(
|
|
190
|
+
`Moshi: tap the preview button in the terminal title bar and pick this MCP UI server (it must reach ports ${opts.port} and ${opts.proxyPort}).`,
|
|
191
|
+
);
|
|
189
192
|
}
|
|
190
193
|
lines.push(
|
|
191
|
-
`SSH: run \`ssh -L ${opts.port}:127.0.0.1:${opts.port} <this-host>\` on your local machine, then open the URL above.`,
|
|
194
|
+
`SSH: run \`ssh -L ${opts.port}:127.0.0.1:${opts.port} -L ${opts.proxyPort}:127.0.0.1:${opts.proxyPort} <this-host>\` on your local machine, then open the URL above.`,
|
|
192
195
|
"mosh can't forward ports - run that ssh command in a separate terminal.",
|
|
193
196
|
);
|
|
194
197
|
return lines.join("\n");
|
|
@@ -314,11 +317,13 @@ export async function maybeStartUiSession(
|
|
|
314
317
|
let active = true;
|
|
315
318
|
let nextStreamSequence = 0;
|
|
316
319
|
let handle: UiServerHandle;
|
|
320
|
+
const resourceListenerToken = randomUUID();
|
|
317
321
|
|
|
318
|
-
const
|
|
322
|
+
const cleanupListeners = () => {
|
|
319
323
|
if (streamToken) {
|
|
320
324
|
state.manager.removeUiStreamListener(streamToken);
|
|
321
325
|
}
|
|
326
|
+
state.manager.removeResourceUpdatedListener?.(resourceListenerToken);
|
|
322
327
|
};
|
|
323
328
|
|
|
324
329
|
handle = await startUiServer({
|
|
@@ -394,7 +399,7 @@ export async function maybeStartUiSession(
|
|
|
394
399
|
|
|
395
400
|
onComplete: (reason: string) => {
|
|
396
401
|
active = false;
|
|
397
|
-
|
|
402
|
+
cleanupListeners();
|
|
398
403
|
|
|
399
404
|
if (state.uiServer === handle) {
|
|
400
405
|
const messages = handle.getSessionMessages();
|
|
@@ -454,6 +459,16 @@ export async function maybeStartUiSession(
|
|
|
454
459
|
});
|
|
455
460
|
}
|
|
456
461
|
|
|
462
|
+
state.manager.registerResourceUpdatedListener?.(
|
|
463
|
+
resourceListenerToken,
|
|
464
|
+
request.serverName,
|
|
465
|
+
request.uiResourceUri,
|
|
466
|
+
(_serverName, uri) => {
|
|
467
|
+
if (!active || state.uiServer !== handle) return;
|
|
468
|
+
handle.sendResourceUpdated(uri);
|
|
469
|
+
},
|
|
470
|
+
);
|
|
471
|
+
|
|
457
472
|
state.uiServer = handle;
|
|
458
473
|
|
|
459
474
|
const viewerPref = process.env.MCP_UI_VIEWER?.toLowerCase();
|
|
@@ -466,7 +481,11 @@ export async function maybeStartUiSession(
|
|
|
466
481
|
if (uiSuppressed) {
|
|
467
482
|
viewer = "suppressed";
|
|
468
483
|
windowOpen = false;
|
|
469
|
-
state.ui?.notify(
|
|
484
|
+
state.ui?.notify(
|
|
485
|
+
`MCP UI window suppressed (MCP_UI_VIEWER=${viewerPref}). Open manually: ${handle.url}\n` +
|
|
486
|
+
`If this session is remote, run ssh -L ${handle.port}:127.0.0.1:${handle.port} -L ${handle.proxyPort}:127.0.0.1:${handle.proxyPort} <this-host> first.`,
|
|
487
|
+
"info",
|
|
488
|
+
);
|
|
470
489
|
log.info("Suppressing MCP UI window (MCP_UI_VIEWER=" + viewerPref + ")", { url: handle.url });
|
|
471
490
|
} else {
|
|
472
491
|
const remoteLikely = remoteByEnv || await hasActiveRemoteLogin();
|
|
@@ -474,6 +493,7 @@ export async function maybeStartUiSession(
|
|
|
474
493
|
state.ui?.notify(remoteAccessHint({
|
|
475
494
|
url: handle.url,
|
|
476
495
|
port: handle.port,
|
|
496
|
+
proxyPort: handle.proxyPort,
|
|
477
497
|
moshi: await probeMoshiGateway(),
|
|
478
498
|
openError,
|
|
479
499
|
openedOnHost,
|
|
@@ -554,12 +574,12 @@ export async function maybeStartUiSession(
|
|
|
554
574
|
},
|
|
555
575
|
close: (reason?: string) => {
|
|
556
576
|
active = false;
|
|
557
|
-
|
|
577
|
+
cleanupListeners();
|
|
558
578
|
handle.close(reason);
|
|
559
579
|
},
|
|
560
580
|
};
|
|
561
581
|
} catch (error) {
|
|
562
|
-
if (error instanceof UrlElicitationRequiredError || isAbortError(error, runtimeSignal)) throw error;
|
|
582
|
+
if (error instanceof UrlElicitationRequiredError || error instanceof InputRequiredNeedsUiError || isAbortError(error, runtimeSignal)) throw error;
|
|
563
583
|
const message = error instanceof Error ? error.message : String(error);
|
|
564
584
|
log.error("Failed to start UI session", error instanceof Error ? error : undefined);
|
|
565
585
|
state.ui?.notify(
|
|
@@ -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
|
|