@iislee/opencodex 2.11.0 → 2.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +109 -0
- package/README.md +114 -19
- package/bin/ocx.mjs +164 -36
- package/bin/package-main.mjs +1 -1
- package/gui/dist/assets/index-BF38heuV.js +104 -0
- package/gui/dist/assets/index-DMiI18Kv.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +19 -10
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +376 -52
- package/src/adapters/base.ts +54 -7
- package/src/adapters/client-fingerprint.ts +18 -12
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +601 -0
- package/src/adapters/cursor/checkpoint-store.ts +303 -0
- package/src/adapters/cursor/cursor-errors.ts +129 -6
- package/src/adapters/cursor/discovery.ts +61 -4
- package/src/adapters/cursor/effort-map.ts +27 -3
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/h2-pool.ts +123 -0
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/images.ts +704 -0
- package/src/adapters/cursor/live-models.ts +174 -58
- package/src/adapters/cursor/live-transport.ts +609 -170
- package/src/adapters/cursor/native-exec-common.ts +23 -2
- package/src/adapters/cursor/native-exec-fs.ts +10 -7
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +0 -1
- package/src/adapters/cursor/native-exec.ts +101 -14
- package/src/adapters/cursor/protobuf-events.ts +829 -11
- package/src/adapters/cursor/protobuf-request.ts +383 -65
- package/src/adapters/cursor/request-builder.ts +237 -31
- package/src/adapters/cursor/tool-definitions.ts +242 -12
- package/src/adapters/cursor/tool-result-normalize.ts +92 -0
- package/src/adapters/cursor/transport.ts +22 -0
- package/src/adapters/cursor/types.ts +28 -1
- package/src/adapters/cursor.ts +133 -7
- package/src/adapters/google-antigravity-replay.ts +558 -34
- package/src/adapters/google-antigravity-wire.ts +43 -10
- package/src/adapters/google-http.ts +38 -10
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +618 -74
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +18 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +92 -54
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1311 -336
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +830 -56
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +104 -21
- package/src/adapters/xai-web-search.ts +185 -0
- package/src/bridge.ts +428 -61
- package/src/chat/inbound.ts +43 -19
- package/src/chat/outbound.ts +82 -26
- package/src/claude/agents-inject.ts +32 -9
- package/src/claude/context-windows.ts +21 -5
- package/src/claude/desktop-3p.ts +243 -9
- package/src/claude/gateway-cache.ts +41 -4
- package/src/claude/inbound.ts +72 -3
- package/src/claude/model-info.ts +38 -15
- package/src/claude/outbound.ts +70 -16
- package/src/cli/account-api.ts +35 -1
- package/src/cli/account-auth.ts +33 -6
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +389 -2
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +30 -6
- package/src/cli/agent.ts +45 -1
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +29 -4
- package/src/cli/claude.ts +103 -34
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +76 -0
- package/src/cli/combo.ts +8 -0
- package/src/cli/config-command.ts +74 -10
- package/src/cli/dispatch.ts +593 -0
- package/src/cli/doctor.ts +315 -43
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +46 -20
- package/src/cli/help.ts +20 -271
- package/src/cli/index.ts +378 -519
- package/src/cli/init.ts +4 -17
- package/src/cli/integrations.ts +120 -2
- package/src/cli/lab.ts +607 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/minimax.ts +497 -0
- package/src/cli/models-runtime.ts +35 -2
- package/src/cli/models.ts +100 -14
- package/src/cli/observe.ts +92 -3
- package/src/cli/opencode.ts +4 -2
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/provider.ts +24 -3
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +437 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +10 -3
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/cli/v2.ts +105 -10
- package/src/cli.ts +1 -1
- package/src/clients/config-export.ts +1358 -21
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-lifecycle.ts +130 -13
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/account-store.ts +29 -2
- package/src/codex/account-usability.ts +25 -2
- package/src/codex/admission.ts +256 -0
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/app-server-processes.ts +493 -106
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +849 -242
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/auth-context.ts +345 -32
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +68 -10
- package/src/codex/catalog/bundled.ts +331 -33
- package/src/codex/catalog/effort.ts +121 -30
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +529 -45
- package/src/codex/catalog/native-models.ts +72 -0
- package/src/codex/catalog/parsing.ts +224 -30
- package/src/codex/catalog/provider-fetch.ts +1460 -134
- package/src/codex/catalog/sync.ts +1449 -186
- package/src/codex/catalog-admission.ts +199 -0
- package/src/codex/catalog-refresh-status.ts +105 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +6 -3
- package/src/codex/codex-write-lock.ts +384 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +651 -0
- package/src/codex/coordinator-doctor.ts +332 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/features.ts +636 -39
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +407 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-migration-guardian.ts +26 -20
- package/src/codex/history-provider.ts +231 -28
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +220 -0
- package/src/codex/inject-coordination.ts +290 -0
- package/src/codex/inject.ts +1073 -152
- package/src/codex/injected-marker.ts +37 -3
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +66 -4
- package/src/codex/log-guard/inspect.ts +506 -0
- package/src/codex/log-guard/lock.ts +150 -0
- package/src/codex/log-guard/maintenance.ts +403 -0
- package/src/codex/log-guard/path-safety.ts +88 -0
- package/src/codex/log-guard/policy.ts +44 -0
- package/src/codex/log-guard/processes.ts +205 -0
- package/src/codex/log-guard/protection.ts +489 -0
- package/src/codex/log-guard/sqlite-errors.ts +9 -0
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/main-account.ts +29 -1
- package/src/codex/management-convergence.ts +167 -0
- package/src/codex/model-cache.ts +56 -10
- package/src/codex/model-entitlements.ts +353 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +178 -0
- package/src/codex/native-main-lock-file.ts +162 -0
- package/src/codex/native-main-owner.ts +329 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1531 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +492 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +682 -0
- package/src/codex/paths.ts +80 -1
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +40 -0
- package/src/codex/plugins-doctor.ts +1 -1
- package/src/codex/pool-rotation.ts +74 -4
- package/src/codex/project-config-warnings.ts +20 -6
- package/src/codex/prompt-journal.ts +352 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +175 -13
- package/src/codex/refresh.ts +11 -2
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +505 -94
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +1009 -28
- package/src/codex/subagent-model-fallback.ts +350 -35
- package/src/codex/sync.ts +191 -2
- package/src/codex/transition-state.ts +612 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +557 -0
- package/src/codex/warmup.ts +187 -81
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +20 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/request.ts +32 -0
- package/src/combos/types.ts +81 -9
- package/src/config/provider-name.ts +24 -0
- package/src/config.ts +1762 -140
- package/src/generated/compatibility-version.json +3164 -0
- package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
- package/src/grok/inject.ts +16 -5
- package/src/grok/inspect.ts +45 -0
- package/src/grok/sync.ts +2 -2
- package/src/images/loop.ts +152 -29
- package/src/images/plan.ts +23 -13
- package/src/integrations/config-io.ts +269 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/omp-yaml-source.ts +358 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +159 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +361 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +691 -0
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +499 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +64 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +164 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +68 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +214 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +229 -0
- package/src/lab/events/validate.ts +781 -0
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +489 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +54 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +310 -0
- package/src/lab/ledger/store.ts +532 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +336 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +118 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +301 -0
- package/src/lab/observe/from-live.ts +117 -0
- package/src/lab/paths.ts +153 -0
- package/src/lab/projection/rebuild.ts +495 -0
- package/src/lab/projection/schema.ts +135 -0
- package/src/lab/projection/verdicts.ts +474 -0
- package/src/lab/projection/verification.ts +412 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +243 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lab/query/catalog.ts +101 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +45 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +112 -0
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +74 -0
- package/src/lib/app-owned-memory-stores.ts +22 -0
- package/src/lib/bounded-body.ts +153 -9
- package/src/lib/bun-runtime.ts +125 -12
- package/src/lib/bun-stream-caps.ts +13 -9
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/config-ownership.ts +6 -2
- package/src/lib/destination-policy.ts +65 -1
- package/src/lib/errors.ts +44 -2
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/lab-activation.ts +223 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +56 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/lab-passive-linker-registration.ts +26 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/local-provider-reload-contract.ts +100 -0
- package/src/lib/optional-shutdown-hooks.ts +57 -0
- package/src/lib/pinned-http.ts +145 -26
- package/src/lib/process-control.ts +4 -1
- package/src/lib/provider-outbound.ts +49 -9
- package/src/lib/redact.ts +419 -3
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/shadow-call.ts +35 -4
- package/src/lib/sse-decoder.ts +41 -0
- package/src/lib/state-store-registrations.ts +10 -2
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/token-estimate.ts +19 -2
- package/src/lib/tool-argument-integers.ts +202 -0
- package/src/lib/translator-budget.ts +44 -0
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-reachability.ts +95 -0
- package/src/lib/upstream-retry.ts +156 -3
- package/src/lib/windows-atomic-replace.ts +155 -0
- package/src/lib/windows-elevation.ts +70 -2
- package/src/lib/windows-secret-acl.ts +409 -69
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/lib/windows-text.ts +106 -0
- package/src/lib/windows-user-principal.ts +341 -0
- package/src/lib/winsw.ts +33 -5
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic.ts +12 -1
- package/src/oauth/callback-server.ts +8 -2
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +46 -5
- package/src/oauth/google-antigravity.ts +35 -3
- package/src/oauth/health.ts +20 -12
- package/src/oauth/index.ts +398 -66
- package/src/oauth/key-providers.ts +16 -0
- package/src/oauth/kimi.ts +16 -2
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/local-token-detect.ts +11 -2
- package/src/oauth/log.ts +3 -1
- package/src/oauth/login-cli.ts +88 -28
- package/src/oauth/nous.ts +798 -0
- package/src/oauth/store.ts +119 -21
- package/src/oauth/token-guardian.ts +9 -3
- package/src/pi/models.ts +2 -2
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/antigravity-models.ts +521 -31
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +144 -0
- package/src/providers/context-cap.ts +22 -5
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/derive.ts +253 -6
- package/src/providers/fastwire.ts +501 -0
- package/src/providers/free-directory.ts +10 -7
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +115 -22
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/openai-sidecar.ts +72 -4
- package/src/providers/openai-tier-startup.ts +31 -2
- package/src/providers/openai-tiers.ts +119 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +29 -0
- package/src/providers/quota.ts +1319 -38
- package/src/providers/registry.ts +1429 -111
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +42 -6
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/providers/xai-transport.ts +11 -4
- package/src/reasoning-effort.ts +49 -1
- package/src/responses/compaction.ts +26 -1
- package/src/responses/custom-tool-compat.ts +266 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +355 -0
- package/src/responses/parser.ts +220 -38
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +426 -0
- package/src/responses/schema.ts +7 -1
- package/src/responses/spill-store.ts +75 -10
- package/src/responses/state.ts +565 -27
- package/src/responses/thought-signature-replay.ts +347 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/router.ts +366 -30
- package/src/routing/analytics.ts +378 -0
- package/src/routing/capability.ts +244 -0
- package/src/routing/compatibility/assemble.ts +73 -0
- package/src/routing/compatibility/behavior.ts +278 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/provider-slot.ts +56 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +495 -0
- package/src/routing/health.ts +412 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +605 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +547 -0
- package/src/routing/quota.ts +145 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +776 -0
- package/src/server/adapter-resolve.ts +2 -29
- package/src/server/auth-cors.ts +267 -46
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +130 -56
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +426 -0
- package/src/server/claude-messages.ts +159 -43
- package/src/server/direct-local-http.ts +347 -0
- package/src/server/effort-policy.ts +18 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +94 -12
- package/src/server/index.ts +865 -181
- package/src/server/lifecycle.ts +284 -13
- package/src/server/live.ts +136 -17
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/local-provider-reload-client.ts +137 -0
- package/src/server/management/agent-settings-routes.ts +398 -103
- package/src/server/management/api-key-usage.ts +31 -5
- package/src/server/management/body.ts +6 -0
- package/src/server/management/combo-routes.ts +62 -24
- package/src/server/management/config-routes.ts +464 -51
- package/src/server/management/context.ts +80 -2
- package/src/server/management/integration-routes.ts +498 -0
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +563 -0
- package/src/server/management/logs-usage-routes.ts +101 -32
- package/src/server/management/model-routes.ts +189 -131
- package/src/server/management/model-rows.ts +163 -0
- package/src/server/management/native-integration-routes.ts +769 -0
- package/src/server/management/oauth-account-routes.ts +80 -4
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +764 -157
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +380 -0
- package/src/server/management/shared.ts +27 -11
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/storage-log-guard-routes.ts +186 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +288 -32
- package/src/server/management/system-routes.ts +77 -0
- package/src/server/management/usage-summary-cache.ts +9 -1
- package/src/server/management/vision-sidecar-options.ts +167 -0
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/management-api.ts +115 -14
- package/src/server/management-auth.ts +220 -5
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +132 -5
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +236 -76
- package/src/server/request-decompress.ts +113 -6
- package/src/server/request-log.ts +235 -22
- package/src/server/responses/agent-task-recovery-cache.ts +143 -0
- package/src/server/responses/agent-task-recovery.ts +465 -0
- package/src/server/responses/collaboration.ts +204 -35
- package/src/server/responses/compact.ts +442 -55
- package/src/server/responses/core.ts +2872 -331
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/encrypted-payload.ts +62 -39
- package/src/server/responses/fetch-helpers.ts +79 -4
- package/src/server/responses/input-admission.ts +185 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +251 -0
- package/src/server/responses/terminal-guard.ts +26 -5
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses/ws-upstream.ts +308 -0
- package/src/server/responses-custom-tool-repair.ts +282 -0
- package/src/server/responses-item-id-repair.ts +54 -6
- package/src/server/responses-json-events.ts +90 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +153 -0
- package/src/server/responses.ts +18 -2
- package/src/server/search.ts +78 -13
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +110 -22
- package/src/server/startup-action-control.ts +8 -1
- package/src/server/startup-health-cache.ts +19 -1
- package/src/server/system-env.ts +80 -9
- package/src/server/ws-bridge.ts +39 -38
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +1111 -90
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/storage/cleanup.ts +2 -2
- package/src/storage/scanner.ts +1 -1
- package/src/storage/worker-lifecycle.ts +14 -14
- package/src/tray/windows-tray.ps1 +83 -9
- package/src/tray/windows.ts +43 -16
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +845 -0
- package/src/types/provider.ts +545 -0
- package/src/types/request.ts +384 -0
- package/src/types/tools.ts +131 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +104 -1236
- package/src/update/index.ts +32 -19
- package/src/update/job.ts +442 -67
- package/src/update/notify.ts +12 -6
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +259 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +268 -16
- package/src/usage/log.ts +606 -41
- package/src/usage/summary.ts +177 -9
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/anthropic-describe.ts +10 -6
- package/src/vision/backends.ts +97 -0
- package/src/vision/describe.ts +9 -3
- package/src/vision/eligibility.ts +250 -0
- package/src/vision/index.ts +238 -24
- package/src/vision/reasoning.ts +55 -0
- package/src/vision/routed-describe.ts +175 -0
- package/src/vision/timeout-bounds.ts +9 -0
- package/src/web-search/anthropic-executor.ts +13 -7
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +88 -0
- package/src/web-search/executor.ts +11 -3
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +150 -15
- package/src/web-search/loop.ts +279 -50
- package/src/web-search/parse.ts +125 -30
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/xai-executor.ts +219 -0
- package/gui/dist/assets/index-DTpMHS4F.js +0 -67
- package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
- package/src/cli/internal-dispatch.ts +0 -20
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text serializers for client config documents.
|
|
3
|
+
*
|
|
4
|
+
* Bun 1.3.14 gives us JSON and JSON5 stringify natively, but the two formats a
|
|
5
|
+
* client config actually needs beyond JSON are hand-rendered here:
|
|
6
|
+
*
|
|
7
|
+
* - **TOML** has no `Bun.TOML.stringify` at all.
|
|
8
|
+
* - **YAML** has `Bun.YAML.stringify`, but it emits flow style with no trailing
|
|
9
|
+
* newline (`{a: 1,b: {c: d}}`). That is valid YAML and completely wrong for a
|
|
10
|
+
* file a user opens, so we render block style ourselves.
|
|
11
|
+
*
|
|
12
|
+
* The renderers originally covered only the shallow shapes our builders emit.
|
|
13
|
+
* That was fine while they only ever saw generated documents — but the
|
|
14
|
+
* integration writer parses the USER's whole config and re-serializes it, so a
|
|
15
|
+
* legitimate `null` or a numeric array in a file we never wrote threw straight
|
|
16
|
+
* out of the writer and surfaced as a 500. They now cover the value domains
|
|
17
|
+
* their formats actually allow.
|
|
18
|
+
*
|
|
19
|
+
* What they still refuse is genuinely unrenderable: a function, a symbol, a
|
|
20
|
+
* non-finite number. Those must fail loudly rather than produce bytes a client
|
|
21
|
+
* might misread — and the writer turns the failure into a structured refusal
|
|
22
|
+
* rather than an exception.
|
|
23
|
+
*
|
|
24
|
+
* Design of record: devlog/_fin/260802_client_toggle_api/011_wp1_builders.md.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export type ConfigFormat = "json" | "yaml" | "toml" | "json5";
|
|
28
|
+
|
|
29
|
+
/** Thrown when a document holds a value its format cannot represent. */
|
|
30
|
+
export class UnserializableValueError extends Error {}
|
|
31
|
+
|
|
32
|
+
export const FORMAT_MEDIA_TYPE: Record<ConfigFormat, string> = {
|
|
33
|
+
json: "application/json",
|
|
34
|
+
yaml: "application/yaml",
|
|
35
|
+
toml: "application/toml",
|
|
36
|
+
json5: "application/json5",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Type guard, not a boolean check — the renderers walk `unknown`, so without
|
|
41
|
+
* the predicate every narrowed branch stays `unknown`.
|
|
42
|
+
*/
|
|
43
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
44
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isYamlScalar(value: unknown): value is string | number | boolean | null {
|
|
48
|
+
return value === null
|
|
49
|
+
|| typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Keep only unambiguous, single-token strings plain. Everything else uses a
|
|
54
|
+
* JSON-compatible double-quoted scalar, which gives deterministic escaping —
|
|
55
|
+
* JSON string escaping is valid YAML 1.2 string escaping.
|
|
56
|
+
*/
|
|
57
|
+
function yamlString(value: string): string {
|
|
58
|
+
const plainSafe = value.length > 0
|
|
59
|
+
&& value.trim() === value
|
|
60
|
+
&& /^[A-Za-z_./][A-Za-z0-9_./-]*$/u.test(value)
|
|
61
|
+
&& !/^(?:null|true|false|yes|no|on|off|~|\.nan|[-+]?\.inf)$/iu.test(value);
|
|
62
|
+
return plainSafe ? value : JSON.stringify(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function yamlScalar(value: string | number | boolean | null): string {
|
|
66
|
+
// `null` renders as the explicit `null` token rather than an empty value:
|
|
67
|
+
// a bare `key:` is also null in YAML, but round-tripping it through other
|
|
68
|
+
// parsers is less predictable than saying so.
|
|
69
|
+
if (value === null) return "null";
|
|
70
|
+
if (typeof value === "string") return yamlString(value);
|
|
71
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
72
|
+
if (Number.isFinite(value)) return String(value);
|
|
73
|
+
throw new UnserializableValueError(`YAML cannot represent the number ${String(value)}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function yamlEmptyCollection(value: unknown): "[]" | "{}" | undefined {
|
|
77
|
+
if (Array.isArray(value) && value.length === 0) return "[]";
|
|
78
|
+
if (isPlainRecord(value) && Object.keys(value).length === 0) return "{}";
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function yamlMapEntryLines(key: string, value: unknown, indent: number): string[] {
|
|
83
|
+
const padding = " ".repeat(indent);
|
|
84
|
+
const renderedKey = yamlString(key);
|
|
85
|
+
if (isYamlScalar(value)) return [`${padding}${renderedKey}: ${yamlScalar(value)}`];
|
|
86
|
+
const empty = yamlEmptyCollection(value);
|
|
87
|
+
if (empty !== undefined) return [`${padding}${renderedKey}: ${empty}`];
|
|
88
|
+
if (Array.isArray(value) || isPlainRecord(value)) {
|
|
89
|
+
return [`${padding}${renderedKey}:`, ...yamlLines(value, indent + 2)];
|
|
90
|
+
}
|
|
91
|
+
throw new UnserializableValueError(`YAML cannot represent the value at key ${JSON.stringify(key)}: ${describeValue(value)}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A map inside a sequence: the first key rides the dash, the rest indent under it. */
|
|
95
|
+
function yamlArrayMapLines(value: Record<string, unknown>, indent: number): string[] {
|
|
96
|
+
const entries = Object.entries(value);
|
|
97
|
+
const padding = " ".repeat(indent);
|
|
98
|
+
if (entries.length === 0) return [`${padding}- {}`];
|
|
99
|
+
|
|
100
|
+
const [first, ...rest] = entries;
|
|
101
|
+
const [firstKey, firstValue] = first!;
|
|
102
|
+
const renderedFirstKey = yamlString(firstKey);
|
|
103
|
+
const firstEmpty = yamlEmptyCollection(firstValue);
|
|
104
|
+
const lines: string[] = [];
|
|
105
|
+
if (isYamlScalar(firstValue)) {
|
|
106
|
+
lines.push(`${padding}- ${renderedFirstKey}: ${yamlScalar(firstValue)}`);
|
|
107
|
+
} else if (firstEmpty !== undefined) {
|
|
108
|
+
lines.push(`${padding}- ${renderedFirstKey}: ${firstEmpty}`);
|
|
109
|
+
} else if (Array.isArray(firstValue) || isPlainRecord(firstValue)) {
|
|
110
|
+
lines.push(`${padding}- ${renderedFirstKey}:`);
|
|
111
|
+
lines.push(...yamlLines(firstValue, indent + 4));
|
|
112
|
+
} else {
|
|
113
|
+
throw new UnserializableValueError(`YAML cannot represent the value at key ${JSON.stringify(firstKey)}: ${describeValue(firstValue)}`);
|
|
114
|
+
}
|
|
115
|
+
for (const [key, child] of rest) lines.push(...yamlMapEntryLines(key, child, indent + 2));
|
|
116
|
+
return lines;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function yamlLines(value: unknown, indent: number): string[] {
|
|
120
|
+
const padding = " ".repeat(indent);
|
|
121
|
+
if (isYamlScalar(value)) return [`${padding}${yamlScalar(value)}`];
|
|
122
|
+
if (Array.isArray(value)) {
|
|
123
|
+
if (value.length === 0) return [`${padding}[]`];
|
|
124
|
+
return value.flatMap(item => {
|
|
125
|
+
if (isYamlScalar(item)) return [`${padding}- ${yamlScalar(item)}`];
|
|
126
|
+
if (isPlainRecord(item)) return yamlArrayMapLines(item, indent);
|
|
127
|
+
/*
|
|
128
|
+
* A nested sequence is ordinary YAML (`matrix: [[1,2],[3,4]]`). This
|
|
129
|
+
* used to throw a plain Error carrying `String(item)` — untyped, so the
|
|
130
|
+
* writer could not turn it into a refusal and it surfaced as a 500, and
|
|
131
|
+
* carrying the value's CONTENTS into the message, which a config may
|
|
132
|
+
* not want repeated.
|
|
133
|
+
*/
|
|
134
|
+
if (Array.isArray(item)) {
|
|
135
|
+
if (item.length === 0) return [`${padding}- []`];
|
|
136
|
+
return [`${padding}-`, ...yamlLines(item, indent + 2)];
|
|
137
|
+
}
|
|
138
|
+
throw new UnserializableValueError(
|
|
139
|
+
`YAML cannot represent this sequence item: ${describeValue(item)}`,
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (isPlainRecord(value)) {
|
|
144
|
+
const entries = Object.entries(value);
|
|
145
|
+
if (entries.length === 0) return [`${padding}{}`];
|
|
146
|
+
return entries.flatMap(([key, child]) => yamlMapEntryLines(key, child, indent));
|
|
147
|
+
}
|
|
148
|
+
throw new UnserializableValueError(`YAML cannot represent this value: ${describeValue(value)}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Block-style YAML for the shallow shapes we emit. Throws on anything else. */
|
|
152
|
+
export function renderYaml(value: unknown, indent = 0): string {
|
|
153
|
+
if (!Number.isInteger(indent) || indent < 0) {
|
|
154
|
+
throw new Error(`YAML indent must be a non-negative integer: ${String(indent)}`);
|
|
155
|
+
}
|
|
156
|
+
return `${yamlLines(value, indent).join("\n")}\n`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** A short, safe description for an error message — never the value's contents. */
|
|
160
|
+
function describeValue(value: unknown): string {
|
|
161
|
+
if (value === null) return "null";
|
|
162
|
+
if (Array.isArray(value)) return "an array";
|
|
163
|
+
if (typeof value === "object") return "an object";
|
|
164
|
+
return typeof value;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** TOML basic-string escape. Mirrors `tomlString` in src/grok/inject.ts. */
|
|
168
|
+
export function tomlString(value: string): string {
|
|
169
|
+
return JSON.stringify(value);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Bare key when safe, basic-string key otherwise (TOML 1.0 §Keys). */
|
|
173
|
+
export function quoteTomlKey(key: string): string {
|
|
174
|
+
return /^[A-Za-z0-9_-]+$/.test(key) ? key : tomlString(key);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function tomlScalar(value: unknown): string {
|
|
178
|
+
if (typeof value === "string") return tomlString(value);
|
|
179
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
180
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
181
|
+
/*
|
|
182
|
+
* Arrays of ANY scalar, not just strings. The string-only check was written
|
|
183
|
+
* against our own builder output; a user's config legitimately holds
|
|
184
|
+
* `ports = [1, 2]`, and refusing it threw out of the writer as a 500.
|
|
185
|
+
* TOML permits mixed-type arrays since 1.0, so the elements are rendered
|
|
186
|
+
* individually rather than required to share a type.
|
|
187
|
+
*/
|
|
188
|
+
if (Array.isArray(value)) {
|
|
189
|
+
return `[${value.map(item => tomlScalar(item)).join(", ")}]`;
|
|
190
|
+
}
|
|
191
|
+
/*
|
|
192
|
+
* An inline table is valid TOML (`items = [{ x = 1 }]`), and rendering one
|
|
193
|
+
* inline is straightforward — but only where the whole array can be
|
|
194
|
+
* rendered, so this stays a scalar-position renderer rather than silently
|
|
195
|
+
* flattening a table into a string.
|
|
196
|
+
*/
|
|
197
|
+
if (isPlainRecord(value)) {
|
|
198
|
+
const entries = Object.entries(value);
|
|
199
|
+
return `{ ${entries.map(([k, v]) => `${quoteTomlKey(k)} = ${tomlScalar(v)}`).join(", ")} }`;
|
|
200
|
+
}
|
|
201
|
+
throw new UnserializableValueError(`TOML cannot represent this value: ${describeValue(value)}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Render `{ [table]: { key: scalar } }` as TOML. Scalars come first, then
|
|
206
|
+
* tables in insertion order, so two calls with the same document produce
|
|
207
|
+
* identical bytes — the stability guarantee `normalizeExportModels` exists to
|
|
208
|
+
* protect.
|
|
209
|
+
*/
|
|
210
|
+
export function renderToml(document: Record<string, unknown>, prefix = ""): string {
|
|
211
|
+
const scalars: string[] = [];
|
|
212
|
+
const tables: string[] = [];
|
|
213
|
+
for (const [key, value] of Object.entries(document)) {
|
|
214
|
+
const path = prefix ? `${prefix}.${quoteTomlKey(key)}` : quoteTomlKey(key);
|
|
215
|
+
if (isPlainRecord(value)) {
|
|
216
|
+
tables.push(`[${path}]\n${renderToml(value, path)}`.trimEnd());
|
|
217
|
+
} else {
|
|
218
|
+
scalars.push(`${quoteTomlKey(key)} = ${tomlScalar(value)}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return `${[scalars.join("\n"), tables.join("\n\n")].filter(Boolean).join("\n\n")}\n`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Ceiling on container nesting for json documents, shared by the parse-time
|
|
226
|
+
* scanner (config-io.ts) and the serializer walk below. One constant on
|
|
227
|
+
* purpose: the walk must accept every document the scanner admits, or a file
|
|
228
|
+
* the classifier reported as recoverable would refuse at rewrite time. Real
|
|
229
|
+
* configs nest a handful of levels.
|
|
230
|
+
*/
|
|
231
|
+
export const MAX_JSON_NESTING = 1000;
|
|
232
|
+
|
|
233
|
+
/** Error messages carry the path to the offending value; keep them readable. */
|
|
234
|
+
function clampPath(path: string): string {
|
|
235
|
+
return path.length > 200 ? `${path.slice(0, 100)}…${path.slice(-100)}` : path;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* JSON.stringify writes a non-finite number as `null` and -0 as `0`; any
|
|
240
|
+
* other finite double round-trips value-exactly (literal-level rounding is
|
|
241
|
+
* the parse-time scanner's concern), so those two are exactly what this walk
|
|
242
|
+
* refuses — refusing more turned a state the classifier had promised as
|
|
243
|
+
* recoverable into a permanent refusal. Documents read from disk are already
|
|
244
|
+
* guarded at parse time, and the writer's merge layer JSON-clones documents —
|
|
245
|
+
* normalizing these values — before serializing, so on the apply/disable path
|
|
246
|
+
* this walk is unreachable for them: it guards the direct serializers
|
|
247
|
+
* (preview/export builders), same posture as the YAML and TOML renderers
|
|
248
|
+
* above, and enforces the nesting ceiling for every json caller before the
|
|
249
|
+
* recursive JSON.stringify can turn depth into a RangeError.
|
|
250
|
+
*
|
|
251
|
+
* Iterative frames instead of recursion or a node stack: depth AND size of
|
|
252
|
+
* the document are inputs under the writer of the config file. Recursion made
|
|
253
|
+
* a deep file a RangeError-500; materializing every node with its path made a
|
|
254
|
+
* wide file allocate a large multiple of its size. Frames keep memory
|
|
255
|
+
* proportional to nesting depth, and path strings exist only for the
|
|
256
|
+
* containers on the current path plus the failing value itself.
|
|
257
|
+
*/
|
|
258
|
+
function assertJsonNumbersRoundTrip(document: unknown, rootPath: string): void {
|
|
259
|
+
const refuse = (value: number, path: string): never => {
|
|
260
|
+
throw new UnserializableValueError(Object.is(value, -0)
|
|
261
|
+
? `JSON cannot rewrite -0 at ${clampPath(path)} without changing it to 0`
|
|
262
|
+
: `JSON cannot rewrite the number at ${clampPath(path)} without changing it to null`);
|
|
263
|
+
};
|
|
264
|
+
if (typeof document === "number" && (!Number.isFinite(document) || Object.is(document, -0))) {
|
|
265
|
+
refuse(document, rootPath);
|
|
266
|
+
}
|
|
267
|
+
type Frame = { container: unknown; keys: string[] | null; index: number; prefix: string };
|
|
268
|
+
const frames: Frame[] = [];
|
|
269
|
+
const pushContainer = (value: unknown, prefix: string) => {
|
|
270
|
+
if (Array.isArray(value)) frames.push({ container: value, keys: null, index: 0, prefix });
|
|
271
|
+
else if (isPlainRecord(value)) frames.push({ container: value, keys: Object.keys(value), index: 0, prefix });
|
|
272
|
+
};
|
|
273
|
+
pushContainer(document, rootPath);
|
|
274
|
+
while (frames.length > 0) {
|
|
275
|
+
const frame = frames[frames.length - 1]!;
|
|
276
|
+
const length = frame.keys ? frame.keys.length : (frame.container as unknown[]).length;
|
|
277
|
+
if (frame.index >= length) { frames.pop(); continue; }
|
|
278
|
+
const i = frame.index;
|
|
279
|
+
frame.index += 1;
|
|
280
|
+
const child = frame.keys
|
|
281
|
+
? (frame.container as Record<string, unknown>)[frame.keys[i]!]
|
|
282
|
+
: (frame.container as unknown[])[i];
|
|
283
|
+
const childPath = () => frame.keys
|
|
284
|
+
? (frame.prefix === "$" ? frame.keys[i]! : `${frame.prefix}.${frame.keys[i]!}`)
|
|
285
|
+
: `${frame.prefix}[${i}]`;
|
|
286
|
+
if (typeof child === "number") {
|
|
287
|
+
if (!Number.isFinite(child) || Object.is(child, -0)) refuse(child, childPath());
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (typeof child === "object" && child !== null) {
|
|
291
|
+
if (frames.length >= MAX_JSON_NESTING) {
|
|
292
|
+
throw new UnserializableValueError(
|
|
293
|
+
`the document nests deeper than ${MAX_JSON_NESTING} levels at ${clampPath(childPath())}, which JSON serialization cannot rewrite safely`);
|
|
294
|
+
}
|
|
295
|
+
pushContainer(child, childPath());
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Every serializer returns text ending in exactly one newline. */
|
|
301
|
+
export function serializeDocument(document: unknown, format: ConfigFormat): string {
|
|
302
|
+
switch (format) {
|
|
303
|
+
case "json": {
|
|
304
|
+
assertJsonNumbersRoundTrip(document, "$");
|
|
305
|
+
return `${JSON.stringify(document, null, 2)}\n`;
|
|
306
|
+
}
|
|
307
|
+
case "json5": return `${Bun.JSON5.stringify(document, null, 2)}\n`;
|
|
308
|
+
case "yaml": return renderYaml(document);
|
|
309
|
+
case "toml": {
|
|
310
|
+
if (!isPlainRecord(document)) throw new Error("TOML root must be a table");
|
|
311
|
+
return renderToml(document);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "What is on disk, and did we put it there?"
|
|
3
|
+
*
|
|
4
|
+
* The classifier is deliberately ordered, and the order is load-bearing: an
|
|
5
|
+
* unreadable file can never be reported as absent, and an edit to a fragment
|
|
6
|
+
* we own can never be reported as ordinary drift. Getting that wrong would let
|
|
7
|
+
* `disable` delete a user's own edits.
|
|
8
|
+
*
|
|
9
|
+
* Design of record: devlog/_fin/260802_client_toggle_api/021 §3.
|
|
10
|
+
*/
|
|
11
|
+
import { ClientPathError, EXPORT_CLIENTS, opencodeProxyBaseUrl, type ExportModel, type ManagedContribution } from "../clients/config-export";
|
|
12
|
+
import type { OcxConfig } from "../types";
|
|
13
|
+
import { PARSE_FAILED, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
14
|
+
import { SNAPSHOT_RETENTION } from "./journal";
|
|
15
|
+
import { canonicalContribution, fingerprint, type OwnershipRecord } from "./ownership";
|
|
16
|
+
import { INTEGRATION_CLIENTS, type IntegrationClientId } from "./registry";
|
|
17
|
+
import { createIntegrationStateStore, type IntegrationStateStore } from "./store";
|
|
18
|
+
|
|
19
|
+
export type IntegrationState = "absent" | "current" | "stale" | "conflict" | "unsafe";
|
|
20
|
+
export type StateReason =
|
|
21
|
+
| "unparseable"
|
|
22
|
+
| "not-regular-file"
|
|
23
|
+
| "foreign-edit"
|
|
24
|
+
| "unowned-key"
|
|
25
|
+
/** A container we would have to write through holds a non-object value. */
|
|
26
|
+
| "blocked-container"
|
|
27
|
+
/** A path selector we cannot resolve, e.g. a relative OPENCLAW_CONFIG_PATH. */
|
|
28
|
+
| "unresolvable-path";
|
|
29
|
+
|
|
30
|
+
export interface IntegrationStatus {
|
|
31
|
+
clientId: IntegrationClientId;
|
|
32
|
+
state: IntegrationState;
|
|
33
|
+
installed: boolean;
|
|
34
|
+
configPath: string;
|
|
35
|
+
appliedAt?: string;
|
|
36
|
+
lastOpId?: string;
|
|
37
|
+
reason?: StateReason;
|
|
38
|
+
/** Snapshot files retained for this client; -1 when they cannot be inspected. */
|
|
39
|
+
snapshotCount: number;
|
|
40
|
+
/** Pruning is behind, so older (possibly credential-bearing) snapshots remain. */
|
|
41
|
+
retentionDegraded: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function readPath(doc: unknown, path: readonly string[]): unknown {
|
|
45
|
+
let cursor: unknown = doc;
|
|
46
|
+
for (const key of path) {
|
|
47
|
+
if (typeof cursor !== "object" || cursor === null || Array.isArray(cursor)) return undefined;
|
|
48
|
+
cursor = (cursor as Record<string, unknown>)[key];
|
|
49
|
+
if (cursor === undefined) return undefined;
|
|
50
|
+
}
|
|
51
|
+
return cursor;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Does the document carry any fragment we would write? */
|
|
55
|
+
export function hasOurFragments(doc: unknown, contribution: ManagedContribution): boolean {
|
|
56
|
+
return contribution.fragments.some(fragment => readPath(doc, fragment.path) !== undefined);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A container on one of our fragment paths that exists but is NOT an object.
|
|
61
|
+
*
|
|
62
|
+
* `setPath` replaces such a value with `{}` on the way to writing our leaf, so
|
|
63
|
+
* a user whose config held `providers: ["something"]` — legal in their schema,
|
|
64
|
+
* just not ours — lost it to an apply that reported success. The classifier
|
|
65
|
+
* called that document `absent` because our leaf was missing, which authorized
|
|
66
|
+
* the write. Detecting it here turns a silent overwrite into a refusal the
|
|
67
|
+
* user can act on; the snapshot exists, but "we backed up the thing we should
|
|
68
|
+
* not have destroyed" is not the promise this feature makes.
|
|
69
|
+
*/
|
|
70
|
+
export function blockedContainerPath(
|
|
71
|
+
doc: unknown,
|
|
72
|
+
contribution: ManagedContribution,
|
|
73
|
+
): readonly string[] | null {
|
|
74
|
+
for (const fragment of contribution.fragments) {
|
|
75
|
+
let cursor: unknown = doc;
|
|
76
|
+
for (let depth = 0; depth < fragment.path.length - 1; depth += 1) {
|
|
77
|
+
const key = fragment.path[depth]!;
|
|
78
|
+
/*
|
|
79
|
+
* ONLY `undefined` means absent. A missing file parses as `{}`, so an
|
|
80
|
+
* absent prefix reads `undefined` — but a parsed `null` is a value the
|
|
81
|
+
* user's file actually contains, and treating it as absent let a
|
|
82
|
+
* document that is literally `null` be replaced wholesale by a
|
|
83
|
+
* "successful" apply.
|
|
84
|
+
*/
|
|
85
|
+
if (cursor === undefined) break;
|
|
86
|
+
// `typeof null === "object"`, so null has to be named explicitly or it
|
|
87
|
+
// walks straight into the dereference below.
|
|
88
|
+
if (cursor === null || typeof cursor !== "object" || Array.isArray(cursor)) {
|
|
89
|
+
return fragment.path.slice(0, depth);
|
|
90
|
+
}
|
|
91
|
+
const next = (cursor as Record<string, unknown>)[key];
|
|
92
|
+
if (next === undefined) break;
|
|
93
|
+
if (typeof next !== "object" || next === null || Array.isArray(next)) {
|
|
94
|
+
return fragment.path.slice(0, depth + 1);
|
|
95
|
+
}
|
|
96
|
+
cursor = next;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Fingerprint the recorded fragments as they appear in the document now.
|
|
104
|
+
*
|
|
105
|
+
* The record intentionally names every path we own. Comparing just those
|
|
106
|
+
* values lets another integration or a user add a sibling without blocking a
|
|
107
|
+
* later refresh, while a change inside our block still fails closed.
|
|
108
|
+
*/
|
|
109
|
+
function recordedFragmentFingerprint(
|
|
110
|
+
doc: unknown,
|
|
111
|
+
record: OwnershipRecord,
|
|
112
|
+
): string | null {
|
|
113
|
+
if (
|
|
114
|
+
!Array.isArray(record.fragmentPaths)
|
|
115
|
+
|| record.fragmentPaths.length === 0
|
|
116
|
+
|| !record.fragmentPaths.every(path => (
|
|
117
|
+
Array.isArray(path)
|
|
118
|
+
&& path.length > 0
|
|
119
|
+
&& path.every(key => typeof key === "string")
|
|
120
|
+
))
|
|
121
|
+
) return null;
|
|
122
|
+
const fragments = [];
|
|
123
|
+
for (const path of record.fragmentPaths) {
|
|
124
|
+
const value = readPath(doc, path);
|
|
125
|
+
if (value === undefined) return null;
|
|
126
|
+
fragments.push({ path, value });
|
|
127
|
+
}
|
|
128
|
+
return fingerprint(canonicalContribution({
|
|
129
|
+
clientId: record.clientId,
|
|
130
|
+
fragments,
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The two-axis rule: the recorded bytes or fragments prove nobody changed
|
|
136
|
+
* what we may rewrite, and the contribution hash proves our catalog has not
|
|
137
|
+
* moved on. Three classes of client (revising the unconditional whole-file
|
|
138
|
+
* rule of devlog 260802_client_toggle_api/021 §3 for json — #1631):
|
|
139
|
+
* registry-declared source-preserving YAML clients are fragment-scoped because
|
|
140
|
+
* their writers patch only the owned leaf, so the whole-file check is skipped;
|
|
141
|
+
* strict-json clients keep the whole-file check but downgrade a drift with
|
|
142
|
+
* intact owned fragments to `stale`, because a rewrite there can lose only
|
|
143
|
+
* formatting (comments cannot parse, non-round-tripping numbers are refused
|
|
144
|
+
* by the serializer); every comment-capable whole-document serializer (yaml,
|
|
145
|
+
* json5, toml) retains the whole-file fingerprint guard as a hard conflict.
|
|
146
|
+
*/
|
|
147
|
+
export function classifyIntegration(input: {
|
|
148
|
+
fileText: string | null;
|
|
149
|
+
fileIsRegular: boolean;
|
|
150
|
+
parsed: unknown | typeof PARSE_FAILED;
|
|
151
|
+
record: OwnershipRecord | null;
|
|
152
|
+
contribution: ManagedContribution;
|
|
153
|
+
/**
|
|
154
|
+
* The file being classified. A record only describes the file it was written
|
|
155
|
+
* for, so this is compared against `record.configPath` before any
|
|
156
|
+
* fingerprint is trusted.
|
|
157
|
+
*/
|
|
158
|
+
configPath?: string;
|
|
159
|
+
clientId?: IntegrationClientId;
|
|
160
|
+
}): { state: IntegrationState; reason?: StateReason } {
|
|
161
|
+
if (input.fileText !== null && !input.fileIsRegular) {
|
|
162
|
+
return { state: "unsafe", reason: "not-regular-file" };
|
|
163
|
+
}
|
|
164
|
+
if (input.parsed === PARSE_FAILED) return { state: "unsafe", reason: "unparseable" };
|
|
165
|
+
/*
|
|
166
|
+
* Checked BEFORE `absent`: our leaf is missing in exactly this case, so the
|
|
167
|
+
* absent branch would authorize an apply that replaces the user's value.
|
|
168
|
+
*/
|
|
169
|
+
if (blockedContainerPath(input.parsed, input.contribution)) {
|
|
170
|
+
return { state: "unsafe", reason: "blocked-container" };
|
|
171
|
+
}
|
|
172
|
+
if (!hasOurFragments(input.parsed, input.contribution)) return { state: "absent" };
|
|
173
|
+
if (!input.record) return { state: "conflict", reason: "unowned-key" };
|
|
174
|
+
/*
|
|
175
|
+
* A record proves ownership of ONE file. Change HOME, XDG_CONFIG_HOME,
|
|
176
|
+
* HERMES_HOME or KIMI_CODE_HOME and the same client resolves to a different
|
|
177
|
+
* path — whose contents may hash identically because we generate the same
|
|
178
|
+
* bytes. Trusting the fingerprint alone would let a record for path A grant
|
|
179
|
+
* `current` on path B, and the writer resolves the CURRENT path, so disable
|
|
180
|
+
* would then delete fragments from a file this record never owned.
|
|
181
|
+
*/
|
|
182
|
+
if (input.clientId !== undefined && input.record.clientId !== input.clientId) {
|
|
183
|
+
return { state: "conflict", reason: "unowned-key" };
|
|
184
|
+
}
|
|
185
|
+
if (input.configPath !== undefined && input.record.configPath !== input.configPath) {
|
|
186
|
+
return { state: "conflict", reason: "unowned-key" };
|
|
187
|
+
}
|
|
188
|
+
const clientId = input.clientId ?? input.record.clientId;
|
|
189
|
+
/*
|
|
190
|
+
* Checked BEFORE file-level drift: an edit INSIDE an owned fragment is a
|
|
191
|
+
* conflict no matter what the rest of the file looks like, so the sibling-
|
|
192
|
+
* edit exemption below can never mask it.
|
|
193
|
+
*/
|
|
194
|
+
if (recordedFragmentFingerprint(input.parsed, input.record) !== input.record.blockFingerprint) {
|
|
195
|
+
return { state: "conflict", reason: "foreign-edit" };
|
|
196
|
+
}
|
|
197
|
+
if (!INTEGRATION_CLIENTS[clientId].sourcePreservingYaml
|
|
198
|
+
&& fingerprint(input.fileText ?? "") !== input.record.fileFingerprint) {
|
|
199
|
+
/*
|
|
200
|
+
* The file changed since we wrote it, but every fragment we own is still
|
|
201
|
+
* byte-for-byte what we put there — a sibling edit, not tampering. Apply
|
|
202
|
+
* rewrites the WHOLE document, so for comment-capable formats (yaml,
|
|
203
|
+
* json5, toml) it would drop comments the user wrote next to us: fail
|
|
204
|
+
* closed there. Strict JSON cannot carry comments — a commented file
|
|
205
|
+
* never reaches this branch because parsing already failed — so the only
|
|
206
|
+
* possible loss is formatting normalization: everything a rewrite would
|
|
207
|
+
* actually change (numbers that would not round-trip, duplicate members
|
|
208
|
+
* a rewrite would delete) is PARSE_FAILED in parseConfig and classifies
|
|
209
|
+
* as unsafe long before this branch, exactly like comments. Refusing
|
|
210
|
+
* forever over formatting
|
|
211
|
+
* dead-ends the integration on the user's first own config edit (#1631).
|
|
212
|
+
* Report drift instead; a re-apply merges into the parsed document as it
|
|
213
|
+
* stands and re-owns the file. This also lets disable proceed on a
|
|
214
|
+
* drifted file — removal still touches only the recorded fragment paths.
|
|
215
|
+
*/
|
|
216
|
+
if (EXPORT_CLIENTS[clientId].format !== "json") {
|
|
217
|
+
return { state: "conflict", reason: "foreign-edit" };
|
|
218
|
+
}
|
|
219
|
+
return { state: "stale" };
|
|
220
|
+
}
|
|
221
|
+
return input.record.blockFingerprint === fingerprint(canonicalContribution(input.contribution))
|
|
222
|
+
? { state: "current" }
|
|
223
|
+
: { state: "stale" };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface IntegrationStateInput {
|
|
227
|
+
clientId: IntegrationClientId;
|
|
228
|
+
models: readonly ExportModel[];
|
|
229
|
+
config: OcxConfig;
|
|
230
|
+
port: number;
|
|
231
|
+
env?: NodeJS.ProcessEnv;
|
|
232
|
+
home?: string;
|
|
233
|
+
/** The whole integration state store, bound to one root. */
|
|
234
|
+
store?: IntegrationStateStore;
|
|
235
|
+
io?: IntegrationIO;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function exportContextOf(input: {
|
|
239
|
+
models: readonly ExportModel[];
|
|
240
|
+
config: OcxConfig;
|
|
241
|
+
port: number;
|
|
242
|
+
}): { baseUrl: string; models: readonly ExportModel[]; config: OcxConfig } {
|
|
243
|
+
return {
|
|
244
|
+
/*
|
|
245
|
+
* Composed through the SAME helper `ocx export` uses. Interpolating the
|
|
246
|
+
* hostname by hand looked equivalent and was not: `::1` produced
|
|
247
|
+
* `http://::1:10100/v1` and `::` produced `http://:::10100/v1`, neither of
|
|
248
|
+
* which is a URL, and a `0.0.0.0` bind wrote a wildcard address no client
|
|
249
|
+
* can dial. `opencodeProxyBaseUrl` brackets IPv6 and maps wildcards to
|
|
250
|
+
* loopback, and every client we write into deserves the same answer the
|
|
251
|
+
* export command already gives.
|
|
252
|
+
*/
|
|
253
|
+
baseUrl: opencodeProxyBaseUrl(input.port, input.config.hostname),
|
|
254
|
+
models: input.models,
|
|
255
|
+
config: input.config,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let retriedThisProcess = false;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Retry pending prunes once per process for the default store, and always for
|
|
263
|
+
* an explicitly supplied one so tests stay order-independent. Never throws: a
|
|
264
|
+
* retry failure is a logged no-op, not a failed read.
|
|
265
|
+
*/
|
|
266
|
+
export function retryPendingPrunesOnce(store: IntegrationStateStore): void {
|
|
267
|
+
if (store.root === createIntegrationStateStore().root) {
|
|
268
|
+
if (retriedThisProcess) return;
|
|
269
|
+
retriedThisProcess = true;
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
store.retryPendingPrunes();
|
|
273
|
+
} catch (error) {
|
|
274
|
+
console.error(`[integrations] prune retry failed: ${String(error)}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Retention is derived from what is ON DISK, not from the maintenance marker.
|
|
280
|
+
* The marker schedules retries and can itself fail to write; a promise about
|
|
281
|
+
* the user's credential-bearing backups must not depend on that.
|
|
282
|
+
*/
|
|
283
|
+
function retentionOf(
|
|
284
|
+
clientId: IntegrationClientId,
|
|
285
|
+
store: IntegrationStateStore,
|
|
286
|
+
): { snapshotCount: number; retentionDegraded: boolean } {
|
|
287
|
+
const counted = store.countSnapshots(clientId);
|
|
288
|
+
if (counted === null) {
|
|
289
|
+
// Cannot inspect: report degraded with -1 rather than a reassuring zero.
|
|
290
|
+
return { snapshotCount: -1, retentionDegraded: true };
|
|
291
|
+
}
|
|
292
|
+
const marked = store.readMaintenance().pruneFailures[clientId] !== undefined;
|
|
293
|
+
return { snapshotCount: counted, retentionDegraded: marked || counted > SNAPSHOT_RETENTION };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** The ONE reader every surface uses. */
|
|
297
|
+
export function readIntegrationState(input: IntegrationStateInput): IntegrationStatus {
|
|
298
|
+
const store = input.store ?? createIntegrationStateStore();
|
|
299
|
+
retryPendingPrunesOnce(store);
|
|
300
|
+
const io = input.io ?? store.io();
|
|
301
|
+
const spec = INTEGRATION_CLIENTS[input.clientId];
|
|
302
|
+
const exportSpec = EXPORT_CLIENTS[input.clientId];
|
|
303
|
+
const retention = retentionOf(input.clientId, store);
|
|
304
|
+
/*
|
|
305
|
+
* Resolution can refuse — a relative OPENCLAW_* selector names a file whose
|
|
306
|
+
* meaning depends on a working directory we cannot know. The LIST route asks
|
|
307
|
+
* every client for its state, so letting that escape would answer 500 for
|
|
308
|
+
* the whole Integrations page because one client is misconfigured.
|
|
309
|
+
*/
|
|
310
|
+
let configPath: string;
|
|
311
|
+
let installed: boolean;
|
|
312
|
+
try {
|
|
313
|
+
configPath = spec.configPath(input.env, input.home);
|
|
314
|
+
installed = io.statKind(spec.detectDir(input.env, input.home)) === "dir";
|
|
315
|
+
} catch (error) {
|
|
316
|
+
if (!(error instanceof ClientPathError)) throw error;
|
|
317
|
+
return {
|
|
318
|
+
clientId: input.clientId,
|
|
319
|
+
state: "unsafe",
|
|
320
|
+
installed: false,
|
|
321
|
+
configPath: "",
|
|
322
|
+
reason: "unresolvable-path",
|
|
323
|
+
...retention,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const target = loadTarget(io, configPath);
|
|
328
|
+
if (!target.ok) {
|
|
329
|
+
return {
|
|
330
|
+
clientId: input.clientId,
|
|
331
|
+
state: "unsafe",
|
|
332
|
+
installed,
|
|
333
|
+
configPath,
|
|
334
|
+
reason: target.why === "read-failed" ? "unparseable" : "not-regular-file",
|
|
335
|
+
...retention,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const parsed = parseConfig(target.before, exportSpec.format);
|
|
340
|
+
const contribution = exportSpec.buildContribution(exportContextOf(input));
|
|
341
|
+
const record = store.readRecords()[input.clientId] ?? null;
|
|
342
|
+
const { state, reason } = classifyIntegration({
|
|
343
|
+
fileText: target.before,
|
|
344
|
+
fileIsRegular: true,
|
|
345
|
+
parsed,
|
|
346
|
+
record,
|
|
347
|
+
contribution,
|
|
348
|
+
configPath,
|
|
349
|
+
clientId: input.clientId,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
clientId: input.clientId,
|
|
354
|
+
state,
|
|
355
|
+
installed,
|
|
356
|
+
configPath,
|
|
357
|
+
...(reason ? { reason } : {}),
|
|
358
|
+
...(record ? { appliedAt: record.appliedAt, lastOpId: record.opId } : {}),
|
|
359
|
+
...retention,
|
|
360
|
+
};
|
|
361
|
+
}
|