@herbertgao/pi-extensions 2026.9.17 → 2026.9.19
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 +31 -33
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/node_modules/@herbertgao/pi-bark/package.json +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +4 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +4 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/working-message.ts +21 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +15 -4
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/index.ts +43 -7
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +7 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +75 -75
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +7 -7
- package/node_modules/@herbertgao/resume-from/package.json +4 -2
- package/node_modules/@herbertgao/sol-pi/package.json +6 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +99 -99
- package/node_modules/@pi-plugins/fast-mode/package.json +1 -1
- package/node_modules/@tifan/pi-handoff/package.json +1 -1
- package/node_modules/@tifan/pi-handoff/src/index.ts +56 -26
- package/node_modules/@tifan/pi-mermaid-open/package.json +1 -1
- package/node_modules/@tifan/pi-preferred-thinking/package.json +1 -1
- package/node_modules/@tifan/pi-recap/package.json +1 -1
- package/node_modules/@tifan/pi-rename/package.json +1 -1
- package/node_modules/pi-antigravity/README.md +29 -6
- package/node_modules/pi-antigravity/package.json +14 -9
- package/node_modules/pi-antigravity/src/auth/accounts.ts +327 -0
- package/node_modules/pi-antigravity/src/auth/index.ts +9 -0
- package/node_modules/pi-antigravity/src/client/client.ts +1 -1
- package/node_modules/pi-antigravity/src/image/image.ts +4 -4
- package/node_modules/pi-antigravity/src/index.ts +203 -13
- package/node_modules/pi-antigravity/src/search/index.ts +1 -0
- package/node_modules/pi-antigravity/src/search/search.ts +325 -0
- package/node_modules/pi-antigravity/src/stream/stream.ts +191 -14
- package/node_modules/pi-antigravity/src/types/enums.ts +45 -36
- package/node_modules/pi-antigravity/src/types/types.ts +5 -12
- package/node_modules/pi-antigravity/src/utils/http.ts +7 -2
- package/node_modules/pi-antigravity/src/utils/util.ts +31 -3
- package/node_modules/pi-lens/package.json +158 -158
- package/node_modules/pi-multi-account/package.json +2 -2
- package/node_modules/pi-typesafe/README.md +3 -1
- package/node_modules/pi-typesafe/dist/auth.d.ts +9 -5
- package/node_modules/pi-typesafe/dist/auth.js +7 -3
- package/node_modules/pi-typesafe/dist/backends.d.ts +33 -3
- package/node_modules/pi-typesafe/dist/backends.js +95 -1
- package/node_modules/pi-typesafe/dist/client.d.ts +5 -5
- package/node_modules/pi-typesafe/dist/client.js +12 -9
- package/node_modules/pi-typesafe/dist/credentials.d.ts +14 -8
- package/node_modules/pi-typesafe/dist/credentials.js +10 -7
- package/node_modules/pi-typesafe/dist/errors.d.ts +2 -2
- package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
- package/node_modules/pi-typesafe/dist/index.js +1 -1
- package/node_modules/pi-typesafe/dist/login.d.ts +2 -2
- package/node_modules/pi-typesafe/dist/login.js +2 -2
- package/node_modules/pi-typesafe/package.json +6 -5
- package/node_modules/pi-web-access/CHANGELOG.md +72 -0
- package/node_modules/pi-web-access/README.md +34 -13
- package/node_modules/pi-web-access/chrome-cookies.ts +79 -25
- package/node_modules/pi-web-access/curator-page.ts +5 -1
- package/node_modules/pi-web-access/curator-server.ts +2 -0
- package/node_modules/pi-web-access/dist/index.js +1337 -1009
- package/node_modules/pi-web-access/exa.ts +20 -52
- package/node_modules/pi-web-access/extract.ts +41 -4
- package/node_modules/pi-web-access/gemini-search.ts +12 -4
- package/node_modules/pi-web-access/index.ts +29 -37
- package/node_modules/pi-web-access/openai-search.ts +35 -7
- package/node_modules/pi-web-access/package.json +8 -7
- package/node_modules/pi-web-access/page-query.ts +2 -2
- package/node_modules/pi-web-access/query-rewrite.ts +2 -3
- package/node_modules/pi-web-access/ssrf-protection.ts +27 -10
- package/node_modules/pi-web-access/storage.ts +48 -77
- package/node_modules/pi-web-access/summary-model-scope.ts +6 -77
- package/node_modules/pi-web-access/summary-review.ts +3 -4
- package/node_modules/pi-web-access/tavily.ts +67 -20
- package/node_modules/pi-web-access/tool-activation.ts +53 -33
- package/node_modules/pi-web-access/utils.ts +1 -1
- package/node_modules/pi-web-access/you.ts +164 -0
- package/node_modules/pi-web-access/zai.ts +161 -0
- package/node_modules/remote-pi/package.json +5 -1
- package/package.json +19 -31
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +0 -1013
- package/node_modules/pi-mcp-adapter/LICENSE +0 -21
- package/node_modules/pi-mcp-adapter/OAUTH.md +0 -369
- package/node_modules/pi-mcp-adapter/README.md +0 -1021
- package/node_modules/pi-mcp-adapter/abort.ts +0 -34
- package/node_modules/pi-mcp-adapter/agent-dir.ts +0 -68
- package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +0 -416
- package/node_modules/pi-mcp-adapter/agent-plugin-provenance.ts +0 -36
- package/node_modules/pi-mcp-adapter/app-bridge.bundle.js +0 -67
- package/node_modules/pi-mcp-adapter/banner.png +0 -0
- package/node_modules/pi-mcp-adapter/bearer-command-resolver.ts +0 -193
- package/node_modules/pi-mcp-adapter/claude-plugin-loader.ts +0 -365
- package/node_modules/pi-mcp-adapter/cli.js +0 -454
- package/node_modules/pi-mcp-adapter/commands.ts +0 -861
- package/node_modules/pi-mcp-adapter/config.ts +0 -1565
- package/node_modules/pi-mcp-adapter/consent-manager.ts +0 -95
- package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +0 -244
- package/node_modules/pi-mcp-adapter/direct-tools.ts +0 -428
- package/node_modules/pi-mcp-adapter/dist/abort.d.ts +0 -2
- package/node_modules/pi-mcp-adapter/dist/abort.js +0 -36
- package/node_modules/pi-mcp-adapter/dist/abort.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/agent-dir.d.ts +0 -10
- package/node_modules/pi-mcp-adapter/dist/agent-dir.js +0 -63
- package/node_modules/pi-mcp-adapter/dist/agent-dir.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.d.ts +0 -8
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js +0 -398
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.d.ts +0 -1
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js +0 -31
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.d.ts +0 -6
- package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.js +0 -194
- package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.d.ts +0 -9
- package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js +0 -362
- package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/config.d.ts +0 -134
- package/node_modules/pi-mcp-adapter/dist/config.js +0 -1361
- package/node_modules/pi-mcp-adapter/dist/config.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/consent-manager.d.ts +0 -18
- package/node_modules/pi-mcp-adapter/dist/consent-manager.js +0 -88
- package/node_modules/pi-mcp-adapter/dist/consent-manager.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/elicitation-handler.d.ts +0 -17
- package/node_modules/pi-mcp-adapter/dist/elicitation-handler.js +0 -316
- package/node_modules/pi-mcp-adapter/dist/elicitation-handler.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/errors.d.ts +0 -131
- package/node_modules/pi-mcp-adapter/dist/errors.js +0 -278
- package/node_modules/pi-mcp-adapter/dist/errors.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/http-ca.d.ts +0 -8
- package/node_modules/pi-mcp-adapter/dist/http-ca.js +0 -88
- package/node_modules/pi-mcp-adapter/dist/http-ca.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/jev-client.d.ts +0 -14
- package/node_modules/pi-mcp-adapter/dist/jev-client.js +0 -367
- package/node_modules/pi-mcp-adapter/dist/jev-client.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/jev-contracts.d.ts +0 -77
- package/node_modules/pi-mcp-adapter/dist/jev-contracts.js +0 -2
- package/node_modules/pi-mcp-adapter/dist/jev-contracts.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/jev-key-store.d.ts +0 -51
- package/node_modules/pi-mcp-adapter/dist/jev-key-store.js +0 -205
- package/node_modules/pi-mcp-adapter/dist/jev-key-store.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.d.ts +0 -2
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +0 -68
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/lifecycle.d.ts +0 -56
- package/node_modules/pi-mcp-adapter/dist/lifecycle.js +0 -410
- package/node_modules/pi-mcp-adapter/dist/lifecycle.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/logger.d.ts +0 -51
- package/node_modules/pi-mcp-adapter/dist/logger.js +0 -131
- package/node_modules/pi-mcp-adapter/dist/logger.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.d.ts +0 -26
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.js +0 -103
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.d.ts +0 -118
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js +0 -1097
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +0 -170
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +0 -1142
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.d.ts +0 -24
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js +0 -203
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.d.ts +0 -53
- package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.js +0 -441
- package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.d.ts +0 -148
- package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.js +0 -689
- package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-probe.d.ts +0 -6
- package/node_modules/pi-mcp-adapter/dist/mcp-probe.js +0 -166
- package/node_modules/pi-mcp-adapter/dist/mcp-probe.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-tasks.d.ts +0 -102
- package/node_modules/pi-mcp-adapter/dist/mcp-tasks.js +0 -369
- package/node_modules/pi-mcp-adapter/dist/mcp-tasks.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-trace.d.ts +0 -95
- package/node_modules/pi-mcp-adapter/dist/mcp-trace.js +0 -242
- package/node_modules/pi-mcp-adapter/dist/mcp-trace.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.d.ts +0 -19
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +0 -301
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/npx-resolver.d.ts +0 -6
- package/node_modules/pi-mcp-adapter/dist/npx-resolver.js +0 -505
- package/node_modules/pi-mcp-adapter/dist/npx-resolver.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.d.ts +0 -2
- package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js +0 -144
- package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/request-headers-command.d.ts +0 -10
- package/node_modules/pi-mcp-adapter/dist/request-headers-command.js +0 -312
- package/node_modules/pi-mcp-adapter/dist/request-headers-command.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/resource-tools.d.ts +0 -1
- package/node_modules/pi-mcp-adapter/dist/resource-tools.js +0 -15
- package/node_modules/pi-mcp-adapter/dist/resource-tools.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/runtime-owner.d.ts +0 -13
- package/node_modules/pi-mcp-adapter/dist/runtime-owner.js +0 -97
- package/node_modules/pi-mcp-adapter/dist/runtime-owner.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/sampling-handler.d.ts +0 -17
- package/node_modules/pi-mcp-adapter/dist/sampling-handler.js +0 -203
- package/node_modules/pi-mcp-adapter/dist/sampling-handler.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/secure-keyring.d.ts +0 -11
- package/node_modules/pi-mcp-adapter/dist/secure-keyring.js +0 -172
- package/node_modules/pi-mcp-adapter/dist/secure-keyring.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/server-manager.d.ts +0 -157
- package/node_modules/pi-mcp-adapter/dist/server-manager.js +0 -1745
- package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/session-approvals.d.ts +0 -34
- package/node_modules/pi-mcp-adapter/dist/session-approvals.js +0 -140
- package/node_modules/pi-mcp-adapter/dist/session-approvals.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/session-recovery.d.ts +0 -26
- package/node_modules/pi-mcp-adapter/dist/session-recovery.js +0 -142
- package/node_modules/pi-mcp-adapter/dist/session-recovery.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/state.d.ts +0 -73
- package/node_modules/pi-mcp-adapter/dist/state.js +0 -2
- package/node_modules/pi-mcp-adapter/dist/state.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +0 -642
- package/node_modules/pi-mcp-adapter/dist/types.js +0 -331
- package/node_modules/pi-mcp-adapter/dist/types.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/ui-app-bridge-helpers.d.ts +0 -6
- package/node_modules/pi-mcp-adapter/dist/ui-app-bridge-helpers.js +0 -37
- package/node_modules/pi-mcp-adapter/dist/ui-app-bridge-helpers.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.d.ts +0 -17
- package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.js +0 -219
- package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/ui-stream-types.d.ts +0 -104
- package/node_modules/pi-mcp-adapter/dist/ui-stream-types.js +0 -59
- package/node_modules/pi-mcp-adapter/dist/ui-stream-types.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/ui-tool-visibility.d.ts +0 -4
- package/node_modules/pi-mcp-adapter/dist/ui-tool-visibility.js +0 -27
- package/node_modules/pi-mcp-adapter/dist/ui-tool-visibility.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.d.ts +0 -15
- package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.js +0 -85
- package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.js.map +0 -1
- package/node_modules/pi-mcp-adapter/dist/utils.d.ts +0 -41
- package/node_modules/pi-mcp-adapter/dist/utils.js +0 -417
- package/node_modules/pi-mcp-adapter/dist/utils.js.map +0 -1
- package/node_modules/pi-mcp-adapter/elicitation-handler.ts +0 -358
- package/node_modules/pi-mcp-adapter/error-signal.ts +0 -32
- package/node_modules/pi-mcp-adapter/errors.ts +0 -364
- package/node_modules/pi-mcp-adapter/examples/jev-accessibility-loop.mjs +0 -85
- package/node_modules/pi-mcp-adapter/examples/jev-semantic-filter.mjs +0 -34
- package/node_modules/pi-mcp-adapter/failure-backoff.ts +0 -23
- package/node_modules/pi-mcp-adapter/glimpse-ui.ts +0 -80
- package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -515
- package/node_modules/pi-mcp-adapter/http-ca.ts +0 -84
- package/node_modules/pi-mcp-adapter/index.ts +0 -2106
- package/node_modules/pi-mcp-adapter/init.ts +0 -721
- package/node_modules/pi-mcp-adapter/jev-client.ts +0 -300
- package/node_modules/pi-mcp-adapter/jev-contracts.ts +0 -53
- package/node_modules/pi-mcp-adapter/jev-key-store.ts +0 -192
- package/node_modules/pi-mcp-adapter/json-schema-validator.ts +0 -83
- package/node_modules/pi-mcp-adapter/lazy-loader.ts +0 -7
- package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -457
- package/node_modules/pi-mcp-adapter/logger.ts +0 -169
- package/node_modules/pi-mcp-adapter/mcp-auth-fetch.ts +0 -113
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +0 -1296
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +0 -1287
- package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +0 -250
- package/node_modules/pi-mcp-adapter/mcp-callback-server.ts +0 -534
- package/node_modules/pi-mcp-adapter/mcp-code.ts +0 -495
- package/node_modules/pi-mcp-adapter/mcp-install.ts +0 -60
- package/node_modules/pi-mcp-adapter/mcp-keyring-helper.cjs +0 -89
- package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +0 -819
- package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +0 -511
- package/node_modules/pi-mcp-adapter/mcp-panel-theme.ts +0 -104
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +0 -1099
- package/node_modules/pi-mcp-adapter/mcp-probe.ts +0 -201
- package/node_modules/pi-mcp-adapter/mcp-references.ts +0 -373
- package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +0 -137
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +0 -747
- package/node_modules/pi-mcp-adapter/mcp-status.ts +0 -105
- package/node_modules/pi-mcp-adapter/mcp-tasks.ts +0 -467
- package/node_modules/pi-mcp-adapter/mcp-trace.ts +0 -306
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +0 -362
- package/node_modules/pi-mcp-adapter/namespace-tools.ts +0 -334
- package/node_modules/pi-mcp-adapter/npx-resolver.ts +0 -543
- package/node_modules/pi-mcp-adapter/oauth-handler.ts +0 -30
- package/node_modules/pi-mcp-adapter/oauth.ts +0 -52
- package/node_modules/pi-mcp-adapter/onboarding-state.ts +0 -76
- package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +0 -156
- package/node_modules/pi-mcp-adapter/package.json +0 -219
- package/node_modules/pi-mcp-adapter/panel-keys.ts +0 -53
- package/node_modules/pi-mcp-adapter/prompts.ts +0 -385
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +0 -1735
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +0 -333
- package/node_modules/pi-mcp-adapter/resource-tools.ts +0 -17
- package/node_modules/pi-mcp-adapter/runtime-owner.ts +0 -106
- package/node_modules/pi-mcp-adapter/sampling-handler.ts +0 -261
- package/node_modules/pi-mcp-adapter/sandbox-proxy-template.ts +0 -144
- package/node_modules/pi-mcp-adapter/search-ranking.ts +0 -309
- package/node_modules/pi-mcp-adapter/secure-keyring.ts +0 -170
- package/node_modules/pi-mcp-adapter/semantic-search.ts +0 -186
- package/node_modules/pi-mcp-adapter/server-manager.ts +0 -2088
- package/node_modules/pi-mcp-adapter/session-approvals.ts +0 -201
- package/node_modules/pi-mcp-adapter/session-recovery.ts +0 -158
- package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +0 -49
- package/node_modules/pi-mcp-adapter/state.ts +0 -71
- package/node_modules/pi-mcp-adapter/tool-approval.ts +0 -205
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +0 -302
- package/node_modules/pi-mcp-adapter/tool-registrar.ts +0 -255
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +0 -573
- package/node_modules/pi-mcp-adapter/ts-shape.ts +0 -149
- package/node_modules/pi-mcp-adapter/types.ts +0 -1120
- package/node_modules/pi-mcp-adapter/ui-app-bridge-helpers.ts +0 -40
- package/node_modules/pi-mcp-adapter/ui-resource-handler.ts +0 -266
- package/node_modules/pi-mcp-adapter/ui-server.ts +0 -993
- package/node_modules/pi-mcp-adapter/ui-session.ts +0 -624
- package/node_modules/pi-mcp-adapter/ui-stream-types.ts +0 -89
- package/node_modules/pi-mcp-adapter/ui-tool-visibility.ts +0 -25
- package/node_modules/pi-mcp-adapter/unix-socket-transport.ts +0 -85
- package/node_modules/pi-mcp-adapter/utils.ts +0 -464
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { DEFAULT_BACKEND, TYPESAFE_KEY_ENV,
|
|
3
|
+
import { DEFAULT_BACKEND, TYPESAFE_KEY_ENV, resolveBackend, usesTypesafeKey } from "./backends.js";
|
|
4
4
|
import { credentialsPath, keySituation, keySourceLabel, piTypesafeDir } from "./credentials.js";
|
|
5
5
|
import { TypeSafeIntegrationError } from "./errors.js";
|
|
6
6
|
const AUTH_VERSION = 1;
|
|
@@ -50,7 +50,11 @@ function writeState(path, state) {
|
|
|
50
50
|
catch { /* best-effort cleanup only */ }
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* What the key situation, the last outcome, and the clock add up to for a valid backend; it never throws for one. An
|
|
55
|
+
* invalid backend throws the same `configuration` error as resolveBackend(), so validate a user-supplied endpoint
|
|
56
|
+
* with resolveBackend() first.
|
|
57
|
+
*/
|
|
54
58
|
export function authState(options = {}) {
|
|
55
59
|
const path = options.path ?? authStatePath();
|
|
56
60
|
const backend = options.backend ?? DEFAULT_BACKEND;
|
|
@@ -99,7 +103,7 @@ export function clearAuthState() {
|
|
|
99
103
|
* state instead of reporting "enabled".
|
|
100
104
|
*/
|
|
101
105
|
export function describeAuth(state = authState()) {
|
|
102
|
-
const config =
|
|
106
|
+
const config = resolveBackend(state.backend);
|
|
103
107
|
const label = `${config.label} key`;
|
|
104
108
|
const since = state.lastFailure ? ` Last failure: ${state.lastFailure.message}${state.lastFailure.at ? ` (${state.lastFailure.at})` : ""}` : "";
|
|
105
109
|
if (state.kind === "missing") {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export type TypeSafeBackend = "typesafe" | "openrouter";
|
|
1
|
+
export type TypeSafeBackend = "typesafe" | "openrouter" | "commandcode";
|
|
2
2
|
export interface BackendConfig {
|
|
3
|
-
/** Human name for status lines: "TypeSafe", "OpenRouter". */
|
|
3
|
+
/** Human name for status lines: "TypeSafe", "OpenRouter", "Command Code". */
|
|
4
4
|
label: string;
|
|
5
5
|
host: string;
|
|
6
6
|
/** The environment variable that carries this backend's key. Absent means the TypeSafe key resolution applies. */
|
|
@@ -16,11 +16,32 @@ export interface BackendConfig {
|
|
|
16
16
|
/** Whether the model list checks the key. A public list accepts any key, so it proves nothing. Absent means it does. */
|
|
17
17
|
modelsVerifyKey?: boolean;
|
|
18
18
|
}
|
|
19
|
+
/** A caller-supplied endpoint that serves the Jev decisions protocol. Passed per call; never added to the registry. */
|
|
20
|
+
export interface BackendEndpoint extends BackendConfig {
|
|
21
|
+
/** Required. The environment variable with this endpoint's key. Must not be TYPESAFE_API_KEY. */
|
|
22
|
+
readonly keyEnv: string;
|
|
23
|
+
/** Model sent when the caller names none. Without it, `model` must be passed to createTypeSafe. */
|
|
24
|
+
readonly defaultModel?: string;
|
|
25
|
+
}
|
|
26
|
+
/** A registry name or a caller-supplied endpoint. */
|
|
27
|
+
export type BackendSpec = TypeSafeBackend | BackendEndpoint;
|
|
28
|
+
/** A backend resolved and validated: what the client will actually use. */
|
|
29
|
+
export interface ResolvedBackend extends BackendConfig {
|
|
30
|
+
/** Registry name, absent for a caller-supplied endpoint. */
|
|
31
|
+
readonly name?: TypeSafeBackend;
|
|
32
|
+
/** Origin only: scheme, host, and port. */
|
|
33
|
+
readonly host: string;
|
|
34
|
+
readonly keyEnv: string;
|
|
35
|
+
/** The model id sent when the caller names none, already in the backend's form. Absent when the endpoint names none. */
|
|
36
|
+
readonly defaultModel?: string;
|
|
37
|
+
/** Always explicit after resolution. */
|
|
38
|
+
readonly modelsVerifyKey: boolean;
|
|
39
|
+
}
|
|
19
40
|
/** The backend every key and auth function assumes when none is named. */
|
|
20
41
|
export declare const DEFAULT_BACKEND: TypeSafeBackend;
|
|
21
42
|
/** The environment variable and login store that the default backend reads. */
|
|
22
43
|
export declare const TYPESAFE_KEY_ENV = "TYPESAFE_API_KEY";
|
|
23
|
-
/** Registry of known judgment backends.
|
|
44
|
+
/** Registry of known judgment backends. Callers pass a `BackendEndpoint` for a host this registry does not name. */
|
|
24
45
|
export declare const DECISIONS_BACKENDS: Record<TypeSafeBackend, BackendConfig>;
|
|
25
46
|
/** The registry entry for a backend name; a `configuration` error for a name the registry does not know. */
|
|
26
47
|
export declare function backendConfig(name: TypeSafeBackend): BackendConfig;
|
|
@@ -39,3 +60,12 @@ export declare function defaultModelId(backend: TypeSafeBackend): string;
|
|
|
39
60
|
* its own environment variable. Only the TypeSafe backend has a login store; every other backend is environment-only.
|
|
40
61
|
*/
|
|
41
62
|
export declare function usesTypesafeKey(backend: BackendConfig): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Resolve a registry name or a caller-supplied endpoint into the validated form the client uses. A name resolves to
|
|
65
|
+
* its registry entry unchanged; an endpoint is validated field by field and returned as a fresh object whose `host`
|
|
66
|
+
* is origin-only. Messages never quote a caller value — a host can carry credentials in its user info — except the
|
|
67
|
+
* label, and only after it is validated. Validation runs on every call; nothing is cached.
|
|
68
|
+
*/
|
|
69
|
+
export declare function resolveBackend(backend?: BackendSpec): ResolvedBackend;
|
|
70
|
+
/** The destination host only — scheme, host, and port, e.g. `api.commandcode.ai` — for consent text. */
|
|
71
|
+
export declare function backendHost(backend?: BackendSpec): string;
|
|
@@ -3,7 +3,7 @@ import { TypeSafeIntegrationError } from "./errors.js";
|
|
|
3
3
|
export const DEFAULT_BACKEND = "typesafe";
|
|
4
4
|
/** The environment variable and login store that the default backend reads. */
|
|
5
5
|
export const TYPESAFE_KEY_ENV = "TYPESAFE_API_KEY";
|
|
6
|
-
/** Registry of known judgment backends.
|
|
6
|
+
/** Registry of known judgment backends. Callers pass a `BackendEndpoint` for a host this registry does not name. */
|
|
7
7
|
export const DECISIONS_BACKENDS = {
|
|
8
8
|
typesafe: { label: "TypeSafe", host: "https://api.typesafe.ai", keyEnv: TYPESAFE_KEY_ENV },
|
|
9
9
|
openrouter: {
|
|
@@ -16,6 +16,16 @@ export const DECISIONS_BACKENDS = {
|
|
|
16
16
|
modelsIdField: "id",
|
|
17
17
|
modelsVerifyKey: false,
|
|
18
18
|
},
|
|
19
|
+
commandcode: {
|
|
20
|
+
label: "Command Code",
|
|
21
|
+
host: "https://api.commandcode.ai",
|
|
22
|
+
keyEnv: "COMMANDCODE_API_KEY",
|
|
23
|
+
path: "/provider/v1/systemone",
|
|
24
|
+
modelsPath: "/provider/v1/models",
|
|
25
|
+
modelsField: "data",
|
|
26
|
+
modelsIdField: "id",
|
|
27
|
+
modelsVerifyKey: false,
|
|
28
|
+
},
|
|
19
29
|
};
|
|
20
30
|
/** The registry entry for a backend name; a `configuration` error for a name the registry does not know. */
|
|
21
31
|
export function backendConfig(name) {
|
|
@@ -28,6 +38,7 @@ export function backendConfig(name) {
|
|
|
28
38
|
const DEFAULT_MODEL = {
|
|
29
39
|
typesafe: "jev-latest",
|
|
30
40
|
openrouter: "typesafe/jev-1.13",
|
|
41
|
+
commandcode: "typesafe/jev",
|
|
31
42
|
};
|
|
32
43
|
/**
|
|
33
44
|
* The model id to send for a caller's `model` on this backend. OpenRouter routes a bare Jev id under the `typesafe`
|
|
@@ -57,3 +68,86 @@ export function defaultModelId(backend) {
|
|
|
57
68
|
export function usesTypesafeKey(backend) {
|
|
58
69
|
return (backend.keyEnv ?? TYPESAFE_KEY_ENV) === TYPESAFE_KEY_ENV;
|
|
59
70
|
}
|
|
71
|
+
const KEY_ENV_MESSAGE = "Backend keyEnv must name an environment variable: letters, digits, and underscores, not starting with a digit.";
|
|
72
|
+
const HOST_MESSAGE = "Backend host must be an absolute https: URL with no user info, path, query, or fragment (http: is allowed only for localhost, 127.0.0.0/8, and [::1]).";
|
|
73
|
+
function refuse(message) {
|
|
74
|
+
throw new TypeSafeIntegrationError("configuration", message);
|
|
75
|
+
}
|
|
76
|
+
/** `https://gw.example.com?` and `https://gw.example.com#` parse clean, so the raw string itself must carry neither. */
|
|
77
|
+
function hasRawQueryOrFragment(value) {
|
|
78
|
+
return value.includes("?") || value.includes("#");
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Resolve a registry name or a caller-supplied endpoint into the validated form the client uses. A name resolves to
|
|
82
|
+
* its registry entry unchanged; an endpoint is validated field by field and returned as a fresh object whose `host`
|
|
83
|
+
* is origin-only. Messages never quote a caller value — a host can carry credentials in its user info — except the
|
|
84
|
+
* label, and only after it is validated. Validation runs on every call; nothing is cached.
|
|
85
|
+
*/
|
|
86
|
+
export function resolveBackend(backend) {
|
|
87
|
+
const spec = backend === undefined ? DEFAULT_BACKEND : backend;
|
|
88
|
+
if (typeof spec === "string") {
|
|
89
|
+
const entry = backendConfig(spec);
|
|
90
|
+
return {
|
|
91
|
+
...entry,
|
|
92
|
+
name: spec,
|
|
93
|
+
host: entry.host,
|
|
94
|
+
keyEnv: entry.keyEnv ?? TYPESAFE_KEY_ENV,
|
|
95
|
+
defaultModel: defaultModelId(spec),
|
|
96
|
+
modelsVerifyKey: entry.modelsVerifyKey !== false,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (spec === null || typeof spec !== "object") {
|
|
100
|
+
refuse("backend must be a registry name or a backend object.");
|
|
101
|
+
}
|
|
102
|
+
const label = typeof spec.label === "string" ? spec.label.trim() : "";
|
|
103
|
+
if (!label || label.length > 60)
|
|
104
|
+
refuse("Backend label must be a nonempty string of at most 60 characters.");
|
|
105
|
+
let host;
|
|
106
|
+
if (typeof spec.host !== "string" || hasRawQueryOrFragment(spec.host))
|
|
107
|
+
refuse(HOST_MESSAGE);
|
|
108
|
+
try {
|
|
109
|
+
host = new URL(spec.host);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
refuse(HOST_MESSAGE);
|
|
113
|
+
}
|
|
114
|
+
const loopback = host.hostname === "localhost" || host.hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(host.hostname);
|
|
115
|
+
if (host.protocol !== "https:" && !(host.protocol === "http:" && loopback))
|
|
116
|
+
refuse(HOST_MESSAGE);
|
|
117
|
+
if (host.username !== "" || host.password !== "" || host.pathname !== "/" || host.search !== "" || host.hash !== "")
|
|
118
|
+
refuse(HOST_MESSAGE);
|
|
119
|
+
for (const [field, message] of [["path", 'Backend path must be a string that starts with "/".'], ["modelsPath", 'Backend modelsPath must be a string that starts with "/".']]) {
|
|
120
|
+
const value = spec[field];
|
|
121
|
+
if (value !== undefined && (typeof value !== "string" || !value.startsWith("/") || hasRawQueryOrFragment(value)))
|
|
122
|
+
refuse(message);
|
|
123
|
+
}
|
|
124
|
+
if (spec.modelsField !== undefined && (typeof spec.modelsField !== "string" || !spec.modelsField))
|
|
125
|
+
refuse("Backend modelsField must be a nonempty string.");
|
|
126
|
+
if (spec.modelsIdField !== undefined && (typeof spec.modelsIdField !== "string" || !spec.modelsIdField))
|
|
127
|
+
refuse("Backend modelsIdField must be a nonempty string.");
|
|
128
|
+
if (spec.modelsVerifyKey !== undefined && typeof spec.modelsVerifyKey !== "boolean")
|
|
129
|
+
refuse("Backend modelsVerifyKey must be a boolean.");
|
|
130
|
+
if (typeof spec.keyEnv !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.keyEnv))
|
|
131
|
+
refuse(KEY_ENV_MESSAGE);
|
|
132
|
+
if (spec.keyEnv.toUpperCase() === TYPESAFE_KEY_ENV) {
|
|
133
|
+
refuse("Backend keyEnv must not be TYPESAFE_API_KEY: the TypeSafe key is only sent to the typesafe backend. Give this endpoint its own variable.");
|
|
134
|
+
}
|
|
135
|
+
if (spec.defaultModel !== undefined && (typeof spec.defaultModel !== "string" || !spec.defaultModel.trim() || spec.defaultModel.length > 100)) {
|
|
136
|
+
refuse("Backend defaultModel must be a nonempty string of at most 100 characters.");
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
label,
|
|
140
|
+
host: host.origin,
|
|
141
|
+
keyEnv: spec.keyEnv,
|
|
142
|
+
modelsVerifyKey: spec.modelsVerifyKey === true,
|
|
143
|
+
...(spec.path === undefined ? {} : { path: spec.path }),
|
|
144
|
+
...(spec.modelsPath === undefined ? {} : { modelsPath: spec.modelsPath }),
|
|
145
|
+
...(spec.modelsField === undefined ? {} : { modelsField: spec.modelsField }),
|
|
146
|
+
...(spec.modelsIdField === undefined ? {} : { modelsIdField: spec.modelsIdField }),
|
|
147
|
+
...(spec.defaultModel === undefined ? {} : { defaultModel: spec.defaultModel }),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** The destination host only — scheme, host, and port, e.g. `api.commandcode.ai` — for consent text. */
|
|
151
|
+
export function backendHost(backend) {
|
|
152
|
+
return new URL(resolveBackend(backend).host).host;
|
|
153
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { Fetch, Questions, SystemOneRequest, SystemOneResult } from "@typesafe-ai/sdk";
|
|
2
|
-
import type {
|
|
2
|
+
import type { BackendSpec } from "./backends.js";
|
|
3
3
|
import type { BatchEvaluation, BatchOptions } from "./batch.js";
|
|
4
4
|
import type { BlockedCap, SpendCaps, UsageLedger, UsageReport } from "./usage.js";
|
|
5
|
-
export { DECISIONS_BACKENDS, DEFAULT_BACKEND } from "./backends.js";
|
|
6
|
-
export type { BackendConfig, TypeSafeBackend } from "./backends.js";
|
|
5
|
+
export { backendHost, resolveBackend, DECISIONS_BACKENDS, DEFAULT_BACKEND } from "./backends.js";
|
|
6
|
+
export type { BackendConfig, BackendEndpoint, BackendSpec, ResolvedBackend, TypeSafeBackend } from "./backends.js";
|
|
7
7
|
export interface TypeSafeOptions {
|
|
8
8
|
/** Defaults to TYPESAFE_API_KEY, then the key saved by `/typesafe login`; never returned. */
|
|
9
9
|
apiKey?: string;
|
|
10
|
-
/** Judgment backend. When omitted, routes to the default TypeSafe host. */
|
|
11
|
-
backend?:
|
|
10
|
+
/** Judgment backend: a registry name or a caller-supplied endpoint. When omitted, routes to the default TypeSafe host. */
|
|
11
|
+
backend?: BackendSpec;
|
|
12
12
|
/** Defaults to the backend's own default (`jev-latest`, `typesafe/jev-1.13` on OpenRouter); a bare Jev id is mapped to the backend's id form before sending. No model is inferred from submitted content. */
|
|
13
13
|
model?: string;
|
|
14
14
|
/** Per request. Default: 15 seconds. No automatic retries. */
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { TypeSafeClient } from "@typesafe-ai/sdk";
|
|
2
2
|
import { recordAuthFailure, recordAuthVerified } from "./auth.js";
|
|
3
|
-
import {
|
|
3
|
+
import { TYPESAFE_KEY_ENV, backendModelId, resolveBackend, usesTypesafeKey } from "./backends.js";
|
|
4
4
|
import { evaluateAll, evaluateMany } from "./batch.js";
|
|
5
5
|
import { keySituation } from "./credentials.js";
|
|
6
6
|
import { TypeSafeIntegrationError, safeError } from "./errors.js";
|
|
7
7
|
import { DEFAULT_MAX_INPUT_BYTES, assertWithinByteLimit, prepareEvaluationRequest } from "./schema.js";
|
|
8
8
|
import { DEFAULT_USD_PER_MTOK, capsFromEnvironment, estimateUsd, mergeCaps, openUsageLedger } from "./usage.js";
|
|
9
|
-
export { DECISIONS_BACKENDS, DEFAULT_BACKEND } from "./backends.js";
|
|
9
|
+
export { backendHost, resolveBackend, DECISIONS_BACKENDS, DEFAULT_BACKEND } from "./backends.js";
|
|
10
10
|
/** The paths the SDK appends to whatever base URL it is given. */
|
|
11
11
|
const SDK_PATH = "/v1/systemone";
|
|
12
12
|
const SDK_MODELS_PATH = "/v1/models";
|
|
@@ -118,13 +118,14 @@ function capsDescription(caps) {
|
|
|
118
118
|
}
|
|
119
119
|
/** A bounded, server-side TypeSafe client independent of Pi's runtime. */
|
|
120
120
|
export function createTypeSafe(options = {}) {
|
|
121
|
+
const backend = resolveBackend(options.backend);
|
|
121
122
|
let apiKey = options.apiKey?.trim();
|
|
122
|
-
const backendName = options.backend ?? DEFAULT_BACKEND;
|
|
123
|
-
const backend = backendConfig(backendName);
|
|
124
123
|
const baseURL = backend.host;
|
|
124
|
+
// Only a registry backend maps ids; a caller-supplied endpoint's model is sent as the caller wrote it.
|
|
125
|
+
const mapModel = (model) => (backend.name === undefined ? model : backendModelId(backend.name, model));
|
|
125
126
|
if (!apiKey) {
|
|
126
127
|
// The same resolution that authState() and ensureApiKey() report, so the status line and the request agree.
|
|
127
|
-
const situation = keySituation(
|
|
128
|
+
const situation = keySituation(options.backend);
|
|
128
129
|
if (situation.kind === "unusable")
|
|
129
130
|
throw new TypeSafeIntegrationError("configuration", situation.reason);
|
|
130
131
|
if (situation.kind === "environment" || situation.kind === "stored")
|
|
@@ -148,10 +149,12 @@ export function createTypeSafe(options = {}) {
|
|
|
148
149
|
const transport = backend.path !== undefined || backend.modelsPath !== undefined ? backendFetch(backend, options.fetch) : options.fetch;
|
|
149
150
|
// The caller's input is validated as written, then mapped to the backend's id form; omitting it sends the backend's
|
|
150
151
|
// own default, which the mapping leaves unchanged.
|
|
151
|
-
const requested = options.model ??
|
|
152
|
+
const requested = options.model ?? backend.defaultModel;
|
|
153
|
+
if (requested === undefined)
|
|
154
|
+
throw new TypeSafeIntegrationError("configuration", `Backend "${backend.label}" names no defaultModel; pass model to createTypeSafe.`);
|
|
152
155
|
if (typeof requested !== "string" || !requested.trim() || requested.length > 100)
|
|
153
156
|
throw new TypeSafeIntegrationError("configuration", "model must be a nonempty string of at most 100 characters.");
|
|
154
|
-
const model =
|
|
157
|
+
const model = mapModel(requested);
|
|
155
158
|
// Do not inherit SDK debug logging or alternate destinations from the environment.
|
|
156
159
|
const client = new TypeSafeClient({
|
|
157
160
|
apiKey,
|
|
@@ -187,7 +190,7 @@ export function createTypeSafe(options = {}) {
|
|
|
187
190
|
if (!Array.isArray(models))
|
|
188
191
|
throw new TypeSafeIntegrationError("response", "TypeSafe returned an unexpected model list.");
|
|
189
192
|
// A backend that serves its list publicly accepts any key, so a success there proves nothing about one.
|
|
190
|
-
if (backend.modelsVerifyKey
|
|
193
|
+
if (backend.modelsVerifyKey && !verificationRecorded) {
|
|
191
194
|
verificationRecorded = true;
|
|
192
195
|
recordAuthVerified();
|
|
193
196
|
}
|
|
@@ -200,7 +203,7 @@ export function createTypeSafe(options = {}) {
|
|
|
200
203
|
async evaluate(input, callOptions = {}) {
|
|
201
204
|
const validated = prepareEvaluationRequest(input, { maxInputBytes });
|
|
202
205
|
// A per-request model meets the same mapping as the client default; the schema already limited the caller's own id.
|
|
203
|
-
const body = JSON.stringify({ ...validated, model: validated.model === undefined ? model :
|
|
206
|
+
const body = JSON.stringify({ ...validated, model: validated.model === undefined ? model : mapModel(validated.model) });
|
|
204
207
|
assertWithinByteLimit(body, maxInputBytes);
|
|
205
208
|
if (callOptions.signal?.aborted)
|
|
206
209
|
throw new TypeSafeIntegrationError("aborted", "TypeSafe request cancelled before submission.");
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { BackendSpec } from "./backends.js";
|
|
2
2
|
export type KeySource = "environment" | "stored";
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* The complete answer to "which key is in effect" for a valid backend: it never throws. An invalid backend — an
|
|
5
|
+
* unknown name or an endpoint object that fails validation — throws the same `configuration` error as resolveBackend.
|
|
6
|
+
*/
|
|
4
7
|
export type KeySituation =
|
|
5
8
|
/** `keyEnv` names the variable that was read; absent means `TYPESAFE_API_KEY`. */
|
|
6
9
|
{
|
|
@@ -30,21 +33,24 @@ export declare function normalizeApiKey(value: unknown): string;
|
|
|
30
33
|
*/
|
|
31
34
|
export declare function readStoredApiKey(): string | undefined;
|
|
32
35
|
/**
|
|
33
|
-
* What the environment, the login store, and file permissions add up to right now for one judgment backend.
|
|
34
|
-
* throws
|
|
36
|
+
* What the environment, the login store, and file permissions add up to right now for one judgment backend. A valid
|
|
37
|
+
* backend never throws: the "unusable" kind carries the user-facing reason (a stored key that other local users can
|
|
38
|
+
* read). An invalid backend — an unknown name or an endpoint object that fails validation — throws the same
|
|
39
|
+
* `configuration` error as resolveBackend(), so validate a user-supplied endpoint with resolveBackend() first.
|
|
35
40
|
* The TypeSafe backend reads `TYPESAFE_API_KEY`, then the login store. Every other backend reads only its own
|
|
36
41
|
* environment variable, because the store holds a TypeSafe key and a login verifies against api.typesafe.ai.
|
|
37
42
|
*/
|
|
38
|
-
export declare function keySituation(backend?:
|
|
43
|
+
export declare function keySituation(backend?: BackendSpec): KeySituation;
|
|
39
44
|
/** Short phrase naming the source; full sentences stay with the caller. */
|
|
40
45
|
export declare function keySourceLabel(situation: KeySituation): string;
|
|
41
46
|
/**
|
|
42
47
|
* The pre-0.4.0 key interface, frozen for existing callers: environment first so CI and scripts stay explicit, the
|
|
43
48
|
* stored key as the interactive default, `undefined` when no key is configured, and a `configuration` error when a
|
|
44
|
-
* store must not be read. New code should call keySituation() instead: same precedence, never throws
|
|
45
|
-
* "must not be read" case arrives as
|
|
49
|
+
* store must not be read. New code should call keySituation() instead: same precedence, never throws for a valid
|
|
50
|
+
* backend (an invalid one throws resolveBackend's `configuration` error), and the "must not be read" case arrives as
|
|
51
|
+
* `unusable` with the reason.
|
|
46
52
|
*/
|
|
47
|
-
export declare function resolveApiKey(backend?:
|
|
53
|
+
export declare function resolveApiKey(backend?: BackendSpec): {
|
|
48
54
|
key: string;
|
|
49
55
|
source: KeySource;
|
|
50
56
|
} | undefined;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
-
import { DEFAULT_BACKEND, TYPESAFE_KEY_ENV,
|
|
4
|
+
import { DEFAULT_BACKEND, TYPESAFE_KEY_ENV, resolveBackend, usesTypesafeKey } from "./backends.js";
|
|
5
5
|
import { TypeSafeIntegrationError } from "./errors.js";
|
|
6
6
|
/** Mirrors Pi's agent directory rule; every pi-typesafe file lives in this one directory. */
|
|
7
7
|
export function piTypesafeDir() {
|
|
@@ -45,14 +45,16 @@ export function readStoredApiKey() {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
|
-
* What the environment, the login store, and file permissions add up to right now for one judgment backend.
|
|
49
|
-
* throws
|
|
48
|
+
* What the environment, the login store, and file permissions add up to right now for one judgment backend. A valid
|
|
49
|
+
* backend never throws: the "unusable" kind carries the user-facing reason (a stored key that other local users can
|
|
50
|
+
* read). An invalid backend — an unknown name or an endpoint object that fails validation — throws the same
|
|
51
|
+
* `configuration` error as resolveBackend(), so validate a user-supplied endpoint with resolveBackend() first.
|
|
50
52
|
* The TypeSafe backend reads `TYPESAFE_API_KEY`, then the login store. Every other backend reads only its own
|
|
51
53
|
* environment variable, because the store holds a TypeSafe key and a login verifies against api.typesafe.ai.
|
|
52
54
|
*/
|
|
53
55
|
export function keySituation(backend = DEFAULT_BACKEND) {
|
|
54
|
-
const config =
|
|
55
|
-
const keyEnv = config.keyEnv
|
|
56
|
+
const config = resolveBackend(backend);
|
|
57
|
+
const keyEnv = config.keyEnv;
|
|
56
58
|
const fromEnvironment = process.env[keyEnv]?.trim();
|
|
57
59
|
if (fromEnvironment)
|
|
58
60
|
return { kind: "environment", key: fromEnvironment, keyEnv };
|
|
@@ -82,8 +84,9 @@ export function keySourceLabel(situation) {
|
|
|
82
84
|
/**
|
|
83
85
|
* The pre-0.4.0 key interface, frozen for existing callers: environment first so CI and scripts stay explicit, the
|
|
84
86
|
* stored key as the interactive default, `undefined` when no key is configured, and a `configuration` error when a
|
|
85
|
-
* store must not be read. New code should call keySituation() instead: same precedence, never throws
|
|
86
|
-
* "must not be read" case arrives as
|
|
87
|
+
* store must not be read. New code should call keySituation() instead: same precedence, never throws for a valid
|
|
88
|
+
* backend (an invalid one throws resolveBackend's `configuration` error), and the "must not be read" case arrives as
|
|
89
|
+
* `unusable` with the reason.
|
|
87
90
|
*/
|
|
88
91
|
export function resolveApiKey(backend = DEFAULT_BACKEND) {
|
|
89
92
|
const situation = keySituation(backend);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BackendConfig,
|
|
1
|
+
import type { BackendConfig, BackendSpec } from "./backends.js";
|
|
2
2
|
export type IntegrationErrorCode = "configuration" | "validation" | "budget" | "aborted" | "timeout" | "http" | "connection" | "response";
|
|
3
3
|
/**
|
|
4
4
|
* Safe to display: never contains upstream bodies, keys, or submitted state. The only header value a message can
|
|
@@ -14,4 +14,4 @@ export declare class TypeSafeIntegrationError extends Error {
|
|
|
14
14
|
* Classify an error into a message safe to display. `backend` names the key variable the 401 advice tells the user
|
|
15
15
|
* to check and selects the 402 wording; omitting it keeps the one-argument call and assumes the default TypeSafe key.
|
|
16
16
|
*/
|
|
17
|
-
export declare function safeError(error: unknown, backend?:
|
|
17
|
+
export declare function safeError(error: unknown, backend?: BackendSpec | BackendConfig): TypeSafeIntegrationError;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { createTypeSafe, DEFAULT_MAX_REQUESTS, DECISIONS_BACKENDS, DEFAULT_BACKEND } from "./client.js";
|
|
2
|
-
export type { TypeSafe, TypeSafeOptions, EvaluationOptions, Evaluation, UsageSnapshot, SpendReport, TypeSafeBackend, BackendConfig, } from "./client.js";
|
|
1
|
+
export { createTypeSafe, backendHost, resolveBackend, DEFAULT_MAX_REQUESTS, DECISIONS_BACKENDS, DEFAULT_BACKEND } from "./client.js";
|
|
2
|
+
export type { TypeSafe, TypeSafeOptions, EvaluationOptions, Evaluation, UsageSnapshot, SpendReport, TypeSafeBackend, BackendConfig, BackendEndpoint, BackendSpec, ResolvedBackend, } from "./client.js";
|
|
3
3
|
export { ask, DEFAULT_ASK_TIMEOUT_MS } from "./ask.js";
|
|
4
4
|
export type { AskAnswer, AskOptions, Judge } from "./ask.js";
|
|
5
5
|
export { fanOut, DEFAULT_CONCURRENCY, evaluateMany, evaluateAll, chunkEvaluationRequest } from "./batch.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createTypeSafe, DEFAULT_MAX_REQUESTS, DECISIONS_BACKENDS, DEFAULT_BACKEND } from "./client.js";
|
|
1
|
+
export { createTypeSafe, backendHost, resolveBackend, DEFAULT_MAX_REQUESTS, DECISIONS_BACKENDS, DEFAULT_BACKEND } from "./client.js";
|
|
2
2
|
export { ask, DEFAULT_ASK_TIMEOUT_MS } from "./ask.js";
|
|
3
3
|
export { fanOut, DEFAULT_CONCURRENCY, evaluateMany, evaluateAll, chunkEvaluationRequest } from "./batch.js";
|
|
4
4
|
export { authState, authStatePath, clearAuthState, describeAuth, recordAuthFailure, recordAuthVerified } from "./auth.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type {
|
|
2
|
+
import type { BackendSpec } from "./backends.js";
|
|
3
3
|
import type { KeySource } from "./credentials.js";
|
|
4
4
|
export interface LoginResult {
|
|
5
5
|
/** Where the verified key was saved. */
|
|
@@ -29,5 +29,5 @@ export type EnsureApiKeyResult = {
|
|
|
29
29
|
* the prompt would verify the key against the wrong service and store it where the TypeSafe key lives.
|
|
30
30
|
*/
|
|
31
31
|
export declare function ensureApiKey(ctx: ExtensionCommandContext, options?: {
|
|
32
|
-
backend?:
|
|
32
|
+
backend?: BackendSpec;
|
|
33
33
|
}): Promise<EnsureApiKeyResult | undefined>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_BACKEND,
|
|
1
|
+
import { DEFAULT_BACKEND, resolveBackend, usesTypesafeKey } from "./backends.js";
|
|
2
2
|
import { createTypeSafe } from "./client.js";
|
|
3
3
|
import { keySituation, normalizeApiKey, storeApiKey } from "./credentials.js";
|
|
4
4
|
import { TypeSafeIntegrationError } from "./errors.js";
|
|
@@ -38,7 +38,7 @@ export async function ensureApiKey(ctx, options = {}) {
|
|
|
38
38
|
return { source: situation.kind };
|
|
39
39
|
if (situation.kind === "unusable")
|
|
40
40
|
throw new TypeSafeIntegrationError("configuration", situation.reason);
|
|
41
|
-
const config =
|
|
41
|
+
const config = resolveBackend(backend);
|
|
42
42
|
if (!usesTypesafeKey(config)) {
|
|
43
43
|
throw new TypeSafeIntegrationError("configuration", `No ${config.label} key. Set ${config.keyEnv} in the environment; /typesafe login stores a TypeSafe key only.`);
|
|
44
44
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-typesafe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "TypeSafe AI (Jev) decisions for Pi: batched Choice/Score/Noul evaluation tool, terminal playground, and a typed API other extensions build on.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -64,12 +64,12 @@
|
|
|
64
64
|
"prepack": "npm run build"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@typesafe-ai/sdk": "^0.6.0"
|
|
68
|
-
"typebox": "^1.3.31"
|
|
67
|
+
"@typesafe-ai/sdk": "^0.6.0"
|
|
69
68
|
},
|
|
70
69
|
"peerDependencies": {
|
|
71
|
-
"@earendil-works/pi-coding-agent": "
|
|
72
|
-
"@earendil-works/pi-tui": "
|
|
70
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
71
|
+
"@earendil-works/pi-tui": "*",
|
|
72
|
+
"typebox": "*"
|
|
73
73
|
},
|
|
74
74
|
"peerDependenciesMeta": {
|
|
75
75
|
"@earendil-works/pi-coding-agent": {
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
"@earendil-works/pi-tui": "^0.86.1",
|
|
85
85
|
"@types/node": "^22.0.0",
|
|
86
86
|
"tsx": "^4.23.13",
|
|
87
|
+
"typebox": "^1.3.31",
|
|
87
88
|
"typescript": "^7.0.2"
|
|
88
89
|
}
|
|
89
90
|
}
|
|
@@ -4,6 +4,78 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.34.0] - 2026-09-30
|
|
8
|
+
|
|
9
|
+
### Highlights
|
|
10
|
+
|
|
11
|
+
- Turning on web tools no longer costs a prompt-cache miss on models like DeepSeek. New sessions now pick the activation style that suits the model.
|
|
12
|
+
- Search with Z.ai's GLM Coding Plan and use your plan quota instead of paying per call.
|
|
13
|
+
- `fetch_content` now gets past Cloudflare "Just a moment..." pages by handing off to your other fetch providers.
|
|
14
|
+
- Fetching works on networks where only your configured proxy can resolve hostnames.
|
|
15
|
+
- `undici` is updated to pick up fixes for known security advisories.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Search with Z.ai's GLM Coding Plan web search when you select `provider: "zai"`. Set `ZAI_API_KEY` or `zaiApiKey` to your plan key, and set `zaiEndpoint: "china"` if your key comes from bigmodel.cn. Searches use your plan quota instead of per-call billing. Thanks to [@nailuoGG](https://github.com/nailuoGG) for [issue #475](https://github.com/nicobailon/pi-web-access/issues/475) and for live-testing the China endpoint in [issue #479](https://github.com/nicobailon/pi-web-access/issues/479).
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- `toolActivation` now defaults to `"auto"`. On models that can take tools added mid-conversation, new sessions start with `web_enable` as before. On other models, such as DeepSeek, every enabled web tool is available from the first request, so turning them on no longer makes Pi resend the whole conversation and miss the prompt cache. Set `"dynamic"` to always start with `web_enable`, or `"eager"` to never use it. Resumed sessions keep the tools they already had. The README's Tool activation section explains the trade-off. Thanks to [@tinoy1336](https://github.com/tinoy1336) for [issue #484](https://github.com/nicobailon/pi-web-access/issues/484) and [@jordi9](https://github.com/jordi9) for [issue #481](https://github.com/nicobailon/pi-web-access/issues/481).
|
|
24
|
+
- Keyed Exa searches with default options or `numResults: 5` now call Exa's `/search` endpoint instead of `/answer`, like every other keyed Exa search. Their answer text now comes from the search results instead of Exa's generated answer. If you route Exa through a custom `exaBaseUrl` gateway, it must support `/search`. Thanks to [@SuTang-vain](https://github.com/SuTang-vain) for [issue #470](https://github.com/nicobailon/pi-web-access/issues/470).
|
|
25
|
+
- When `fetch_content` can't get a page, its list of fallback options now explains how to turn on the keyless Jina Reader fallback. It names only the setting you still need and keeps your current or default provider order. The hint doesn't appear if Jina already ran for that fetch, and it warns that Jina's servers fetch the target URLs. Thanks to [@SuTang-vain](https://github.com/SuTang-vain) for [PR #471](https://github.com/nicobailon/pi-web-access/pull/471).
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- `fetch_content` now treats a Cloudflare "Just a moment..." challenge page returned with HTTP 200 as a failed fetch, so your configured fallback providers can retrieve the real page. Detection needs Cloudflare's `cf-mitigated: challenge` header or its challenge-page scripts, so a page that only says "Just a moment..." is unaffected. Raw mode still returns the response unchanged, and authenticated fetches report the challenge without falling back to other providers. Thanks to [@SuTang-vain](https://github.com/SuTang-vain) for [issue #472](https://github.com/nicobailon/pi-web-access/issues/472).
|
|
30
|
+
- With `ssrf.trustEnvProxy: true`, the `proxy` configured in `web-search.json` now resolves hostnames for web tools instead of your local DNS, so `fetch_content` works where only the proxy can resolve names. A per-call `proxy` with a different value is still checked against local DNS. When local resolution fails for a request going through the configured proxy, the error now names this setting. Thanks to [@ChenAuCarre](https://github.com/ChenAuCarre) for [issue #476](https://github.com/nicobailon/pi-web-access/issues/476).
|
|
31
|
+
- Exa results without a title are now labeled with their site's hostname, such as `cdn.jsdelivr.net`, instead of `Source N`. Results whose URL has no hostname, such as `mailto:` or `file:` links, still use `Source N`. Thanks to [@SuTang-vain](https://github.com/SuTang-vain) for [PR #469](https://github.com/nicobailon/pi-web-access/pull/469).
|
|
32
|
+
- `undici` is updated to 8.11.2. Earlier 8.x releases, including the 8.10.0 this package previously installed, have known security advisories such as decompression denial of service and a TLS certificate validation bypass. Thanks to [@setanta00](https://github.com/setanta00) for [issue #483](https://github.com/nicobailon/pi-web-access/issues/483).
|
|
33
|
+
|
|
34
|
+
## [0.33.0] - 2026-09-27
|
|
35
|
+
|
|
36
|
+
### Highlights
|
|
37
|
+
|
|
38
|
+
- Keep every web tool available from the first message with `"toolActivation": "eager"`, for models that rarely call `web_enable` on their own.
|
|
39
|
+
- Search with You.com when you select it explicitly.
|
|
40
|
+
- Give each Pi process its own fetched-page cache so parallel sessions stop evicting each other's pages.
|
|
41
|
+
- Model allowlists in `enabledModels` or `--models` now work with every pattern Pi accepts.
|
|
42
|
+
- Resumed older sessions keep the tools they had instead of gaining `web_enable`.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- Set `"toolActivation": "eager"` in `web-search.json` to skip `web_enable` and keep every enabled web tool available from the first message. With the default `"dynamic"` setting, `web_enable` now tells the model to call it first whenever current, external, or linked information could help. Thanks to [@ackalker](https://github.com/ackalker) for [issue #458](https://github.com/nicobailon/pi-web-access/issues/458).
|
|
47
|
+
- Set `PI_WEB_ACCESS_CACHE_ROOT` to a directory to give a Pi process its own fetched-page cache. The cache goes in a `web-search-cache` folder inside that directory, and `web-search.json` stays where it is. Parallel sessions then stop evicting each other's cached pages. Thanks to [@goodman-b](https://github.com/goodman-b) for [issue #457](https://github.com/nicobailon/pi-web-access/issues/457).
|
|
48
|
+
- You.com is available as an optional search provider. Set `youApiKey` or `YDC_API_KEY`, then select `provider: "you"`, add it to a provider list, or list it in `searchRouting`. It also appears in the Curator. `auto` and `provider: "all"` never pick it. Thanks to [@mouse-value-add](https://github.com/mouse-value-add) for [PR #461](https://github.com/nicobailon/pi-web-access/pull/461).
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- Resuming a session that never had `web_enable`, for example one started before pi-web-access was installed or upgraded, no longer adds it partway through the conversation. The session keeps the tools it already had. Thanks to [@nilsoskar](https://github.com/nilsoskar) for [issue #462](https://github.com/nicobailon/pi-web-access/issues/462).
|
|
53
|
+
- Answer mode, query rewrite, and summaries now follow the model list Pi builds from `enabledModels` or `--models`, so every pattern Pi accepts works here too, including patterns such as `@(!(openai-codex))/*` and partial names such as `sonnet`. Before, an allowlist made of those patterns blocked every model. Thanks to [@IdrisGit](https://github.com/IdrisGit) for [issue #463](https://github.com/nicobailon/pi-web-access/issues/463) and [PR #464](https://github.com/nicobailon/pi-web-access/pull/464).
|
|
54
|
+
- `web_enable` now depends on the version of Pi you are running, not an older Pi package that may be installed next to the extension, so those setups no longer lose it. It needs Pi 0.86.0 or newer. On older Pi, every enabled web tool stays available from the start. Thanks to [@PhrZer](https://github.com/PhrZer) for [PR #456](https://github.com/nicobailon/pi-web-access/pull/456) and [@nguyenchiencong](https://github.com/nguyenchiencong) for [issue #444](https://github.com/nicobailon/pi-web-access/issues/444).
|
|
55
|
+
|
|
56
|
+
## [0.32.0] - 2026-09-26
|
|
57
|
+
|
|
58
|
+
### Highlights
|
|
59
|
+
|
|
60
|
+
- Spread Tavily searches across up to 20 API keys, with automatic failover when one runs out of quota.
|
|
61
|
+
- Use Gemini Web browser cookies on KDE Linux, where Chrome keeps its password in KWallet.
|
|
62
|
+
- Search with OpenAI through OpenCode and `opencode-go` without session errors or the wrong model.
|
|
63
|
+
- See full search queries and URLs in tool-call labels.
|
|
64
|
+
- Globally installed Pi no longer shows false version or duplicate TypeBox warnings.
|
|
65
|
+
|
|
66
|
+
### Added
|
|
67
|
+
|
|
68
|
+
- Tavily can use a pool of up to 20 API keys. Set `TAVILY_API_KEY_1` through `TAVILY_API_KEY_20`, and when a key hits a quota or auth error the search moves on to the next one. `TAVILY_API_KEY_INDEX` picks the key to try first, and `TAVILY_API_KEY` / `tavilyApiKey` is tried last. Thanks to [@apoapostolov](https://github.com/apoapostolov) for [PR #430](https://github.com/nicobailon/pi-web-access/pull/430).
|
|
69
|
+
|
|
70
|
+
### Fixed
|
|
71
|
+
|
|
72
|
+
- On KDE Linux, Chrome and Chromium cookies for Gemini Web now decrypt with the password stored in KWallet when Secret Service doesn't have it. Thanks to [@dianzuan](https://github.com/dianzuan) for [issue #436](https://github.com/nicobailon/pi-web-access/issues/436).
|
|
73
|
+
- OpenAI search requests to `opencode.ai` now include OpenCode's session headers, fixing `400 MissingSessionID` errors. Other destinations never receive these headers. Thanks to [@juliocc](https://github.com/juliocc) for [PR #445](https://github.com/nicobailon/pi-web-access/pull/445).
|
|
74
|
+
- When `openaiSearchModel` isn't set, OpenAI search through gateways such as `opencode-go` now picks the newest GPT model instead of a non-OpenAI model.
|
|
75
|
+
- Globally installed Pi no longer shows a false "requires Pi 0.86.1 or newer" warning, and dynamic tool activation now works there. Thanks to [@samsimsom](https://github.com/samsimsom) for [issue #428](https://github.com/nicobailon/pi-web-access/issues/428).
|
|
76
|
+
- TypeBox now comes from Pi instead of being installed separately, which avoids duplicate copies and Pi startup warnings. Thanks to [@ksreenivasan](https://github.com/ksreenivasan) for [issue #442](https://github.com/nicobailon/pi-web-access/issues/442) and [PR #443](https://github.com/nicobailon/pi-web-access/pull/443).
|
|
77
|
+
- Tool-call labels show full search queries and fetch URLs when the terminal is wide enough, instead of cutting them off at 60 characters. Thanks to [@TheBestPessimist](https://github.com/TheBestPessimist) for [issue #440](https://github.com/nicobailon/pi-web-access/issues/440).
|
|
78
|
+
|
|
7
79
|
## [0.31.0] - 2026-09-22
|
|
8
80
|
|
|
9
81
|
### Highlights
|