@pyric/cli 0.1.0-alpha.20 → 0.1.0-alpha.21
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 +64 -89
- package/dist/assurance/.generated/conformance-verdicts.d.ts +68 -1
- package/dist/assurance/.generated/conformance-verdicts.d.ts.map +1 -1
- package/dist/assurance/.generated/conformance-verdicts.js +68 -1
- package/dist/assurance/.generated/conformance-verdicts.js.map +1 -1
- package/dist/bridge/client/dispatch.d.ts +25 -19
- package/dist/bridge/client/dispatch.d.ts.map +1 -1
- package/dist/bridge/client/dispatch.js +41 -48
- package/dist/bridge/client/dispatch.js.map +1 -1
- package/dist/bridge/client/tool-family-factories.d.ts +26 -0
- package/dist/bridge/client/tool-family-factories.d.ts.map +1 -0
- package/dist/bridge/client/tool-family-factories.js +10 -0
- package/dist/bridge/client/tool-family-factories.js.map +1 -0
- package/dist/bridge/server/mcp-contract.d.ts +7 -6
- package/dist/bridge/server/mcp-contract.d.ts.map +1 -1
- package/dist/bridge/server/mcp-contract.js +9 -48
- package/dist/bridge/server/mcp-contract.js.map +1 -1
- package/dist/bridge/server/peer.d.ts +11 -1
- package/dist/bridge/server/peer.d.ts.map +1 -1
- package/dist/bridge/server/peer.js +22 -1
- package/dist/bridge/server/peer.js.map +1 -1
- package/dist/bridge/server/tool-family-factories.d.ts +30 -0
- package/dist/bridge/server/tool-family-factories.d.ts.map +1 -0
- package/dist/bridge/server/tool-family-factories.js +22 -0
- package/dist/bridge/server/tool-family-factories.js.map +1 -0
- package/dist/bridge/server/tool-metadata.d.ts +17 -18
- package/dist/bridge/server/tool-metadata.d.ts.map +1 -1
- package/dist/bridge/server/tool-metadata.js +24 -59
- package/dist/bridge/server/tool-metadata.js.map +1 -1
- package/dist/bridge/tool-families.d.ts +48 -0
- package/dist/bridge/tool-families.d.ts.map +1 -0
- package/dist/bridge/tool-families.generated.d.ts +32 -0
- package/dist/bridge/tool-families.generated.d.ts.map +1 -0
- package/dist/bridge/tool-families.generated.js +15 -0
- package/dist/bridge/tool-families.generated.js.map +1 -0
- package/dist/bridge/tool-families.js +51 -0
- package/dist/bridge/tool-families.js.map +1 -0
- package/dist/bridge/tool-family-records/conformance.d.ts +7 -0
- package/dist/bridge/tool-family-records/conformance.d.ts.map +1 -0
- package/dist/bridge/tool-family-records/conformance.js +6 -0
- package/dist/bridge/tool-family-records/conformance.js.map +1 -0
- package/dist/bridge/tool-family-records/firestore-data.d.ts +7 -0
- package/dist/bridge/tool-family-records/firestore-data.d.ts.map +1 -0
- package/dist/bridge/tool-family-records/firestore-data.js +15 -0
- package/dist/bridge/tool-family-records/firestore-data.js.map +1 -0
- package/dist/bridge/tool-family-records/firestore-inspect.d.ts +7 -0
- package/dist/bridge/tool-family-records/firestore-inspect.d.ts.map +1 -0
- package/dist/bridge/tool-family-records/firestore-inspect.js +6 -0
- package/dist/bridge/tool-family-records/firestore-inspect.js.map +1 -0
- package/dist/bridge/tool-family-records/firestore-rules.d.ts +7 -0
- package/dist/bridge/tool-family-records/firestore-rules.d.ts.map +1 -0
- package/dist/bridge/tool-family-records/firestore-rules.js +15 -0
- package/dist/bridge/tool-family-records/firestore-rules.js.map +1 -0
- package/dist/bridge/tool-family-records/firestore-simulator.d.ts +7 -0
- package/dist/bridge/tool-family-records/firestore-simulator.d.ts.map +1 -0
- package/dist/bridge/tool-family-records/firestore-simulator.js +16 -0
- package/dist/bridge/tool-family-records/firestore-simulator.js.map +1 -0
- package/dist/bridge/tool-family-records/rtdb-inspection.d.ts +7 -0
- package/dist/bridge/tool-family-records/rtdb-inspection.d.ts.map +1 -0
- package/dist/bridge/tool-family-records/rtdb-inspection.js +6 -0
- package/dist/bridge/tool-family-records/rtdb-inspection.js.map +1 -0
- package/dist/bundler/externals.d.ts +37 -0
- package/dist/bundler/externals.d.ts.map +1 -0
- package/dist/bundler/externals.js +52 -0
- package/dist/bundler/externals.js.map +1 -0
- package/dist/bundler/index.d.ts +17 -0
- package/dist/bundler/index.d.ts.map +1 -0
- package/dist/bundler/index.js +17 -0
- package/dist/bundler/index.js.map +1 -0
- package/dist/cli/inlined-sdk-scanner.d.ts +6 -0
- package/dist/cli/inlined-sdk-scanner.d.ts.map +1 -0
- package/dist/cli/inlined-sdk-scanner.js +82 -0
- package/dist/cli/inlined-sdk-scanner.js.map +1 -0
- package/dist/cli/sandbox-interlock.d.ts +97 -0
- package/dist/cli/sandbox-interlock.d.ts.map +1 -0
- package/dist/cli/sandbox-interlock.js +112 -0
- package/dist/cli/sandbox-interlock.js.map +1 -0
- package/dist/cli/sandbox-runner.d.ts +20 -12
- package/dist/cli/sandbox-runner.d.ts.map +1 -1
- package/dist/cli/sandbox-runner.js +24 -21
- package/dist/cli/sandbox-runner.js.map +1 -1
- package/dist/cli/serve.d.ts +6 -10
- package/dist/cli/serve.d.ts.map +1 -1
- package/dist/cli/serve.js +57 -77
- package/dist/cli/serve.js.map +1 -1
- package/dist/cli/unsupported-runtime.d.ts +27 -0
- package/dist/cli/unsupported-runtime.d.ts.map +1 -0
- package/dist/cli/unsupported-runtime.js +63 -0
- package/dist/cli/unsupported-runtime.js.map +1 -0
- package/dist/conformance/.generated/can-i-use-browser.d.ts +1 -1
- package/dist/conformance/.generated/can-i-use-browser.d.ts.map +1 -1
- package/dist/conformance/.generated/can-i-use-browser.js +1 -1
- package/dist/conformance/.generated/can-i-use-browser.js.map +1 -1
- package/dist/conformance/.generated/can-i-use.d.ts +1 -1
- package/dist/conformance/.generated/can-i-use.d.ts.map +1 -1
- package/dist/conformance/.generated/can-i-use.js +1 -1
- package/dist/conformance/.generated/can-i-use.js.map +1 -1
- package/dist/conformance/.generated/conformance-docs.d.ts.map +1 -1
- package/dist/conformance/.generated/conformance-docs.js +1 -1
- package/dist/conformance/.generated/conformance-docs.js.map +1 -1
- package/dist/functions-rtdb/child.d.ts +37 -0
- package/dist/functions-rtdb/child.d.ts.map +1 -1
- package/dist/functions-rtdb/child.js +90 -21
- package/dist/functions-rtdb/child.js.map +1 -1
- package/dist/functions-rtdb/development-runtime.d.ts +3 -0
- package/dist/functions-rtdb/development-runtime.d.ts.map +1 -1
- package/dist/functions-rtdb/development-runtime.js +1 -0
- package/dist/functions-rtdb/development-runtime.js.map +1 -1
- package/dist/google-endpoints.d.ts +67 -0
- package/dist/google-endpoints.d.ts.map +1 -0
- package/dist/google-endpoints.js +119 -0
- package/dist/google-endpoints.js.map +1 -0
- package/dist/next/runtime-env.d.ts.map +1 -1
- package/dist/next/runtime-env.js +5 -0
- package/dist/next/runtime-env.js.map +1 -1
- package/dist/next/with-pyric.d.ts +1 -1
- package/dist/next/with-pyric.js +1 -1
- package/dist/register/beacon.d.ts +69 -0
- package/dist/register/beacon.d.ts.map +1 -0
- package/dist/register/beacon.js +199 -0
- package/dist/register/beacon.js.map +1 -0
- package/dist/register/index.d.ts.map +1 -1
- package/dist/register/index.js +65 -6
- package/dist/register/index.js.map +1 -1
- package/dist/register/net-guard.d.ts +110 -0
- package/dist/register/net-guard.d.ts.map +1 -0
- package/dist/register/net-guard.js +462 -0
- package/dist/register/net-guard.js.map +1 -0
- package/dist/serve/ai-diagnostics-relay.d.ts +63 -0
- package/dist/serve/ai-diagnostics-relay.d.ts.map +1 -0
- package/dist/serve/ai-diagnostics-relay.js +125 -0
- package/dist/serve/ai-diagnostics-relay.js.map +1 -0
- package/dist/serve/ai-proxy.d.ts +92 -0
- package/dist/serve/ai-proxy.d.ts.map +1 -0
- package/dist/serve/ai-proxy.js +269 -0
- package/dist/serve/ai-proxy.js.map +1 -0
- package/dist/serve/ai-status.d.ts +44 -0
- package/dist/serve/ai-status.d.ts.map +1 -0
- package/dist/serve/ai-status.js +77 -0
- package/dist/serve/ai-status.js.map +1 -0
- package/dist/serve/ai-terminal-blocks.d.ts +97 -0
- package/dist/serve/ai-terminal-blocks.d.ts.map +1 -0
- package/dist/serve/ai-terminal-blocks.js +128 -0
- package/dist/serve/ai-terminal-blocks.js.map +1 -0
- package/dist/serve/ai-terminal-text.d.ts +36 -0
- package/dist/serve/ai-terminal-text.d.ts.map +1 -0
- package/dist/serve/ai-terminal-text.js +48 -0
- package/dist/serve/ai-terminal-text.js.map +1 -0
- package/dist/serve/beacon-route.d.ts +53 -0
- package/dist/serve/beacon-route.d.ts.map +1 -0
- package/dist/serve/beacon-route.js +101 -0
- package/dist/serve/beacon-route.js.map +1 -0
- package/dist/serve/entries/active-auth-registry.d.ts +16 -0
- package/dist/serve/entries/active-auth-registry.d.ts.map +1 -0
- package/dist/serve/entries/active-auth-registry.js +48 -0
- package/dist/serve/entries/active-auth-registry.js.map +1 -0
- package/dist/serve/entries/active-auth.d.ts +18 -0
- package/dist/serve/entries/active-auth.d.ts.map +1 -0
- package/dist/serve/entries/active-auth.js +84 -0
- package/dist/serve/entries/active-auth.js.map +1 -0
- package/dist/serve/entries/auth-helper-core.d.ts +1 -0
- package/dist/serve/entries/auth-helper-core.d.ts.map +1 -1
- package/dist/serve/entries/auth-helper-core.js +4 -0
- package/dist/serve/entries/auth-helper-core.js.map +1 -1
- package/dist/serve/entries/auth-helper-dom.d.ts.map +1 -1
- package/dist/serve/entries/auth-helper-dom.js +28 -2
- package/dist/serve/entries/auth-helper-dom.js.map +1 -1
- package/dist/serve/entries/auth.d.ts.map +1 -1
- package/dist/serve/entries/auth.js +8 -0
- package/dist/serve/entries/auth.js.map +1 -1
- package/dist/serve/entries/bridge-url.d.ts +1 -1
- package/dist/serve/entries/bridge-url.js +1 -1
- package/dist/serve/entries/init.d.ts.map +1 -1
- package/dist/serve/entries/init.js +70 -4
- package/dist/serve/entries/init.js.map +1 -1
- package/dist/serve/entries/runtime.d.ts.map +1 -1
- package/dist/serve/entries/runtime.js +9 -0
- package/dist/serve/entries/runtime.js.map +1 -1
- package/dist/serve/namespace.d.ts +14 -5
- package/dist/serve/namespace.d.ts.map +1 -1
- package/dist/serve/namespace.js +57 -124
- package/dist/serve/namespace.js.map +1 -1
- package/dist/serve/runtime/chip-config.d.ts.map +1 -1
- package/dist/serve/runtime/chip-config.js +16 -2
- package/dist/serve/runtime/chip-config.js.map +1 -1
- package/dist/serve/runtime/chip-dialog.d.ts +18 -0
- package/dist/serve/runtime/chip-dialog.d.ts.map +1 -0
- package/dist/serve/runtime/chip-dialog.js +315 -0
- package/dist/serve/runtime/chip-dialog.js.map +1 -0
- package/dist/serve/runtime/chip-install.d.ts +2 -0
- package/dist/serve/runtime/chip-install.d.ts.map +1 -1
- package/dist/serve/runtime/chip-install.js +9 -4
- package/dist/serve/runtime/chip-install.js.map +1 -1
- package/dist/serve/runtime/chip-user-search.d.ts +16 -0
- package/dist/serve/runtime/chip-user-search.d.ts.map +1 -0
- package/dist/serve/runtime/chip-user-search.js +354 -0
- package/dist/serve/runtime/chip-user-search.js.map +1 -0
- package/dist/serve/runtime/chip.d.ts +10 -0
- package/dist/serve/runtime/chip.d.ts.map +1 -1
- package/dist/serve/runtime/chip.js +118 -15
- package/dist/serve/runtime/chip.js.map +1 -1
- package/dist/serve/runtime/identity.d.ts +17 -0
- package/dist/serve/runtime/identity.d.ts.map +1 -0
- package/dist/serve/runtime/identity.js +10 -0
- package/dist/serve/runtime/identity.js.map +1 -0
- package/dist/serve/sandbox-session.d.ts +7 -0
- package/dist/serve/sandbox-session.d.ts.map +1 -1
- package/dist/serve/sandbox-session.js +2 -0
- package/dist/serve/sandbox-session.js.map +1 -1
- package/dist/serve/site-ui/_astro/{RulesCodeEditor.XtDmHOlN.js → RulesCodeEditor.DsTAsKIa.js} +1 -1
- package/dist/serve/site-ui/_astro/app.BOr2kwae.js +115 -0
- package/dist/serve/site-ui/_astro/app.DhAcImFD.js +1 -0
- package/dist/serve/site-ui/_astro/{chess-showcase.B72XYeoh.js → chess-showcase.Bn_pP6UJ.js} +2 -2
- package/dist/serve/site-ui/_astro/{example-runtime.C25_lx6Z.js → example-runtime.CXopTGv7.js} +1 -1
- package/dist/serve/site-ui/_astro/sandbox-controls.rOuZTOUG.js +510 -0
- package/dist/serve/site-ui/_astro/{seed.Co0yUJY5.js → seed.03a08f9t.js} +2 -2
- package/dist/serve/site-ui/_astro/writes.BsPBG3dp.js +653 -0
- package/dist/serve/site-ui/auth/index.html +1 -1
- package/dist/serve/site-ui/docs/_rhythm/index.html +1 -1
- package/dist/serve/site-ui/docs/agent/set-up-your-agent/index.html +1 -1
- package/dist/serve/site-ui/docs/agent/watch-and-review/index.html +1 -1
- package/dist/serve/site-ui/docs/agent/work-with-an-agent/index.html +1 -1
- package/dist/serve/site-ui/docs/ai-compat/index.html +1 -1
- package/dist/serve/site-ui/docs/api-reference/index.html +8 -1
- package/dist/serve/site-ui/docs/api-reference.md +7 -0
- package/dist/serve/site-ui/docs/app-compat/index.html +1 -1
- package/dist/serve/site-ui/docs/auth-compat/index.html +3 -3
- package/dist/serve/site-ui/docs/auth-compat.md +2 -2
- package/dist/serve/site-ui/docs/build/ai-logic/index.html +6 -1
- package/dist/serve/site-ui/docs/build/ai-logic.md +6 -0
- package/dist/serve/site-ui/docs/build/authentication/index.html +1 -1
- package/dist/serve/site-ui/docs/build/cloud-firestore/index.html +1 -1
- package/dist/serve/site-ui/docs/build/cloud-messaging/index.html +1 -1
- package/dist/serve/site-ui/docs/build/cloud-storage/index.html +1 -1
- package/dist/serve/site-ui/docs/build/realtime-database/index.html +1 -1
- package/dist/serve/site-ui/docs/conformance-scores/index.html +6 -2
- package/dist/serve/site-ui/docs/conformance-scores.md +5 -1
- package/dist/serve/site-ui/docs/create-pyric-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/database-compat/index.html +44 -44
- package/dist/serve/site-ui/docs/database-compat.md +43 -43
- package/dist/serve/site-ui/docs/examples/chess/index.html +1 -1
- package/dist/serve/site-ui/docs/examples/index.html +1 -1
- package/dist/serve/site-ui/docs/firestore-compat/index.html +4 -4
- package/dist/serve/site-ui/docs/firestore-compat.md +3 -3
- package/dist/serve/site-ui/docs/firestore-flutter-compat/index.html +319 -0
- package/dist/serve/site-ui/docs/firestore-flutter-compat.md +344 -0
- package/dist/serve/site-ui/docs/functions-rtdb-compat/index.html +1 -1
- package/dist/serve/site-ui/docs/get-started/backend-bundlers/index.html +105 -0
- package/dist/serve/site-ui/docs/get-started/backend-bundlers.md +165 -0
- package/dist/serve/site-ui/docs/get-started/how-the-swap-works/index.html +1 -1
- package/dist/serve/site-ui/docs/get-started/nextjs/index.html +1 -1
- package/dist/serve/site-ui/docs/get-started/start-building/index.html +1 -1
- package/dist/serve/site-ui/docs/get-started/vite/index.html +1 -1
- package/dist/serve/site-ui/docs/index.json +2967 -1482
- package/dist/serve/site-ui/docs/messaging-compat/index.html +1 -1
- package/dist/serve/site-ui/docs/observe/resolve-runtime-status/index.html +1 -1
- package/dist/serve/site-ui/docs/observe/see-whats-happening/index.html +1 -1
- package/dist/serve/site-ui/docs/observe/shape-your-data/index.html +1 -1
- package/dist/serve/site-ui/docs/overview/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-app-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-auth-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-database-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-firestore-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-messaging-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-admin-storage-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ai-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ai-scripting-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-analytics-reference-api/index.html +124 -0
- package/dist/serve/site-ui/docs/pyric-analytics-reference-api.md +287 -0
- package/dist/serve/site-ui/docs/pyric-app-check-reference-api/index.html +87 -0
- package/dist/serve/site-ui/docs/pyric-app-check-reference-api.md +199 -0
- package/dist/serve/site-ui/docs/pyric-app-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-auth-reference-api/index.html +6 -2
- package/dist/serve/site-ui/docs/pyric-auth-reference-api.md +2 -1
- package/dist/serve/site-ui/docs/pyric-cli-assurance-browser-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-assurance-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-bridge-client-reference-api/index.html +2 -2
- package/dist/serve/site-ui/docs/pyric-cli-bridge-client-reference-api.md +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-bridge-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-bundler-reference-api/index.html +56 -0
- package/dist/serve/site-ui/docs/pyric-cli-bundler-reference-api.md +72 -0
- package/dist/serve/site-ui/docs/pyric-cli-conformance-browser-reference-api/index.html +2 -1
- package/dist/serve/site-ui/docs/pyric-cli-conformance-browser-reference-api.md +1 -0
- package/dist/serve/site-ui/docs/pyric-cli-conformance-docs-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-conformance-reference-api/index.html +2 -1
- package/dist/serve/site-ui/docs/pyric-cli-conformance-reference-api.md +1 -0
- package/dist/serve/site-ui/docs/pyric-cli-credentials-node-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-discover-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-next-reference-api/index.html +2 -2
- package/dist/serve/site-ui/docs/pyric-cli-next-reference-api.md +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-register-app-bridge-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-register-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-remote-reference-api/index.html +34 -9
- package/dist/serve/site-ui/docs/pyric-cli-remote-reference-api.md +16 -8
- package/dist/serve/site-ui/docs/pyric-cli-serve-worker-reference-api/index.html +133 -93
- package/dist/serve/site-ui/docs/pyric-cli-serve-worker-reference-api.md +61 -1
- package/dist/serve/site-ui/docs/pyric-cli-verify-reference-api/index.html +2 -2
- package/dist/serve/site-ui/docs/pyric-cli-verify-reference-api.md +1 -1
- package/dist/serve/site-ui/docs/pyric-cli-vite-reference-api/index.html +2 -2
- package/dist/serve/site-ui/docs/pyric-cli-vite-reference-api.md +2 -2
- package/dist/serve/site-ui/docs/pyric-database-reference-api/index.html +2 -2
- package/dist/serve/site-ui/docs/pyric-database-reference-api.md +1 -1
- package/dist/serve/site-ui/docs/pyric-firestore-lite-reference-api/index.html +559 -0
- package/dist/serve/site-ui/docs/pyric-firestore-lite-reference-api.md +1240 -0
- package/dist/serve/site-ui/docs/pyric-firestore-reference-api/index.html +40 -11
- package/dist/serve/site-ui/docs/pyric-firestore-reference-api.md +18 -8
- package/dist/serve/site-ui/docs/pyric-functions-reference-api/index.html +67 -0
- package/dist/serve/site-ui/docs/pyric-functions-reference-api.md +149 -0
- package/dist/serve/site-ui/docs/pyric-messaging-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-messaging-sw-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-performance-reference-api/index.html +35 -0
- package/dist/serve/site-ui/docs/pyric-performance-reference-api.md +69 -0
- package/dist/serve/site-ui/docs/pyric-remote-config-reference-api/index.html +132 -0
- package/dist/serve/site-ui/docs/pyric-remote-config-reference-api.md +307 -0
- package/dist/serve/site-ui/docs/pyric-rules-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-sandbox-database-reference-api/index.html +52 -17
- package/dist/serve/site-ui/docs/pyric-sandbox-database-reference-api.md +48 -9
- package/dist/serve/site-ui/docs/pyric-sandbox-firestore-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-sandbox-reference-api/index.html +239 -49
- package/dist/serve/site-ui/docs/pyric-sandbox-reference-api.md +148 -31
- package/dist/serve/site-ui/docs/pyric-storage-reference-api/index.html +22 -4
- package/dist/serve/site-ui/docs/pyric-storage-reference-api.md +24 -2
- package/dist/serve/site-ui/docs/pyric-ui-agents-reference-api/index.html +26 -96
- package/dist/serve/site-ui/docs/pyric-ui-agents-reference-api.md +20 -51
- package/dist/serve/site-ui/docs/pyric-ui-auth-hooks-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-auth-reference-api/index.html +2 -2
- package/dist/serve/site-ui/docs/pyric-ui-auth-reference-api.md +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-events-hooks-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-events-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-firestore-hooks-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-firestore-reference-api/index.html +2 -2
- package/dist/serve/site-ui/docs/pyric-ui-firestore-reference-api.md +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-primitives-reference-api/index.html +124 -26
- package/dist/serve/site-ui/docs/pyric-ui-primitives-reference-api.md +58 -5
- package/dist/serve/site-ui/docs/pyric-ui-rtdb-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-rules-hooks-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-rules-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-storage-hooks-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-storage-reference-api/index.html +4 -5
- package/dist/serve/site-ui/docs/pyric-ui-storage-reference-api.md +3 -4
- package/dist/serve/site-ui/docs/pyric-ui-traffic-hooks-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/pyric-ui-traffic-reference-api/index.html +1 -1
- package/dist/serve/site-ui/docs/reference/cli/index.html +6 -3
- package/dist/serve/site-ui/docs/reference/cli.md +8 -2
- package/dist/serve/site-ui/docs/rules-compat/index.html +10 -2
- package/dist/serve/site-ui/docs/rules-compat.md +9 -1
- package/dist/serve/site-ui/docs/secure/audit-your-rules/index.html +1 -1
- package/dist/serve/site-ui/docs/secure/firestore-rules-limits/index.html +1 -1
- package/dist/serve/site-ui/docs/secure/read-a-denial/index.html +1 -1
- package/dist/serve/site-ui/docs/secure/rtdb-rules-in-typescript/index.html +1 -1
- package/dist/serve/site-ui/docs/secure/rules-standard-library/index.html +1 -1
- package/dist/serve/site-ui/docs/secure/secure-it-with-rules/index.html +1 -1
- package/dist/serve/site-ui/docs/secure/simulate-and-lint/index.html +1 -1
- package/dist/serve/site-ui/docs/secure/write-a-rules-test-suite/index.html +1 -1
- package/dist/serve/site-ui/docs/ship/ship-to-production/index.html +1 -1
- package/dist/serve/site-ui/docs/ship/test-in-node/index.html +1 -1
- package/dist/serve/site-ui/docs/storage-compat/index.html +13 -13
- package/dist/serve/site-ui/docs/storage-compat.md +12 -12
- package/dist/serve/site-ui/docs/trust/how-we-know-it-matches-firebase/index.html +1 -1
- package/dist/serve/site-ui/docs/trust/rules-standard-library/index.html +1 -1
- package/dist/serve/site-ui/docs/trust/versioning-and-compatibility/index.html +1 -1
- package/dist/serve/site-ui/docs/tutorial/index.html +1 -1
- package/dist/serve/site-ui/examples/chess/index.html +1 -1
- package/dist/serve/site-ui/firestore/index.html +1 -1
- package/dist/serve/site-ui/llms.txt +9 -0
- package/dist/serve/site-ui/rtdb/index.html +1 -1
- package/dist/serve/site-ui/settings/index.html +1 -1
- package/dist/serve/site-ui/storage/index.html +1 -1
- package/dist/serve/site-ui/studio/index.html +1 -1
- package/dist/serve/site-ui/traffic/index.html +1 -1
- package/dist/serve/studio/disk-workspace.d.ts.map +1 -1
- package/dist/serve/studio/disk-workspace.js +64 -2
- package/dist/serve/studio/disk-workspace.js.map +1 -1
- package/dist/serve/vite-plugin.d.ts +7 -7
- package/dist/serve/vite-plugin.js +1 -1
- package/dist/serve/vite-plugin.js.map +1 -1
- package/dist/serve/vite-sandbox-generation.d.ts.map +1 -1
- package/dist/serve/vite-sandbox-generation.js +12 -0
- package/dist/serve/vite-sandbox-generation.js.map +1 -1
- package/dist/serve/worker/client/auth.js +1 -2
- package/dist/serve/worker/client/auth.js.map +1 -1
- package/dist/serve/worker/client/core.d.ts +21 -1
- package/dist/serve/worker/client/core.d.ts.map +1 -1
- package/dist/serve/worker/client/core.js +79 -3
- package/dist/serve/worker/client/core.js.map +1 -1
- package/dist/serve/worker/client.d.ts +1 -1
- package/dist/serve/worker/client.d.ts.map +1 -1
- package/dist/serve/worker/client.js +1 -1
- package/dist/serve/worker/client.js.map +1 -1
- package/dist/serve/worker/context-resolver.d.ts +30 -0
- package/dist/serve/worker/context-resolver.d.ts.map +1 -0
- package/dist/serve/worker/context-resolver.js +44 -0
- package/dist/serve/worker/context-resolver.js.map +1 -0
- package/dist/serve/worker/entry.js +23 -19
- package/dist/serve/worker/entry.js.map +1 -1
- package/dist/serve/worker/host/core.d.ts +1 -0
- package/dist/serve/worker/host/core.d.ts.map +1 -1
- package/dist/serve/worker/host/core.js +12 -6
- package/dist/serve/worker/host/core.js.map +1 -1
- package/dist/serve/worker/host/storage.js +2 -2
- package/dist/serve/worker/host-auth.js +1 -2
- package/dist/serve/worker/host-auth.js.map +1 -1
- package/dist/serve/worker/index.d.ts +1 -1
- package/dist/serve/worker/index.d.ts.map +1 -1
- package/dist/serve/worker/index.js +2 -2
- package/dist/serve/worker/index.js.map +1 -1
- package/dist/serve/worker/port-lifecycle.d.ts +18 -0
- package/dist/serve/worker/port-lifecycle.d.ts.map +1 -0
- package/dist/serve/worker/port-lifecycle.js +28 -0
- package/dist/serve/worker/port-lifecycle.js.map +1 -0
- package/dist/serve/worker/serve-init.d.ts.map +1 -1
- package/dist/serve/worker/serve-init.js +8 -2
- package/dist/serve/worker/serve-init.js.map +1 -1
- package/dist/verify/fixture.d.ts +1 -1
- package/package.json +16 -5
- package/src/assurance/.generated/conformance-verdicts.ts +68 -1
- package/src/bridge/client/dispatch.ts +52 -56
- package/src/bridge/client/tool-family-factories.ts +34 -0
- package/src/bridge/server/mcp-contract.ts +15 -59
- package/src/bridge/server/peer.ts +24 -1
- package/src/bridge/server/tool-family-factories.ts +40 -0
- package/src/bridge/server/tool-metadata.ts +29 -61
- package/src/bridge/tool-families.generated.ts +17 -0
- package/src/bridge/tool-families.ts +87 -0
- package/src/bridge/tool-family-records/conformance.ts +6 -0
- package/src/bridge/tool-family-records/firestore-data.ts +15 -0
- package/src/bridge/tool-family-records/firestore-inspect.ts +6 -0
- package/src/bridge/tool-family-records/firestore-rules.ts +15 -0
- package/src/bridge/tool-family-records/firestore-simulator.ts +16 -0
- package/src/bridge/tool-family-records/rtdb-inspection.ts +6 -0
- package/src/bundler/externals.ts +55 -0
- package/src/bundler/index.ts +21 -0
- package/src/cli/cli.test.ts +0 -44
- package/src/cli/inlined-sdk-scanner.ts +77 -0
- package/src/cli/sandbox-interlock.ts +167 -0
- package/src/cli/sandbox-runner.ts +37 -26
- package/src/cli/serve.ts +68 -69
- package/src/cli/unsupported-runtime.ts +68 -0
- package/src/conformance/.generated/can-i-use-browser.ts +2 -2
- package/src/conformance/.generated/can-i-use.ts +2 -2
- package/src/conformance/.generated/conformance-docs.ts +1 -1
- package/src/functions-rtdb/child.ts +115 -21
- package/src/functions-rtdb/development-runtime.ts +4 -0
- package/src/google-endpoints.ts +155 -0
- package/src/next/runtime-env.ts +5 -0
- package/src/next/with-pyric.ts +1 -1
- package/src/register/beacon.ts +232 -0
- package/src/register/index.ts +68 -6
- package/src/register/net-guard.ts +567 -0
- package/src/serve/ai-diagnostics-relay.ts +243 -0
- package/src/serve/ai-proxy.ts +327 -0
- package/src/serve/ai-status.ts +109 -0
- package/src/serve/ai-terminal-blocks.ts +210 -0
- package/src/serve/ai-terminal-text.ts +52 -0
- package/src/serve/beacon-route.ts +141 -0
- package/src/serve/entries/active-auth-registry.ts +56 -0
- package/src/serve/entries/active-auth.ts +100 -0
- package/src/serve/entries/auth-helper-core.ts +5 -0
- package/src/serve/entries/auth-helper-dom.ts +26 -2
- package/src/serve/entries/auth.ts +8 -0
- package/src/serve/entries/bridge-url.ts +1 -1
- package/src/serve/entries/init.ts +78 -3
- package/src/serve/entries/runtime.ts +10 -0
- package/src/serve/namespace.ts +73 -140
- package/src/serve/runtime/chip-config.ts +16 -2
- package/src/serve/runtime/chip-dialog.ts +366 -0
- package/src/serve/runtime/chip-install.ts +10 -4
- package/src/serve/runtime/chip-user-search.ts +391 -0
- package/src/serve/runtime/chip.ts +140 -15
- package/src/serve/runtime/identity.ts +26 -0
- package/src/serve/sandbox-session.ts +9 -0
- package/src/serve/studio/disk-workspace.ts +76 -2
- package/src/serve/vite-plugin.ts +8 -8
- package/src/serve/vite-sandbox-generation.ts +15 -0
- package/src/serve/worker/client/auth.ts +1 -1
- package/src/serve/worker/client/core.ts +80 -3
- package/src/serve/worker/client.ts +1 -1
- package/src/serve/worker/context-resolver.ts +60 -0
- package/src/serve/worker/entry.ts +22 -18
- package/src/serve/worker/host/core.ts +9 -7
- package/src/serve/worker/host/storage.ts +2 -2
- package/src/serve/worker/host-auth.ts +1 -1
- package/src/serve/worker/index.ts +4 -1
- package/src/serve/worker/port-lifecycle.ts +45 -0
- package/src/serve/worker/serve-init.ts +9 -2
- package/src/verify/fixture.ts +1 -1
- package/dist/serve/site-ui/_astro/app.B52xr-DK.js +0 -1
- package/dist/serve/site-ui/_astro/app.DWBraPUw.js +0 -12
- package/dist/serve/site-ui/_astro/sandbox-controls.DEpnKg-O.js +0 -1169
- package/dist/serve/site-ui/_astro/writes.rpjrFzmu.js +0 -102
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import type { SandboxEvent } from 'pyric/sandbox';
|
|
2
|
+
import { capTerminalText } from './ai-terminal-text.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* AI diagnostics relay: headless dev visibility for the three things the
|
|
6
|
+
* broker does that no app-level signal reports.
|
|
7
|
+
*
|
|
8
|
+
* The broker lands `service_mutation` events (`service: 'ai'`) on the
|
|
9
|
+
* sandbox's unified stream. That stream is browser-side: the worker host fans
|
|
10
|
+
* it out over the port for Studio, and it never reaches the dev server. An
|
|
11
|
+
* agent driving `pyric dev` headlessly therefore saw NOTHING.
|
|
12
|
+
*
|
|
13
|
+
* - `request_rejected`: a bad role, empty `contents`, a missing thought
|
|
14
|
+
* signature, or an engine that answered an error envelope.
|
|
15
|
+
* - `response_blocked`: the SILENT refusal. A safety / recitation /
|
|
16
|
+
* blocklist filter answers HTTP 200 with an empty candidate, so nothing
|
|
17
|
+
* throws anywhere and the app simply renders nothing.
|
|
18
|
+
* - `model_substituted`: not a refusal at all. The answer arrives, from a
|
|
19
|
+
* model the developer never asked for (an openai `modelMap` entry or
|
|
20
|
+
* catch-all `model`, a gemini experimental alias). Same shape of problem:
|
|
21
|
+
* the sandbox quietly did something other than what the code said, and
|
|
22
|
+
* only the terminal can say so.
|
|
23
|
+
*
|
|
24
|
+
* One record per kind in {@link AI_DIAGNOSTIC_RELAYS} pairs the sandbox `op`
|
|
25
|
+
* with the narrower that turns it into a wire payload. The terminal formatter
|
|
26
|
+
* and throttle key for each kind live in `ai-terminal-blocks.ts`, keyed by the
|
|
27
|
+
* same {@link AiDiagnosticKind} union, so the compiler flags a kind that is
|
|
28
|
+
* relayed but never rendered.
|
|
29
|
+
*
|
|
30
|
+
* This is the AI twin of the Firebase Activity Guard reporter next door
|
|
31
|
+
* (`activity-guard.ts`): a browser-safe subscriber shared by the SharedWorker
|
|
32
|
+
* and in-page planes, which fire-and-forget POSTs to the dev server. It rides
|
|
33
|
+
* the DENIAL relay's existing channel (`POST /__pyric/denials`, see the denial
|
|
34
|
+
* relay section of `namespace.ts`) rather than opening a new one: a broker
|
|
35
|
+
* refusal is the same category of thing as a rules denial, it wants the same
|
|
36
|
+
* terminal treatment, and it wants that endpoint's per-(target, reason)
|
|
37
|
+
* throttle, because an agent retry loop re-sends the same request over and
|
|
38
|
+
* over.
|
|
39
|
+
*
|
|
40
|
+
* Best-effort, exactly like the activity reporter and the denial relay: never
|
|
41
|
+
* throws, never awaited into app flow, and a relay failure (dev server gone,
|
|
42
|
+
* no `fetch`, non-pyric host) is swallowed. Diagnostics, not a correctness
|
|
43
|
+
* path.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/** The slice of the sandbox this relay needs. Live events only: replaying
|
|
47
|
+
* `history()` on boot would re-announce refusals the terminal already
|
|
48
|
+
* printed (and the activity feed's reason for taking history does not apply,
|
|
49
|
+
* since nothing here correlates across events). */
|
|
50
|
+
export interface AiDiagnosticsFeed {
|
|
51
|
+
subscribe(listener: (event: SandboxEvent) => void): unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The `kind` discriminator on every payload this relay POSTs. It is also the
|
|
55
|
+
* key of the formatter table in `ai-terminal-blocks.ts`. */
|
|
56
|
+
export type AiDiagnosticKind = 'ai-rejection' | 'ai-blocked' | 'ai-model-substituted';
|
|
57
|
+
|
|
58
|
+
/** The sandbox events this relay reads. */
|
|
59
|
+
type ServiceMutation = Extract<SandboxEvent, { kind: 'service_mutation' }>;
|
|
60
|
+
|
|
61
|
+
/** Shape POSTed for a `request_rejected` event. `namespace.ts` reads it back
|
|
62
|
+
* loosely (`AiRejectionPayload` in `ai-terminal-blocks.ts`): the body is JSON
|
|
63
|
+
* from the served page, and nothing on the far side enforces this type. */
|
|
64
|
+
interface AiRejectionRelayPayload {
|
|
65
|
+
kind: 'ai-rejection';
|
|
66
|
+
/** Model resource the rejected op targeted (the event's `path`). */
|
|
67
|
+
model?: string;
|
|
68
|
+
/** Which engine the broker resolved to (`scripted` | `openai` | `gemini` |
|
|
69
|
+
* `custom`); a rejection reads very differently for each. */
|
|
70
|
+
engine?: string;
|
|
71
|
+
/** Wire error status (e.g. `INVALID_ARGUMENT`) and HTTP-ish code. */
|
|
72
|
+
status?: string;
|
|
73
|
+
code?: number;
|
|
74
|
+
/** The rejection reason: production's own message for shape errors. */
|
|
75
|
+
message: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Shape POSTed for a `response_blocked` event. Carries the wire's own
|
|
79
|
+
* vocabulary, `finishReason` (a candidate was withheld) or `blockReason`
|
|
80
|
+
* (the prompt was refused), rather than a pre-rendered sentence, so the
|
|
81
|
+
* terminal formatter owns the phrasing exactly the way it does for a
|
|
82
|
+
* rejection. */
|
|
83
|
+
interface AiBlockedRelayPayload {
|
|
84
|
+
kind: 'ai-blocked';
|
|
85
|
+
/** Model resource the blocked op targeted (the event's `path`). */
|
|
86
|
+
model?: string;
|
|
87
|
+
/** Which engine the broker resolved to. */
|
|
88
|
+
engine?: string;
|
|
89
|
+
/** Candidate-level block: `SAFETY`, `RECITATION`, `BLOCKLIST`, and so on. */
|
|
90
|
+
finishReason?: string;
|
|
91
|
+
/** Production's own explanatory text for the block, when it sent one. */
|
|
92
|
+
finishMessage?: string;
|
|
93
|
+
/** Prompt-level block: `promptFeedback.blockReason`. */
|
|
94
|
+
blockReason?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Shape POSTed for a `model_substituted` event. Not a refusal: the answer
|
|
98
|
+
* arrives normally, just from a model the developer never named. */
|
|
99
|
+
interface AiModelSubstitutionRelayPayload {
|
|
100
|
+
kind: 'ai-model-substituted';
|
|
101
|
+
/** The model the request asked for (the event's `path`). */
|
|
102
|
+
requestedModel: string;
|
|
103
|
+
/** The model the engine actually calls (bare, no `models/` prefix). */
|
|
104
|
+
effectiveModel: string;
|
|
105
|
+
/** Which engine substituted (`openai` | `gemini` | ...). */
|
|
106
|
+
engine?: string;
|
|
107
|
+
/** Why it differs, e.g. `engine modelMap`, `experimental alias`. */
|
|
108
|
+
reason?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
type AiDiagnosticRelayPayload =
|
|
112
|
+
| AiRejectionRelayPayload
|
|
113
|
+
| AiBlockedRelayPayload
|
|
114
|
+
| AiModelSubstitutionRelayPayload;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A string field off the event's free-form `detail` record, or `undefined`
|
|
118
|
+
* when the emitter did not set it. `detail` is a public contract anyone can
|
|
119
|
+
* post onto, so every field is re-checked, and every value is length-capped
|
|
120
|
+
* here rather than on the far side: a model server that answers with a
|
|
121
|
+
* megabyte in its `finishMessage` must not push a megabyte through the
|
|
122
|
+
* fire-and-forget POST just to have the terminal cut it.
|
|
123
|
+
*/
|
|
124
|
+
function detailText(detail: Record<string, unknown>, field: string): string | undefined {
|
|
125
|
+
const value = detail[field];
|
|
126
|
+
if (typeof value !== 'string') return undefined;
|
|
127
|
+
return capTerminalText(value);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The event's `path` when it names a model, or `undefined`. */
|
|
131
|
+
function eventModel(event: ServiceMutation): string | undefined {
|
|
132
|
+
if (typeof event.path !== 'string' || event.path === '') return undefined;
|
|
133
|
+
return capTerminalText(event.path);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function detailOf(event: ServiceMutation): Record<string, unknown> {
|
|
137
|
+
return (event.detail ?? {}) as Record<string, unknown>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Narrow a `request_rejected` event to its relay payload. */
|
|
141
|
+
function toRejectionPayload(event: ServiceMutation): AiRejectionRelayPayload | null {
|
|
142
|
+
const detail = detailOf(event);
|
|
143
|
+
const payload: AiRejectionRelayPayload = {
|
|
144
|
+
kind: 'ai-rejection',
|
|
145
|
+
message: detailText(detail, 'message') ?? 'request rejected',
|
|
146
|
+
};
|
|
147
|
+
payload.model = eventModel(event);
|
|
148
|
+
payload.engine = detailText(detail, 'engine');
|
|
149
|
+
payload.status = detailText(detail, 'status');
|
|
150
|
+
if (typeof detail.code === 'number') payload.code = detail.code;
|
|
151
|
+
return payload;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Narrow a `response_blocked` event to its relay payload. */
|
|
155
|
+
function toBlockedPayload(event: ServiceMutation): AiBlockedRelayPayload | null {
|
|
156
|
+
const detail = detailOf(event);
|
|
157
|
+
const payload: AiBlockedRelayPayload = { kind: 'ai-blocked' };
|
|
158
|
+
payload.model = eventModel(event);
|
|
159
|
+
payload.engine = detailText(detail, 'engine');
|
|
160
|
+
payload.finishReason = detailText(detail, 'finishReason');
|
|
161
|
+
payload.finishMessage = detailText(detail, 'finishMessage');
|
|
162
|
+
payload.blockReason = detailText(detail, 'blockReason');
|
|
163
|
+
return payload;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Narrow a `model_substituted` event to its relay payload, and enforce the
|
|
168
|
+
* invariant the terminal line depends on: an effective model equal to the
|
|
169
|
+
* requested one is NOT a substitution and never relays. The broker already
|
|
170
|
+
* compares before emitting; this re-checks because the event stream is a
|
|
171
|
+
* public contract anyone can post onto.
|
|
172
|
+
*/
|
|
173
|
+
function toModelSubstitutionPayload(
|
|
174
|
+
event: ServiceMutation,
|
|
175
|
+
): AiModelSubstitutionRelayPayload | null {
|
|
176
|
+
const detail = detailOf(event);
|
|
177
|
+
const requestedModel = detailText(detail, 'requestedModel') ?? eventModel(event) ?? '';
|
|
178
|
+
const effectiveModel = detailText(detail, 'effectiveModel') ?? '';
|
|
179
|
+
if (requestedModel === '' || effectiveModel === '') return null;
|
|
180
|
+
let requestedBare = requestedModel;
|
|
181
|
+
if (requestedModel.startsWith('models/')) requestedBare = requestedModel.slice('models/'.length);
|
|
182
|
+
if (effectiveModel === requestedBare || effectiveModel === requestedModel) return null;
|
|
183
|
+
const payload: AiModelSubstitutionRelayPayload = {
|
|
184
|
+
kind: 'ai-model-substituted',
|
|
185
|
+
requestedModel,
|
|
186
|
+
effectiveModel,
|
|
187
|
+
};
|
|
188
|
+
payload.engine = detailText(detail, 'engine');
|
|
189
|
+
payload.reason = detailText(detail, 'reason');
|
|
190
|
+
return payload;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** One record per relayed kind: the sandbox `op` that produces it, and the
|
|
194
|
+
* narrower that turns that event into a wire payload. */
|
|
195
|
+
interface AiDiagnosticRelay {
|
|
196
|
+
op: string;
|
|
197
|
+
fromEvent(event: ServiceMutation): AiDiagnosticRelayPayload | null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const AI_DIAGNOSTIC_RELAYS: { [K in AiDiagnosticKind]: AiDiagnosticRelay } = {
|
|
201
|
+
'ai-rejection': { op: 'request_rejected', fromEvent: toRejectionPayload },
|
|
202
|
+
'ai-blocked': { op: 'response_blocked', fromEvent: toBlockedPayload },
|
|
203
|
+
'ai-model-substituted': { op: 'model_substituted', fromEvent: toModelSubstitutionPayload },
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/** The relay record for a sandbox `op`, or `null` for an op nothing relays. */
|
|
207
|
+
function relayForOp(op: string): AiDiagnosticRelay | null {
|
|
208
|
+
for (const relay of Object.values(AI_DIAGNOSTIC_RELAYS)) {
|
|
209
|
+
if (relay.op === op) return relay;
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Subscribe to the sandbox event stream and relay every broker refusal,
|
|
216
|
+
* rejected request or blocked response, plus every silent model substitution,
|
|
217
|
+
* to the dev server for terminal visibility.
|
|
218
|
+
*
|
|
219
|
+
* Browser-safe (shared by the SharedWorker host and the in-page runtime);
|
|
220
|
+
* returns nothing to unsubscribe with, mirroring the guard next door, since
|
|
221
|
+
* the subscription is observational and lives for the plane's lifetime.
|
|
222
|
+
*/
|
|
223
|
+
export function setupAiDiagnosticsRelay(feed: AiDiagnosticsFeed, fetchFn: typeof fetch): void {
|
|
224
|
+
feed.subscribe((event) => {
|
|
225
|
+
if (event.kind !== 'service_mutation') return;
|
|
226
|
+
if (event.service !== 'ai') return;
|
|
227
|
+
const relay = relayForOp(event.op);
|
|
228
|
+
if (relay === null) return;
|
|
229
|
+
const payload = relay.fromEvent(event);
|
|
230
|
+
if (payload === null) return;
|
|
231
|
+
try {
|
|
232
|
+
void fetchFn('/__pyric/denials', {
|
|
233
|
+
method: 'POST',
|
|
234
|
+
headers: { 'content-type': 'application/json' },
|
|
235
|
+
body: JSON.stringify(payload),
|
|
236
|
+
}).catch(() => {
|
|
237
|
+
/* Diagnostics are best-effort and must never affect app behavior. */
|
|
238
|
+
});
|
|
239
|
+
} catch {
|
|
240
|
+
/* An injected or unavailable fetch must never affect app behavior. */
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/__pyric/ai-proxy`: the same-origin passthrough to an OpenAI-compatible
|
|
3
|
+
* upstream, plus the terminal diagnostics that make an upstream failure
|
|
4
|
+
* visible to a developer with no browser console open.
|
|
5
|
+
*
|
|
6
|
+
* The route is always mounted; it only touches the network when a request
|
|
7
|
+
* arrives. Everything about WHERE it forwards is resolved by
|
|
8
|
+
* {@link resolveAiProxyUpstream}, which the startup banner (`ai-status.ts`)
|
|
9
|
+
* shares so the two can never disagree.
|
|
10
|
+
*/
|
|
11
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
12
|
+
import type { ServeLogger } from './server.js';
|
|
13
|
+
import { redactUrl } from 'pyric/ai/internal';
|
|
14
|
+
import { sanitizeForTerminal } from './ai-terminal-text.js';
|
|
15
|
+
|
|
16
|
+
/** The dev server's diagnostics throttle, as this module needs it: the
|
|
17
|
+
* denials route owns the instance, and the proxy shares it so a failing
|
|
18
|
+
* upstream cannot out-shout a denied listener. */
|
|
19
|
+
export interface AiDiagnosticThrottle {
|
|
20
|
+
shouldPrint(key: string, now: number): boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Default upstream: local Ollama's OpenAI-compatible endpoint. */
|
|
24
|
+
export const AI_PROXY_DEFAULT_UPSTREAM = 'http://localhost:11434/v1';
|
|
25
|
+
|
|
26
|
+
/** The route the dev server mounts for OpenAI-compatible traffic. */
|
|
27
|
+
export const AI_PROXY_ROUTE = '/__pyric/ai-proxy';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Request headers that must NOT be forwarded upstream: origin-sensitive
|
|
31
|
+
* browser context (`origin`/`referer`/`cookie`, since the upstream is a
|
|
32
|
+
* different origin and must never see the page's), hop-by-hop headers, and
|
|
33
|
+
* the two the proxy re-derives (`host`, `content-length`). `accept-encoding`
|
|
34
|
+
* is dropped so fetch negotiates its own compression and the streamed body
|
|
35
|
+
* needs no length fixups.
|
|
36
|
+
*/
|
|
37
|
+
const AI_PROXY_STRIPPED_HEADERS = new Set([
|
|
38
|
+
'host',
|
|
39
|
+
'origin',
|
|
40
|
+
'referer',
|
|
41
|
+
'cookie',
|
|
42
|
+
'connection',
|
|
43
|
+
'content-length',
|
|
44
|
+
'accept-encoding',
|
|
45
|
+
'keep-alive',
|
|
46
|
+
'proxy-authorization',
|
|
47
|
+
'te',
|
|
48
|
+
'trailer',
|
|
49
|
+
'transfer-encoding',
|
|
50
|
+
'upgrade',
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
/** The upstream-failure shapes the proxy surfaces to the terminal. */
|
|
54
|
+
type AiProxyFailure =
|
|
55
|
+
| { kind: 'unreachable'; target: string; latencyMs: number; cause: string }
|
|
56
|
+
| {
|
|
57
|
+
kind: 'status';
|
|
58
|
+
target: string;
|
|
59
|
+
latencyMs: number;
|
|
60
|
+
status: number;
|
|
61
|
+
/** The upstream's raw `Retry-After` header, when it sent one. */
|
|
62
|
+
retryAfter?: string;
|
|
63
|
+
}
|
|
64
|
+
| { kind: 'stream-abort'; target: string; latencyMs: number; cause: string };
|
|
65
|
+
|
|
66
|
+
/** Longest `Retry-After` value echoed to the terminal (delta-seconds and
|
|
67
|
+
* HTTP-dates are both far shorter; anything longer is upstream noise). */
|
|
68
|
+
const AI_PROXY_RETRY_AFTER_MAX = 64;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Render a `Retry-After` value for the terminal, or `null` when there is
|
|
72
|
+
* nothing worth printing.
|
|
73
|
+
*
|
|
74
|
+
* The header is remote-authored, so it is sanitized and length-capped like
|
|
75
|
+
* every other upstream string that reaches the terminal. Delta-seconds (the
|
|
76
|
+
* common form) get an `s` suffix so the wait reads as a duration; the
|
|
77
|
+
* HTTP-date form is echoed verbatim, since `Wed, 21 Oct 2026 07:28:00 GMTs`
|
|
78
|
+
* would be nonsense.
|
|
79
|
+
*/
|
|
80
|
+
function formatRetryAfter(raw: string): string | null {
|
|
81
|
+
const clean = sanitizeForTerminal(raw).slice(0, AI_PROXY_RETRY_AFTER_MAX).trim();
|
|
82
|
+
if (clean === '') return null;
|
|
83
|
+
if (/^\d+$/.test(clean)) return `${clean}s`;
|
|
84
|
+
return clean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Mask credentials, then flatten and cap, in that order. Both the target and
|
|
89
|
+
* a raw fetch error message are remote-influenced: the URL carries whatever
|
|
90
|
+
* the page put in the query string, and an error message can quote it back.
|
|
91
|
+
*/
|
|
92
|
+
function safeUpstreamText(text: string): string {
|
|
93
|
+
return sanitizeForTerminal(redactUrl(text));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The throttle key for one upstream failure. A failing upstream fails on
|
|
98
|
+
* every request, so the key is the failure itself (kind, status, and the
|
|
99
|
+
* redacted target) rather than anything per-request: one line per window,
|
|
100
|
+
* exactly like a denied listener that re-fires.
|
|
101
|
+
*/
|
|
102
|
+
function aiProxyThrottleKey(failure: AiProxyFailure): string {
|
|
103
|
+
const target = redactUrl(failure.target);
|
|
104
|
+
if (failure.kind === 'status') return `ai-proxy status ${failure.status} ${target}`;
|
|
105
|
+
return `ai-proxy ${failure.kind} ${target}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Print one upstream failure, unless the throttle already printed it. */
|
|
109
|
+
function noteAiProxyFailure(
|
|
110
|
+
failure: AiProxyFailure,
|
|
111
|
+
throttle: AiDiagnosticThrottle,
|
|
112
|
+
logger?: ServeLogger,
|
|
113
|
+
): void {
|
|
114
|
+
if (logger === undefined) return;
|
|
115
|
+
if (!throttle.shouldPrint(aiProxyThrottleKey(failure), Date.now())) return;
|
|
116
|
+
logger.note(formatAiProxyWarning(failure));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Format an upstream failure into the compact terminal block, the same
|
|
121
|
+
* ` [pyric] ...` idiom the denial relay prints, so ai-proxy trouble reads
|
|
122
|
+
* like every other dev-server diagnostic. Two lines: what went wrong, then
|
|
123
|
+
* the (redacted) upstream URL with the elapsed time. Exported for unit tests.
|
|
124
|
+
*
|
|
125
|
+
* A 429 says so in words (`rate limited`): the bare number reads as one more
|
|
126
|
+
* failed request, when it actually means the upstream is shedding load or the
|
|
127
|
+
* key is out of quota. When the upstream sent a `Retry-After`, a third line
|
|
128
|
+
* prints the wait it asked for, and states that NOTHING here waits on the
|
|
129
|
+
* developer's behalf. The proxy has no retry loop (the status rides straight
|
|
130
|
+
* through to the caller), so the backoff is the app's decision, not pyric's.
|
|
131
|
+
*/
|
|
132
|
+
export function formatAiProxyWarning(failure: AiProxyFailure): string {
|
|
133
|
+
const target = safeUpstreamText(failure.target);
|
|
134
|
+
let headline: string;
|
|
135
|
+
if (failure.kind === 'unreachable') {
|
|
136
|
+
headline = `upstream unreachable: ${safeUpstreamText(failure.cause)}`;
|
|
137
|
+
} else if (failure.kind === 'status') {
|
|
138
|
+
const rateLimited = failure.status === 429;
|
|
139
|
+
headline = `upstream returned ${failure.status}${rateLimited ? ' (rate limited / quota exhausted)' : ''}`;
|
|
140
|
+
} else {
|
|
141
|
+
headline = `upstream stream aborted mid-response: ${safeUpstreamText(failure.cause)}`;
|
|
142
|
+
}
|
|
143
|
+
const lines = [
|
|
144
|
+
` ⚠ [pyric] ai-proxy: ${headline}`,
|
|
145
|
+
` POST ${target} (${failure.latencyMs}ms)`,
|
|
146
|
+
];
|
|
147
|
+
if (failure.kind === 'unreachable') {
|
|
148
|
+
lines.push(
|
|
149
|
+
` set PYRIC_AI_PROXY_UPSTREAM to an OpenAI-compatible base URL (default ${AI_PROXY_DEFAULT_UPSTREAM})`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
if (failure.kind === 'status' && failure.retryAfter !== undefined) {
|
|
153
|
+
const wait = formatRetryAfter(failure.retryAfter);
|
|
154
|
+
if (wait !== null) {
|
|
155
|
+
lines.push(` Retry-After: ${wait}, no automatic retry; the status went to the caller`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return lines.join('\n');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Resolve the upstream `/__pyric/ai-proxy` forwards to, and say WHERE that
|
|
163
|
+
* value came from. One resolution shared by the proxy handler (which needs
|
|
164
|
+
* the target) and the startup line (which needs the provenance, so a
|
|
165
|
+
* developer can tell a deliberate `PYRIC_AI_PROXY_UPSTREAM` from the Ollama
|
|
166
|
+
* default nobody chose).
|
|
167
|
+
*/
|
|
168
|
+
export function resolveAiProxyUpstream(
|
|
169
|
+
configured: string | undefined,
|
|
170
|
+
): { target: string; source: 'option' | 'env' | 'default' } {
|
|
171
|
+
const envUpstream = process.env.PYRIC_AI_PROXY_UPSTREAM;
|
|
172
|
+
let raw = AI_PROXY_DEFAULT_UPSTREAM;
|
|
173
|
+
let source: 'option' | 'env' | 'default' = 'default';
|
|
174
|
+
if (configured !== undefined) {
|
|
175
|
+
raw = configured;
|
|
176
|
+
source = 'option';
|
|
177
|
+
} else if (envUpstream !== undefined) {
|
|
178
|
+
raw = envUpstream;
|
|
179
|
+
source = 'env';
|
|
180
|
+
}
|
|
181
|
+
return { target: raw.replace(/\/$/, ''), source };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Handle `POST /__pyric/ai-proxy/<suffix>`: a same-origin passthrough to the
|
|
186
|
+
* configured OpenAI-compatible upstream, so the browser openai engine
|
|
187
|
+
* (running in the served page or the SharedWorker host) reaches a localhost
|
|
188
|
+
* upstream like Ollama with ZERO CORS setup (no `OLLAMA_ORIGINS`, ever).
|
|
189
|
+
*
|
|
190
|
+
* Behavior:
|
|
191
|
+
* - POST only (the OpenAI chat-completions surface is POST); 405 otherwise.
|
|
192
|
+
* - the path suffix + query ride through verbatim
|
|
193
|
+
* (`/__pyric/ai-proxy/chat/completions` becomes `<upstream>/chat/completions`).
|
|
194
|
+
* - the request body is forwarded verbatim; headers minus the
|
|
195
|
+
* origin-sensitive/hop-by-hop set above (`authorization` DOES forward,
|
|
196
|
+
* since upstreams may require a key).
|
|
197
|
+
* - the response body is STREAMED through chunk-by-chunk. SSE passthrough
|
|
198
|
+
* must never buffer, or `stream: true` completions would arrive all at
|
|
199
|
+
* once at the end.
|
|
200
|
+
* - an unreachable upstream answers 502 with a plain-text explanation.
|
|
201
|
+
* - every upstream failure (unreachable/timeout, non-2xx status, mid-stream
|
|
202
|
+
* abort) ALSO prints a structured warning on the dev server's terminal
|
|
203
|
+
* logger; a stopped Ollama used to fail in total silence. Diagnostics
|
|
204
|
+
* only: nothing here changes what the caller receives. No logger wired
|
|
205
|
+
* means dropped silently (same opt-out contract as the denial relay).
|
|
206
|
+
*/
|
|
207
|
+
export async function handleAiProxy(
|
|
208
|
+
configuredUpstream: string | undefined,
|
|
209
|
+
throttle: AiDiagnosticThrottle,
|
|
210
|
+
req: IncomingMessage,
|
|
211
|
+
res: ServerResponse,
|
|
212
|
+
url: URL,
|
|
213
|
+
logger?: ServeLogger,
|
|
214
|
+
): Promise<void> {
|
|
215
|
+
if (req.method !== 'POST') {
|
|
216
|
+
res.writeHead(405, { allow: 'POST' }).end('method not allowed');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const upstreamBase = resolveAiProxyUpstream(configuredUpstream).target;
|
|
220
|
+
const suffix = url.pathname.slice(AI_PROXY_ROUTE.length);
|
|
221
|
+
const target = `${upstreamBase}${suffix}${url.search}`;
|
|
222
|
+
|
|
223
|
+
// Buffer the REQUEST body (small JSON payloads); the RESPONSE streams.
|
|
224
|
+
const raw = await new Promise<Buffer>((resolve, reject) => {
|
|
225
|
+
const chunks: Buffer[] = [];
|
|
226
|
+
req.on('data', (c: Buffer) => chunks.push(c));
|
|
227
|
+
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
228
|
+
req.on('error', reject);
|
|
229
|
+
});
|
|
230
|
+
// Copy into a plain-ArrayBuffer view; fetch's BodyInit typing rejects
|
|
231
|
+
// Buffer's ArrayBufferLike backing.
|
|
232
|
+
const body = new Uint8Array(raw.byteLength);
|
|
233
|
+
body.set(raw);
|
|
234
|
+
|
|
235
|
+
const headers: Record<string, string> = {};
|
|
236
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
237
|
+
if (value === undefined) continue;
|
|
238
|
+
if (AI_PROXY_STRIPPED_HEADERS.has(key.toLowerCase())) continue;
|
|
239
|
+
headers[key] = Array.isArray(value) ? value.join(', ') : value;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const startedAt = Date.now();
|
|
243
|
+
let upstream: Response;
|
|
244
|
+
try {
|
|
245
|
+
upstream = await fetch(target, { method: 'POST', headers, body });
|
|
246
|
+
} catch (e) {
|
|
247
|
+
const cause = e instanceof Error ? e.message : String(e);
|
|
248
|
+
noteAiProxyFailure(
|
|
249
|
+
{ kind: 'unreachable', target, latencyMs: Date.now() - startedAt, cause },
|
|
250
|
+
throttle,
|
|
251
|
+
logger,
|
|
252
|
+
);
|
|
253
|
+
// The 502 body quotes the same two strings the terminal block does, so it
|
|
254
|
+
// gets the same masking: the target carries the page's own query string,
|
|
255
|
+
// and a fetch error routinely echoes the URL back.
|
|
256
|
+
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
257
|
+
res.end(
|
|
258
|
+
`pyric dev ai-proxy: upstream ${redactUrl(target)} unreachable: ${redactUrl(cause)}\n` +
|
|
259
|
+
'Set PYRIC_AI_PROXY_UPSTREAM to an OpenAI-compatible base URL ' +
|
|
260
|
+
`(default ${AI_PROXY_DEFAULT_UPSTREAM}).`,
|
|
261
|
+
);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// A refusal from a reachable upstream (bad model, missing key, rate limit)
|
|
266
|
+
// is just as invisible to a headless developer as a dead socket: the
|
|
267
|
+
// status rides through to the caller untouched, and is ALSO announced here.
|
|
268
|
+
// `Retry-After` rides along when the upstream sent one (429s and 503s carry
|
|
269
|
+
// it): it is the only place the requested backoff appears, since nothing in
|
|
270
|
+
// this path retries or sleeps.
|
|
271
|
+
if (!upstream.ok) {
|
|
272
|
+
const retryAfter = upstream.headers.get('retry-after');
|
|
273
|
+
const failure: AiProxyFailure = {
|
|
274
|
+
kind: 'status',
|
|
275
|
+
target,
|
|
276
|
+
latencyMs: Date.now() - startedAt,
|
|
277
|
+
status: upstream.status,
|
|
278
|
+
};
|
|
279
|
+
if (retryAfter !== null) failure.retryAfter = retryAfter;
|
|
280
|
+
noteAiProxyFailure(failure, throttle, logger);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const responseHeaders: Record<string, string> = { 'cache-control': 'no-store' };
|
|
284
|
+
const contentType = upstream.headers.get('content-type');
|
|
285
|
+
if (contentType) responseHeaders['content-type'] = contentType;
|
|
286
|
+
res.writeHead(upstream.status, responseHeaders);
|
|
287
|
+
if (!upstream.body) {
|
|
288
|
+
res.end();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
(res as ServerResponse & { flushHeaders?: () => void }).flushHeaders?.();
|
|
292
|
+
|
|
293
|
+
// Chunk-by-chunk passthrough. A dropped client cancels the upstream read
|
|
294
|
+
// so an abandoned SSE stream doesn't keep the upstream generating.
|
|
295
|
+
const reader = upstream.body.getReader();
|
|
296
|
+
let clientGone = false;
|
|
297
|
+
res.on('close', () => {
|
|
298
|
+
clientGone = true;
|
|
299
|
+
void reader.cancel().catch(() => {});
|
|
300
|
+
});
|
|
301
|
+
try {
|
|
302
|
+
for (;;) {
|
|
303
|
+
const { done, value } = await reader.read();
|
|
304
|
+
if (done) break;
|
|
305
|
+
res.write(value);
|
|
306
|
+
}
|
|
307
|
+
} catch (e) {
|
|
308
|
+
// Nothing to salvage for the caller: the response is already committed
|
|
309
|
+
// with the upstream's status, so a truncated body is all it gets. But a
|
|
310
|
+
// half-delivered completion is exactly the failure that used to vanish,
|
|
311
|
+
// so announce it, unless the CLIENT is the one who walked away (a closed
|
|
312
|
+
// tab cancelling an SSE stream is normal, not a fault worth a warning).
|
|
313
|
+
if (!clientGone) {
|
|
314
|
+
noteAiProxyFailure(
|
|
315
|
+
{
|
|
316
|
+
kind: 'stream-abort',
|
|
317
|
+
target,
|
|
318
|
+
latencyMs: Date.now() - startedAt,
|
|
319
|
+
cause: e instanceof Error ? e.message : String(e),
|
|
320
|
+
},
|
|
321
|
+
throttle,
|
|
322
|
+
logger,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
res.end();
|
|
327
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AI line in the dev server's startup banner.
|
|
3
|
+
*
|
|
4
|
+
* AI is the only live service that used to boot silently: hosting, sandbox
|
|
5
|
+
* bundles, rules, Studio, the bridge and persistence all announce themselves,
|
|
6
|
+
* so "nothing about AI" read as "AI isn't part of this server" rather than
|
|
7
|
+
* "AI is here, unconfigured". One description ({@link describeAiStatus}),
|
|
8
|
+
* rendered in the two dev-server flavors, so the `pyric dev` banner and the
|
|
9
|
+
* Vite plugin can never disagree about what AI resolved to.
|
|
10
|
+
*/
|
|
11
|
+
import type { AiEngineConfigWire } from './worker/protocol.js';
|
|
12
|
+
import { GEMINI_DEFAULT_BASE_URL, redactUrl } from 'pyric/ai/internal';
|
|
13
|
+
import { AI_PROXY_ROUTE, resolveAiProxyUpstream } from './ai-proxy.js';
|
|
14
|
+
import { sanitizeForTerminal } from './ai-terminal-text.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* What the dev server itself knows about AI by the time it finishes booting.
|
|
18
|
+
*
|
|
19
|
+
* Everything here is resolved SYNCHRONOUSLY at bootstrap: the plugin's
|
|
20
|
+
* `ai.engine`/`ai.model` (already reduced to the worker wire shape) and the
|
|
21
|
+
* proxy upstream. Nothing in this shape forces a broker, a page, or an
|
|
22
|
+
* upstream connection into existence just to be reported. The engine is
|
|
23
|
+
* instantiated lazily, on the first `ai.*` op, in the page or the worker.
|
|
24
|
+
*/
|
|
25
|
+
export interface AiStartupStatus {
|
|
26
|
+
/** Engine resolved by the dev server (the Vite plugin's `ai.engine` /
|
|
27
|
+
* `ai.model`). Absent means nothing server-side: the served page's own
|
|
28
|
+
* `getAI(...)` chooses the engine at runtime, and the server cannot know
|
|
29
|
+
* which (there is no CLI surface for it under `pyric dev`). */
|
|
30
|
+
engine?: AiEngineConfigWire;
|
|
31
|
+
/** `ai.mode`: `production` passes through to Google AI instead of mirroring. */
|
|
32
|
+
mode?: 'sandbox' | 'production';
|
|
33
|
+
/** Configured OpenAI-compatible upstream (`ai.proxyUpstream`). Absent falls
|
|
34
|
+
* back to `PYRIC_AI_PROXY_UPSTREAM`, then the local-Ollama default. */
|
|
35
|
+
proxyUpstream?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Where the resolved proxy upstream came from, in the terminal's words. The
|
|
39
|
+
* provenance is the point of the line: a developer has to be able to tell a
|
|
40
|
+
* deliberate upstream from the Ollama default nobody chose, so every source
|
|
41
|
+
* names the knob that set it. */
|
|
42
|
+
function describeUpstreamProvenance(source: 'option' | 'env' | 'default'): string {
|
|
43
|
+
if (source === 'option') return ' (ai.proxyUpstream)';
|
|
44
|
+
if (source === 'env') return ' (PYRIC_AI_PROXY_UPSTREAM)';
|
|
45
|
+
return ' (default)';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The openai engine's model binding, which may not be pinned at all. */
|
|
49
|
+
function describeOpenAiModel(model: string | undefined): string {
|
|
50
|
+
if (model === undefined) return 'no model pinned';
|
|
51
|
+
return `model ${sanitizeForTerminal(model)}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Mark + body for one AI status line, in both terminal flavors. */
|
|
55
|
+
function describeAiStatus(status: AiStartupStatus): { mark: string; body: string } {
|
|
56
|
+
const upstream = resolveAiProxyUpstream(status.proxyUpstream);
|
|
57
|
+
const provenance = describeUpstreamProvenance(upstream.source);
|
|
58
|
+
const proxyChain = `${AI_PROXY_ROUTE} → ${redactUrl(upstream.target)}${provenance}`;
|
|
59
|
+
|
|
60
|
+
const engine = status.engine;
|
|
61
|
+
if (engine === undefined) {
|
|
62
|
+
// Visible absence, the `• rules no firestore.rules ...` idiom: AI
|
|
63
|
+
// silence used to be indistinguishable from AI-not-wired-up.
|
|
64
|
+
return {
|
|
65
|
+
mark: '•',
|
|
66
|
+
body: `no engine configured; the served page's getAI() picks one; ${proxyChain}`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (engine.kind === 'openai') {
|
|
70
|
+
const model = describeOpenAiModel(engine.model);
|
|
71
|
+
const baseUrl = engine.baseUrl ?? AI_PROXY_ROUTE;
|
|
72
|
+
let endpoint = proxyChain;
|
|
73
|
+
if (baseUrl !== AI_PROXY_ROUTE) {
|
|
74
|
+
endpoint = `${redactUrl(baseUrl)} (direct, bypasses ${AI_PROXY_ROUTE})`;
|
|
75
|
+
}
|
|
76
|
+
return { mark: '✔', body: `openai (${model}) → ${endpoint}` };
|
|
77
|
+
}
|
|
78
|
+
if (engine.kind === 'gemini') {
|
|
79
|
+
let passthrough = '';
|
|
80
|
+
if (status.mode === 'production') passthrough = 'production passthrough, ';
|
|
81
|
+
const hasKey = engine.apiKey !== undefined && engine.apiKey !== '';
|
|
82
|
+
let key = 'no API key; set GEMINI_API_KEY';
|
|
83
|
+
if (hasKey) key = 'API key set';
|
|
84
|
+
let endpoint = GEMINI_DEFAULT_BASE_URL;
|
|
85
|
+
if (engine.baseUrl !== undefined) endpoint = redactUrl(engine.baseUrl);
|
|
86
|
+
return { mark: '✔', body: `gemini (${passthrough}${key}) → ${endpoint}` };
|
|
87
|
+
}
|
|
88
|
+
const responses = engine.script?.length ?? 0;
|
|
89
|
+
return { mark: '✔', body: `scripted (${responses} canned response(s), no network)` };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The banner line for `pyric dev`'s aligned status column (`✔ hosting ...`).
|
|
94
|
+
* Exported for unit tests.
|
|
95
|
+
*/
|
|
96
|
+
export function formatAiStatusLine(status: AiStartupStatus): string {
|
|
97
|
+
const { mark, body } = describeAiStatus(status);
|
|
98
|
+
return `${mark} ai ${body}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The same report in the Vite dev server's flavor (` ✔ [pyric] ...`, the
|
|
103
|
+
* idiom `vite-functions-development.ts` prints). Same content, so the two
|
|
104
|
+
* dev-server front doors never disagree about what AI resolved to.
|
|
105
|
+
*/
|
|
106
|
+
export function formatAiStatusNote(status: AiStartupStatus): string {
|
|
107
|
+
const { mark, body } = describeAiStatus(status);
|
|
108
|
+
return ` ${mark} [pyric] ai: ${body}`;
|
|
109
|
+
}
|