@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,967 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompt-layers.ts — the Codex prompt-layer surface in `$CODEX_HOME/config.toml`.
|
|
3
|
+
*
|
|
4
|
+
* Scope boundary: this module owns the five `include_*` prompt toggles and the
|
|
5
|
+
* generated `developer_instructions` projection. It is a SIBLING of
|
|
6
|
+
* `features.ts`, not an extension of it: that module's header explicitly forbids
|
|
7
|
+
* broadening itself past `multi_agent_v2`, so the technique is copied here
|
|
8
|
+
* rather than the file being widened.
|
|
9
|
+
*
|
|
10
|
+
* Two design decisions are load-bearing and were forced by an adversarial audit
|
|
11
|
+
* (devlog/_plan/260802_codex_set_prompt_composer/):
|
|
12
|
+
*
|
|
13
|
+
* 1. NO USER PROSE IS PARSED BACK OUT OF TOML. Custom layers live in
|
|
14
|
+
* `opencodex-prompt.json`, which we own outright; config.toml receives a
|
|
15
|
+
* write-only projection of the enabled subset. Layer identity never has to
|
|
16
|
+
* survive a round trip through a TOML parser.
|
|
17
|
+
*
|
|
18
|
+
* 2. NO TOML LIBRARY IS USED TO VERIFY WHAT WE WROTE. Measured on Bun 1.3.14,
|
|
19
|
+
* `Bun.TOML.parse` transposes `\t` and `\f`, rejects `\u0007`, and does not
|
|
20
|
+
* trim the newline after an opening `'''`. Codex parses with Rust
|
|
21
|
+
* `toml_edit`, so verifying through a JS parser could report success on a
|
|
22
|
+
* file Codex reads differently. Instead the accepted character set is
|
|
23
|
+
* restricted until escaping is total under three rules, and verification is
|
|
24
|
+
* a byte comparison.
|
|
25
|
+
*
|
|
26
|
+
* CODEX_HOME is resolved at CALL time (the `features.ts:58-67` pattern) so tests
|
|
27
|
+
* can point fixtures via env or an explicit path.
|
|
28
|
+
*/
|
|
29
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
30
|
+
import { dirname, join, resolve } from "node:path";
|
|
31
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
32
|
+
import { expandUserPath } from "../config";
|
|
33
|
+
import { CODEX_CONFIG_PATH } from "./paths";
|
|
34
|
+
import { OCX_SECTION_MARKER } from "./injected-marker";
|
|
35
|
+
import {
|
|
36
|
+
durableWrite,
|
|
37
|
+
durableWriteExclusive,
|
|
38
|
+
durableDelete,
|
|
39
|
+
encodeJournal,
|
|
40
|
+
ensureDir,
|
|
41
|
+
hashBytes,
|
|
42
|
+
recoverIfNeeded as recoverJournal,
|
|
43
|
+
type JournalRecord,
|
|
44
|
+
} from "./prompt-journal";
|
|
45
|
+
import { release, stillHeld, tryAcquire } from "./prompt-lock";
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Inventory — ONE definition, consumed by the route and the GUI alike.
|
|
49
|
+
// Classes are the five in devlog 001 §4; the partition is total and disjoint.
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
export type LayerClass =
|
|
53
|
+
| "base"
|
|
54
|
+
| "config-toggle"
|
|
55
|
+
| "feature-gated"
|
|
56
|
+
| "runtime-conditional"
|
|
57
|
+
| "extension-unknown";
|
|
58
|
+
|
|
59
|
+
export type ToggleId =
|
|
60
|
+
| "permissions"
|
|
61
|
+
| "collaboration"
|
|
62
|
+
| "environment"
|
|
63
|
+
| "apps"
|
|
64
|
+
| "skills";
|
|
65
|
+
|
|
66
|
+
export interface LayerDescriptor {
|
|
67
|
+
id: string;
|
|
68
|
+
class: LayerClass;
|
|
69
|
+
/** config key for config-toggle and feature-gated rows; null otherwise */
|
|
70
|
+
key: string | null;
|
|
71
|
+
/** documented default when the key is absent */
|
|
72
|
+
default: boolean | null;
|
|
73
|
+
/** assembly index from devlog 001 §1; null when registration-order dependent */
|
|
74
|
+
order: number | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Assembly order per `core/src/session/world_state.rs`. `base-instructions` is
|
|
79
|
+
* NOT a world-state section — it travels in the Responses `instructions` field
|
|
80
|
+
* — so it carries order 0 and sits ahead of the rest.
|
|
81
|
+
*
|
|
82
|
+
* `plugins` is `runtime-conditional`, not feature-gated: `core/src/mcp.rs:200`
|
|
83
|
+
* computes `selected_plugin_available || !capability_summaries().is_empty()`,
|
|
84
|
+
* so `[features] plugins` influences the right operand but does not gate
|
|
85
|
+
* emission.
|
|
86
|
+
*/
|
|
87
|
+
export const LAYER_INVENTORY: readonly LayerDescriptor[] = Object.freeze([
|
|
88
|
+
{ id: "base-instructions", class: "base", key: null, default: null, order: 0 },
|
|
89
|
+
{ id: "model-switch", class: "runtime-conditional", key: null, default: null, order: 1 },
|
|
90
|
+
{ id: "personality", class: "feature-gated", key: "features.personality", default: true, order: 2 },
|
|
91
|
+
{ id: "context-window-guidance", class: "feature-gated", key: "features.token_budget", default: false, order: 3 },
|
|
92
|
+
{ id: "realtime", class: "runtime-conditional", key: null, default: null, order: 4 },
|
|
93
|
+
{ id: "agents-md", class: "runtime-conditional", key: null, default: null, order: 5 },
|
|
94
|
+
{ id: "permissions", class: "config-toggle", key: "include_permissions_instructions", default: true, order: 6 },
|
|
95
|
+
{ id: "collaboration", class: "config-toggle", key: "include_collaboration_mode_instructions", default: true, order: 7 },
|
|
96
|
+
{ id: "environment", class: "config-toggle", key: "include_environment_context", default: true, order: 8 },
|
|
97
|
+
{ id: "environments-instructions", class: "feature-gated", key: "features.deferred_executor", default: false, order: 9 },
|
|
98
|
+
{ id: "apps", class: "config-toggle", key: "include_apps_instructions", default: true, order: 10 },
|
|
99
|
+
{ id: "plugins", class: "runtime-conditional", key: null, default: null, order: 11 },
|
|
100
|
+
{ id: "tools", class: "feature-gated", key: "features.deferred_tool_world_state", default: false, order: 12 },
|
|
101
|
+
{ id: "skills", class: "config-toggle", key: "skills.include_instructions", default: true, order: 13 },
|
|
102
|
+
{ id: "multi-agent-mode", class: "feature-gated", key: "features.multi_agent_v2.enabled", default: false, order: 14 },
|
|
103
|
+
] as const);
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The write allowlist. Fixed, never computed: `config_toml.rs` does NOT carry
|
|
107
|
+
* serde's `deny_unknown_fields`, so a typo'd key is silently ignored in normal
|
|
108
|
+
* mode and a hard startup error under `--strict-config`. A fixed table means
|
|
109
|
+
* the GUI can never emit a key it did not intend.
|
|
110
|
+
*/
|
|
111
|
+
const TOGGLE_KEYS: Record<ToggleId, { table: string | null; key: string }> = {
|
|
112
|
+
permissions: { table: null, key: "include_permissions_instructions" },
|
|
113
|
+
collaboration: { table: null, key: "include_collaboration_mode_instructions" },
|
|
114
|
+
environment: { table: null, key: "include_environment_context" },
|
|
115
|
+
apps: { table: null, key: "include_apps_instructions" },
|
|
116
|
+
skills: { table: "skills", key: "include_instructions" },
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const TOGGLE_IDS = Object.freeze(Object.keys(TOGGLE_KEYS) as ToggleId[]);
|
|
120
|
+
|
|
121
|
+
export function isToggleId(value: string): value is ToggleId {
|
|
122
|
+
return Object.prototype.hasOwnProperty.call(TOGGLE_KEYS, value);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Paths
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
export interface Paths {
|
|
130
|
+
configPath?: string;
|
|
131
|
+
storePath?: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function activeCodexHome(): string {
|
|
135
|
+
const raw = process.env.CODEX_HOME?.trim();
|
|
136
|
+
if (!raw) return CODEX_CONFIG_PATH.slice(0, -"/config.toml".length);
|
|
137
|
+
const path = resolve(expandUserPath(raw));
|
|
138
|
+
try {
|
|
139
|
+
return realpathSync.native(path);
|
|
140
|
+
} catch {
|
|
141
|
+
return path;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function activeConfigPath(opts?: Paths): string {
|
|
146
|
+
return opts?.configPath ?? join(activeCodexHome(), "config.toml");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function activeStorePath(opts?: Paths): string {
|
|
150
|
+
return opts?.storePath ?? join(activeCodexHome(), "opencodex-prompt.json");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function journalPathFor(storePath: string): string {
|
|
154
|
+
return `${storePath.replace(/\.json$/, "")}.journal`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function lockPathFor(storePath: string): string {
|
|
158
|
+
return `${storePath.replace(/\.json$/, "")}.lock`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Character policy — see the header. Defined over Unicode SCALAR VALUES, not
|
|
163
|
+
// UTF-16 code units, because a lone surrogate is not a scalar value and UTF-8
|
|
164
|
+
// encoding would silently substitute U+FFFD.
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
export interface CharacterFinding {
|
|
168
|
+
/** code-point index, consistent across module, route and editor */
|
|
169
|
+
position: number;
|
|
170
|
+
reason: "control" | "unpaired-surrogate";
|
|
171
|
+
codePoint: number;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Tab to four spaces, CRLF and lone CR to LF. Applied BEFORE validation. */
|
|
175
|
+
export function normalizeBody(body: string): string {
|
|
176
|
+
return body.replace(/\r\n?/g, "\n").replace(/\t/g, " ");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** First offending scalar, or null. Run AFTER normalizeBody. */
|
|
180
|
+
export function findInvalidCharacter(body: string): CharacterFinding | null {
|
|
181
|
+
let position = 0;
|
|
182
|
+
for (let i = 0; i < body.length; ) {
|
|
183
|
+
const code = body.codePointAt(i)!;
|
|
184
|
+
const unit = body.charCodeAt(i);
|
|
185
|
+
const isHighSurrogate = unit >= 0xd800 && unit <= 0xdbff;
|
|
186
|
+
const isLowSurrogate = unit >= 0xdc00 && unit <= 0xdfff;
|
|
187
|
+
// codePointAt only combines a well-formed pair, so a surviving surrogate
|
|
188
|
+
// code point here is unpaired by construction.
|
|
189
|
+
if ((isHighSurrogate || isLowSurrogate) && code === unit) {
|
|
190
|
+
return { position, reason: "unpaired-surrogate", codePoint: code };
|
|
191
|
+
}
|
|
192
|
+
const isNewline = code === 0x0a;
|
|
193
|
+
const isC0 = code < 0x20 && !isNewline;
|
|
194
|
+
const isDel = code === 0x7f;
|
|
195
|
+
const isC1 = code >= 0x80 && code <= 0x9f;
|
|
196
|
+
if (isC0 || isDel || isC1) {
|
|
197
|
+
return { position, reason: "control", codePoint: code };
|
|
198
|
+
}
|
|
199
|
+
i += code > 0xffff ? 2 : 1;
|
|
200
|
+
position += 1;
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* TOML basic-string encoding, total over the accepted set: three rules, none of
|
|
207
|
+
* them in the range where `Bun.TOML.parse` misbehaves. `\r` cannot appear
|
|
208
|
+
* because normalizeBody removed it; control characters cannot appear because
|
|
209
|
+
* findInvalidCharacter rejected them.
|
|
210
|
+
*/
|
|
211
|
+
export function encodeBasicString(body: string): string {
|
|
212
|
+
return `"${body.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Inverse of `encodeBasicString`, deliberately narrow: it accepts ONLY the three
|
|
217
|
+
* escapes we emit. `\t`, `\f`, `\b`, `\r` and `\uXXXX` are refused rather than
|
|
218
|
+
* guessed — decoding them correctly is exactly the ambiguity the restricted set
|
|
219
|
+
* exists to avoid.
|
|
220
|
+
*/
|
|
221
|
+
export function decodeBasicString(literal: string): string | null {
|
|
222
|
+
if (literal.length < 2 || !literal.startsWith('"') || !literal.endsWith('"')) return null;
|
|
223
|
+
const inner = literal.slice(1, -1);
|
|
224
|
+
let out = "";
|
|
225
|
+
for (let i = 0; i < inner.length; i += 1) {
|
|
226
|
+
const ch = inner[i]!;
|
|
227
|
+
if (ch !== "\\") {
|
|
228
|
+
if (ch === '"') return null; // unescaped quote: not a single literal
|
|
229
|
+
out += ch;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const next = inner[i + 1];
|
|
233
|
+
if (next === "\\") out += "\\";
|
|
234
|
+
else if (next === '"') out += '"';
|
|
235
|
+
else if (next === "n") out += "\n";
|
|
236
|
+
else return null; // any other escape is outside what we will decode
|
|
237
|
+
i += 1;
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// Byte-level hashing. The revision covers COMPLETE file bytes plus existence,
|
|
244
|
+
// so removing the marker while leaving the value intact still changes it.
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
function readFileOrNull(path: string): string | null {
|
|
248
|
+
try {
|
|
249
|
+
if (!existsSync(path)) return null;
|
|
250
|
+
return readFileSync(path, "utf8");
|
|
251
|
+
} catch {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function computeRevision(configBytes: string | null, storeBytes: string | null): string {
|
|
257
|
+
const hash = createHash("sha256");
|
|
258
|
+
hash.update("cfg:");
|
|
259
|
+
hash.update(configBytes ?? "\0absent");
|
|
260
|
+
hash.update("\nstore:");
|
|
261
|
+
hash.update(storeBytes ?? "\0absent");
|
|
262
|
+
return `sha256:${hash.digest("hex")}`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export { readFileOrNull as readFileBytes };
|
|
266
|
+
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// Scoped TOML scanning. Line-based like `features.ts:80-93`: booleans need no
|
|
269
|
+
// escaping, and line editing preserves the user's comments and formatting
|
|
270
|
+
// exactly where a re-serialize would not.
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
const TABLE_HEADER = /^\s*\[/;
|
|
274
|
+
|
|
275
|
+
/** Lines of the root scope: everything before the first `[table]` header. */
|
|
276
|
+
function rootLines(content: string): string[] {
|
|
277
|
+
const lines = content.split("\n");
|
|
278
|
+
const first = lines.findIndex(l => TABLE_HEADER.test(l));
|
|
279
|
+
return first === -1 ? lines : lines.slice(0, first);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Lines of `[header]`'s body, up to the next table header. */
|
|
283
|
+
function tableLines(content: string, header: string): string[] | null {
|
|
284
|
+
const lines = content.split("\n");
|
|
285
|
+
const escaped = header.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
286
|
+
const start = lines.findIndex(l => new RegExp(`^\\s*\\[${escaped}\\]\\s*(?:#.*)?$`).test(l));
|
|
287
|
+
if (start === -1) return null;
|
|
288
|
+
const rest = lines.slice(start + 1);
|
|
289
|
+
const end = rest.findIndex(l => TABLE_HEADER.test(l));
|
|
290
|
+
return end === -1 ? rest : rest.slice(0, end);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function boolInLines(lines: string[], key: string): boolean | null {
|
|
294
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
295
|
+
const pattern = new RegExp(`^\\s*${escaped}\\s*=\\s*(true|false)\\s*(?:#.*)?$`);
|
|
296
|
+
for (const line of lines) {
|
|
297
|
+
const m = pattern.exec(line);
|
|
298
|
+
if (m) return m[1] === "true";
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
// Ownership of the generated projection.
|
|
305
|
+
//
|
|
306
|
+
// Canonical physical form, always exactly two lines at the top of the document:
|
|
307
|
+
//
|
|
308
|
+
// # Auto-injected by opencodex
|
|
309
|
+
// developer_instructions = "<single-line basic string>"
|
|
310
|
+
//
|
|
311
|
+
// Replacement is "find the marker, replace the next line" — never a span search.
|
|
312
|
+
// Adjacency mirrors `injected-marker.ts:53-60`, tightened by a shape check.
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
const DEV_INSTRUCTIONS_KEY = "developer_instructions";
|
|
316
|
+
const CANONICAL_LINE = /^developer_instructions = "(?:[^"\\]|\\.)*"$/;
|
|
317
|
+
const ANY_DEV_INSTRUCTIONS = /^\s*(?:developer_instructions|"developer_instructions"|'developer_instructions')\s*=/;
|
|
318
|
+
|
|
319
|
+
export type Ownership =
|
|
320
|
+
/** no such key anywhere in the root scope */
|
|
321
|
+
| { state: "absent" }
|
|
322
|
+
/** marker-adjacent and canonically shaped: ours to rewrite */
|
|
323
|
+
| { state: "owned"; line: number; literal: string }
|
|
324
|
+
/** marker-adjacent but reshaped: refuse, offer repair */
|
|
325
|
+
| { state: "owned-malformed"; line: number; raw: string }
|
|
326
|
+
/** no marker: externally authored, refuse and offer adoption */
|
|
327
|
+
| { state: "external"; line: number; raw: string };
|
|
328
|
+
|
|
329
|
+
export function inspectOwnership(configBytes: string | null): Ownership {
|
|
330
|
+
if (configBytes === null) return { state: "absent" };
|
|
331
|
+
const lines = rootLines(configBytes);
|
|
332
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
333
|
+
const raw = lines[i]!;
|
|
334
|
+
if (!ANY_DEV_INSTRUCTIONS.test(raw)) continue;
|
|
335
|
+
const marked = i > 0 && lines[i - 1]!.includes(OCX_SECTION_MARKER);
|
|
336
|
+
if (!marked) return { state: "external", line: i + 1, raw };
|
|
337
|
+
if (!CANONICAL_LINE.test(raw)) return { state: "owned-malformed", line: i + 1, raw };
|
|
338
|
+
const literal = raw.slice(`${DEV_INSTRUCTIONS_KEY} = `.length);
|
|
339
|
+
return { state: "owned", line: i + 1, literal };
|
|
340
|
+
}
|
|
341
|
+
return { state: "absent" };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
// Store — the single source of truth for custom layers.
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
export interface CustomLayer {
|
|
349
|
+
/** [a-z0-9]{6}, stable across edits */
|
|
350
|
+
id: string;
|
|
351
|
+
title: string;
|
|
352
|
+
body: string;
|
|
353
|
+
enabled: boolean;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const LAYER_ID = /^[a-z0-9]{6}$/;
|
|
357
|
+
|
|
358
|
+
function isCustomLayer(value: unknown): value is CustomLayer {
|
|
359
|
+
if (!value || typeof value !== "object") return false;
|
|
360
|
+
const v = value as Record<string, unknown>;
|
|
361
|
+
return typeof v.id === "string" && LAYER_ID.test(v.id)
|
|
362
|
+
&& typeof v.title === "string"
|
|
363
|
+
&& typeof v.body === "string"
|
|
364
|
+
&& typeof v.enabled === "boolean";
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** null means unreadable/malformed, which is NOT the same as an empty store. */
|
|
368
|
+
export function parseStore(storeBytes: string | null): CustomLayer[] | null {
|
|
369
|
+
if (storeBytes === null) return null;
|
|
370
|
+
let parsed: unknown;
|
|
371
|
+
try {
|
|
372
|
+
parsed = JSON.parse(storeBytes);
|
|
373
|
+
} catch {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
377
|
+
const layers = (parsed as { layers?: unknown }).layers;
|
|
378
|
+
if (!Array.isArray(layers) || !layers.every(isCustomLayer)) return null;
|
|
379
|
+
const ids = new Set(layers.map(l => l.id));
|
|
380
|
+
if (ids.size !== layers.length) return null;
|
|
381
|
+
return layers;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Enabled layers, joined in order. This is the value written to config.toml. */
|
|
385
|
+
export function composeProjection(layers: readonly CustomLayer[]): string {
|
|
386
|
+
return layers.filter(l => l.enabled).map(l => l.body).join("\n\n");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
// Snapshot
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
|
|
393
|
+
export interface ToggleState {
|
|
394
|
+
id: ToggleId;
|
|
395
|
+
key: string;
|
|
396
|
+
/** null = the key is absent from the user file */
|
|
397
|
+
userFileValue: boolean | null;
|
|
398
|
+
/**
|
|
399
|
+
* userFileValue ?? default. NOT the resolved Codex value: opencodex reads one
|
|
400
|
+
* of the eight config layers, so it reports this file's value under a name
|
|
401
|
+
* that says as much.
|
|
402
|
+
*/
|
|
403
|
+
defaultedUserValue: boolean;
|
|
404
|
+
default: boolean;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export type Drift =
|
|
408
|
+
| "journal-present"
|
|
409
|
+
| "projection-stale"
|
|
410
|
+
| "store-missing"
|
|
411
|
+
| "owned-malformed"
|
|
412
|
+
| null;
|
|
413
|
+
|
|
414
|
+
export interface PromptLayerSnapshot {
|
|
415
|
+
configPath: string;
|
|
416
|
+
storePath: string;
|
|
417
|
+
configExists: boolean;
|
|
418
|
+
readable: boolean;
|
|
419
|
+
developerInstructionsOwned: boolean;
|
|
420
|
+
/** non-null blocks mutations until resolved */
|
|
421
|
+
drift: Drift;
|
|
422
|
+
toggles: ToggleState[];
|
|
423
|
+
custom: CustomLayer[];
|
|
424
|
+
modelInstructionsFile: string | null;
|
|
425
|
+
revision: string;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function readToggle(configBytes: string | null, id: ToggleId): ToggleState {
|
|
429
|
+
const spec = TOGGLE_KEYS[id];
|
|
430
|
+
const descriptor = LAYER_INVENTORY.find(d => d.id === id)!;
|
|
431
|
+
const fallback = descriptor.default ?? true;
|
|
432
|
+
const key = spec.table ? `${spec.table}.${spec.key}` : spec.key;
|
|
433
|
+
let value: boolean | null = null;
|
|
434
|
+
if (configBytes !== null) {
|
|
435
|
+
const scope = spec.table ? tableLines(configBytes, spec.table) : rootLines(configBytes);
|
|
436
|
+
value = scope === null ? null : boolInLines(scope, spec.key);
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
id,
|
|
440
|
+
key,
|
|
441
|
+
userFileValue: value,
|
|
442
|
+
defaultedUserValue: value ?? fallback,
|
|
443
|
+
default: fallback,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function readModelInstructionsFile(configBytes: string | null): string | null {
|
|
448
|
+
if (configBytes === null) return null;
|
|
449
|
+
for (const line of rootLines(configBytes)) {
|
|
450
|
+
const m = /^\s*model_instructions_file\s*=\s*"([^"]*)"\s*(?:#.*)?$/.exec(line);
|
|
451
|
+
if (m) return m[1]!;
|
|
452
|
+
}
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Pure. Never writes, never locks, never recovers — a GET must not modify a
|
|
458
|
+
* user's configuration, so drift is REPORTED here and resolved elsewhere.
|
|
459
|
+
*/
|
|
460
|
+
export function readPromptLayers(opts?: Paths): PromptLayerSnapshot {
|
|
461
|
+
const configPath = activeConfigPath(opts);
|
|
462
|
+
const storePath = activeStorePath(opts);
|
|
463
|
+
const configExists = existsSync(configPath);
|
|
464
|
+
const configBytes = readFileOrNull(configPath);
|
|
465
|
+
const storeExists = existsSync(storePath);
|
|
466
|
+
const storeBytes = readFileOrNull(storePath);
|
|
467
|
+
|
|
468
|
+
// Present but unreadable is a hard stop; absent is an ordinary first run.
|
|
469
|
+
const readable = !configExists || configBytes !== null;
|
|
470
|
+
const ownership = inspectOwnership(configBytes);
|
|
471
|
+
const layers = parseStore(storeBytes);
|
|
472
|
+
const projection = ownership.state === "owned"
|
|
473
|
+
? decodeBasicString(ownership.literal)
|
|
474
|
+
: null;
|
|
475
|
+
|
|
476
|
+
let drift: Drift = null;
|
|
477
|
+
if (existsSync(`${storePath.replace(/\.json$/, "")}.journal`)) {
|
|
478
|
+
drift = "journal-present";
|
|
479
|
+
} else if (ownership.state === "owned-malformed") {
|
|
480
|
+
drift = "owned-malformed";
|
|
481
|
+
} else if (!storeExists && projection !== null && projection.length > 0) {
|
|
482
|
+
// The store is gone while a live projection remains. Treating this as an
|
|
483
|
+
// empty store would erase the active prompt on the next write.
|
|
484
|
+
drift = "store-missing";
|
|
485
|
+
} else if (layers !== null && projection !== null && composeProjection(layers) !== projection) {
|
|
486
|
+
drift = "projection-stale";
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return {
|
|
490
|
+
configPath,
|
|
491
|
+
storePath,
|
|
492
|
+
configExists,
|
|
493
|
+
readable,
|
|
494
|
+
developerInstructionsOwned: ownership.state === "owned",
|
|
495
|
+
drift,
|
|
496
|
+
toggles: TOGGLE_IDS.map(id => readToggle(configBytes, id)),
|
|
497
|
+
custom: layers ?? [],
|
|
498
|
+
modelInstructionsFile: readModelInstructionsFile(configBytes),
|
|
499
|
+
revision: computeRevision(configBytes, storeBytes),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ---------------------------------------------------------------------------
|
|
504
|
+
// Writing
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
|
|
507
|
+
export type WriteError =
|
|
508
|
+
| "config_unreadable"
|
|
509
|
+
| "stale_revision"
|
|
510
|
+
| "developer_instructions_not_owned"
|
|
511
|
+
| "unknown_layer"
|
|
512
|
+
| "store_unreadable"
|
|
513
|
+
| "invalid_characters"
|
|
514
|
+
| "write_superseded"
|
|
515
|
+
| "recovery_required"
|
|
516
|
+
| "locked";
|
|
517
|
+
|
|
518
|
+
export type WriteResult =
|
|
519
|
+
| { ok: true; changed: boolean; snapshot: PromptLayerSnapshot }
|
|
520
|
+
| { ok: false; error: WriteError; detail?: string };
|
|
521
|
+
|
|
522
|
+
/** Line editing, not re-serialization: the user's comments and layout survive. */
|
|
523
|
+
function dominantEol(content: string): "\r\n" | "\n" {
|
|
524
|
+
const crlf = (content.match(/\r\n/g) ?? []).length;
|
|
525
|
+
if (crlf === 0) return "\n";
|
|
526
|
+
const bareLf = (content.match(/\n/g) ?? []).length - crlf;
|
|
527
|
+
return crlf >= bareLf ? "\r\n" : "\n";
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function splitLines(content: string): string[] {
|
|
531
|
+
return content.replace(/\r\n/g, "\n").split("\n");
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function joinLines(lines: string[], eol: "\r\n" | "\n"): string {
|
|
535
|
+
const text = lines.join("\n");
|
|
536
|
+
return eol === "\n" ? text : text.replace(/\n/g, "\r\n");
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function firstTableIndex(lines: string[]): number {
|
|
540
|
+
const idx = lines.findIndex(l => TABLE_HEADER.test(l));
|
|
541
|
+
return idx === -1 ? lines.length : idx;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** Set a root-scope boolean, inserting above the first table when absent. */
|
|
545
|
+
function setRootBool(content: string, key: string, value: boolean): string {
|
|
546
|
+
const eol = dominantEol(content);
|
|
547
|
+
const lines = splitLines(content);
|
|
548
|
+
const limit = firstTableIndex(lines);
|
|
549
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
550
|
+
const pattern = new RegExp(`^(\\s*${escaped}\\s*=\\s*)(?:true|false)(\\s*(?:#.*)?)$`);
|
|
551
|
+
for (let i = 0; i < limit; i += 1) {
|
|
552
|
+
const m = pattern.exec(lines[i]!);
|
|
553
|
+
if (m) {
|
|
554
|
+
lines[i] = `${m[1]}${value}${m[2]}`;
|
|
555
|
+
return joinLines(lines, eol);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
lines.splice(limit, 0, `${key} = ${value}`);
|
|
559
|
+
return joinLines(lines, eol);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** Set a boolean inside `[table]`, appending the table when absent. */
|
|
563
|
+
function setTableBool(content: string, table: string, key: string, value: boolean): string {
|
|
564
|
+
const eol = dominantEol(content);
|
|
565
|
+
const lines = splitLines(content);
|
|
566
|
+
const escaped = table.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
567
|
+
const start = lines.findIndex(l => new RegExp(`^\\s*\\[${escaped}\\]\\s*(?:#.*)?$`).test(l));
|
|
568
|
+
if (start === -1) {
|
|
569
|
+
const tail = lines.length > 0 && lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
|
|
570
|
+
lines.splice(tail, 0, `[${table}]`, `${key} = ${value}`);
|
|
571
|
+
return joinLines(lines, eol);
|
|
572
|
+
}
|
|
573
|
+
const keyEscaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
574
|
+
const pattern = new RegExp(`^(\\s*${keyEscaped}\\s*=\\s*)(?:true|false)(\\s*(?:#.*)?)$`);
|
|
575
|
+
let end = start + 1;
|
|
576
|
+
while (end < lines.length && !TABLE_HEADER.test(lines[end]!)) end += 1;
|
|
577
|
+
for (let i = start + 1; i < end; i += 1) {
|
|
578
|
+
const m = pattern.exec(lines[i]!);
|
|
579
|
+
if (m) {
|
|
580
|
+
lines[i] = `${m[1]}${value}${m[2]}`;
|
|
581
|
+
return joinLines(lines, eol);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
lines.splice(end, 0, `${key} = ${value}`);
|
|
585
|
+
return joinLines(lines, eol);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Replace, insert, or remove the generated two-line block. Canonical form is
|
|
590
|
+
* marker + assignment at the top of the document; replacement is "find the
|
|
591
|
+
* marker, replace the next line" rather than a span search.
|
|
592
|
+
*/
|
|
593
|
+
function setProjection(content: string | null, projection: string | null): string {
|
|
594
|
+
const base = content ?? "";
|
|
595
|
+
const eol = dominantEol(base);
|
|
596
|
+
const lines = splitLines(base);
|
|
597
|
+
const limit = firstTableIndex(lines);
|
|
598
|
+
|
|
599
|
+
let markerAt = -1;
|
|
600
|
+
for (let i = 0; i < limit; i += 1) {
|
|
601
|
+
if (i > 0 && lines[i - 1]!.includes(OCX_SECTION_MARKER) && ANY_DEV_INSTRUCTIONS.test(lines[i]!)) {
|
|
602
|
+
markerAt = i - 1;
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (markerAt !== -1) {
|
|
608
|
+
if (projection === null) lines.splice(markerAt, 2);
|
|
609
|
+
else lines[markerAt + 1] = `${DEV_INSTRUCTIONS_KEY} = ${encodeBasicString(projection)}`;
|
|
610
|
+
return joinLines(lines, eol);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (projection === null) return joinLines(lines, eol);
|
|
614
|
+
lines.splice(0, 0, OCX_SECTION_MARKER, `${DEV_INSTRUCTIONS_KEY} = ${encodeBasicString(projection)}`);
|
|
615
|
+
return joinLines(lines, eol);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function serializeStore(layers: readonly CustomLayer[]): string {
|
|
619
|
+
return `${JSON.stringify({ layers }, null, 2)}\n`;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
interface Mutation {
|
|
623
|
+
nextConfig: string | null;
|
|
624
|
+
nextStore: string | null;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* The seven-step transaction. Every filesystem mutation in this module goes
|
|
629
|
+
* through here so the journal, the lock, and the per-target byte checks cannot
|
|
630
|
+
* be bypassed by a future caller.
|
|
631
|
+
*/
|
|
632
|
+
function commit(
|
|
633
|
+
opts: Paths | undefined,
|
|
634
|
+
revision: string,
|
|
635
|
+
build: (snapshot: PromptLayerSnapshot, configBytes: string | null, storeBytes: string | null)
|
|
636
|
+
=> Mutation | { error: WriteError; detail?: string },
|
|
637
|
+
): WriteResult {
|
|
638
|
+
const configPath = activeConfigPath(opts);
|
|
639
|
+
const storePath = activeStorePath(opts);
|
|
640
|
+
const journalPath = journalPathFor(storePath);
|
|
641
|
+
const lockPath = lockPathFor(storePath);
|
|
642
|
+
|
|
643
|
+
ensureDir(configPath);
|
|
644
|
+
ensureDir(storePath);
|
|
645
|
+
|
|
646
|
+
const acquired = tryAcquire(lockPath);
|
|
647
|
+
if (!acquired.ok) return { ok: false, error: "locked" };
|
|
648
|
+
const handle = acquired.handle;
|
|
649
|
+
|
|
650
|
+
try {
|
|
651
|
+
// 1. recovery first: a journal on disk means an earlier attempt never
|
|
652
|
+
// committed, and we must not stack a second transaction on top of it.
|
|
653
|
+
const recovered = recoverJournal(journalPath, { configPath, storePath });
|
|
654
|
+
if (!recovered.ok) return { ok: false, error: "recovery_required", detail: recovered.detail };
|
|
655
|
+
|
|
656
|
+
// 2. re-read and compare against the caller's edit base.
|
|
657
|
+
const configBytes = readFileOrNull(configPath);
|
|
658
|
+
const storeBytes = readFileOrNull(storePath);
|
|
659
|
+
if (existsSync(configPath) && configBytes === null) {
|
|
660
|
+
return { ok: false, error: "config_unreadable" };
|
|
661
|
+
}
|
|
662
|
+
if (computeRevision(configBytes, storeBytes) !== revision) {
|
|
663
|
+
return { ok: false, error: "stale_revision" };
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const snapshot = readPromptLayers({ configPath, storePath });
|
|
667
|
+
const built = build(snapshot, configBytes, storeBytes);
|
|
668
|
+
if ("error" in built) return { ok: false, error: built.error, detail: built.detail };
|
|
669
|
+
|
|
670
|
+
const { nextConfig, nextStore } = built;
|
|
671
|
+
const configChanged = nextConfig !== configBytes;
|
|
672
|
+
const storeChanged = nextStore !== storeBytes;
|
|
673
|
+
if (!configChanged && !storeChanged) {
|
|
674
|
+
return { ok: true, changed: false, snapshot };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// 3. journal the intent. Its existence is NOT a commit.
|
|
678
|
+
const record: JournalRecord = {
|
|
679
|
+
configPath,
|
|
680
|
+
storePath,
|
|
681
|
+
preConfig: hashBytes(configBytes),
|
|
682
|
+
postConfig: hashBytes(nextConfig),
|
|
683
|
+
preStore: hashBytes(storeBytes),
|
|
684
|
+
postStore: hashBytes(nextStore),
|
|
685
|
+
preConfigBytes: configBytes,
|
|
686
|
+
postConfigBytes: nextConfig,
|
|
687
|
+
preStoreBytes: storeBytes,
|
|
688
|
+
postStoreBytes: nextStore,
|
|
689
|
+
};
|
|
690
|
+
durableWrite(journalPath, encodeJournal(record));
|
|
691
|
+
|
|
692
|
+
// 4/5. each target re-verifies ITS OWN bytes immediately before its rename,
|
|
693
|
+
// so a third party writing between step 2 and here is not overwritten.
|
|
694
|
+
if (configChanged) {
|
|
695
|
+
if (hashBytes(readFileOrNull(configPath)) !== record.preConfig) {
|
|
696
|
+
return rollback(record, journalPath, "stale_revision");
|
|
697
|
+
}
|
|
698
|
+
if (nextConfig === null) durableDelete(configPath);
|
|
699
|
+
else durableWrite(configPath, nextConfig);
|
|
700
|
+
}
|
|
701
|
+
if (storeChanged) {
|
|
702
|
+
if (hashBytes(readFileOrNull(storePath)) !== record.preStore) {
|
|
703
|
+
return rollback(record, journalPath, "stale_revision");
|
|
704
|
+
}
|
|
705
|
+
if (nextStore === null) durableDelete(storePath);
|
|
706
|
+
else durableWrite(storePath, nextStore);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// 6. verify COMPLETE bytes, not just our two lines: another writer could
|
|
710
|
+
// change an unrelated key and a narrow check would report success.
|
|
711
|
+
const finalConfig = hashBytes(readFileOrNull(configPath));
|
|
712
|
+
const finalStore = hashBytes(readFileOrNull(storePath));
|
|
713
|
+
if (finalConfig !== record.postConfig || finalStore !== record.postStore) {
|
|
714
|
+
return { ok: false, error: "write_superseded" };
|
|
715
|
+
}
|
|
716
|
+
if (!stillHeld(handle)) return { ok: false, error: "write_superseded" };
|
|
717
|
+
|
|
718
|
+
durableDelete(journalPath); // this deletion is the commit
|
|
719
|
+
return { ok: true, changed: true, snapshot: readPromptLayers({ configPath, storePath }) };
|
|
720
|
+
} finally {
|
|
721
|
+
release(handle);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/** Undo whatever landed, then drop the journal. Never touches an unknown file. */
|
|
726
|
+
function rollback(record: JournalRecord, journalPath: string, error: WriteError): WriteResult {
|
|
727
|
+
const configNow = hashBytes(readFileOrNull(record.configPath));
|
|
728
|
+
const storeNow = hashBytes(readFileOrNull(record.storePath));
|
|
729
|
+
if (configNow !== record.preConfig && configNow !== record.postConfig) {
|
|
730
|
+
return { ok: false, error: "recovery_required", detail: record.configPath };
|
|
731
|
+
}
|
|
732
|
+
if (storeNow !== record.preStore && storeNow !== record.postStore) {
|
|
733
|
+
return { ok: false, error: "recovery_required", detail: record.storePath };
|
|
734
|
+
}
|
|
735
|
+
if (configNow === record.postConfig) {
|
|
736
|
+
if (record.preConfigBytes === null) durableDelete(record.configPath);
|
|
737
|
+
else durableWrite(record.configPath, record.preConfigBytes);
|
|
738
|
+
}
|
|
739
|
+
if (storeNow === record.postStore) {
|
|
740
|
+
if (record.preStoreBytes === null) durableDelete(record.storePath);
|
|
741
|
+
else durableWrite(record.storePath, record.preStoreBytes);
|
|
742
|
+
}
|
|
743
|
+
durableDelete(journalPath);
|
|
744
|
+
return { ok: false, error };
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/** Flip one of the five prompt toggles. */
|
|
748
|
+
export function setToggle(id: string, enabled: boolean, revision: string, opts?: Paths): WriteResult {
|
|
749
|
+
if (!isToggleId(id)) return { ok: false, error: "unknown_layer" };
|
|
750
|
+
const spec = TOGGLE_KEYS[id];
|
|
751
|
+
return commit(opts, revision, (_snapshot, configBytes, storeBytes) => ({
|
|
752
|
+
nextConfig: spec.table
|
|
753
|
+
? setTableBool(configBytes ?? "", spec.table, spec.key, enabled)
|
|
754
|
+
: setRootBool(configBytes ?? "", spec.key, enabled),
|
|
755
|
+
nextStore: storeBytes,
|
|
756
|
+
}));
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/** Replace the whole custom-layer list; order is composition order. */
|
|
760
|
+
export function writeCustomLayers(layers: readonly CustomLayer[], revision: string, opts?: Paths): WriteResult {
|
|
761
|
+
for (const layer of layers) {
|
|
762
|
+
const normalized = normalizeBody(layer.body);
|
|
763
|
+
const invalid = findInvalidCharacter(normalized);
|
|
764
|
+
if (invalid !== null) {
|
|
765
|
+
return { ok: false, error: "invalid_characters", detail: `layer ${layer.id} at code point ${invalid.position}` };
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const normalizedLayers = layers.map(l => ({ ...l, body: normalizeBody(l.body) }));
|
|
769
|
+
return commit(opts, revision, (snapshot, configBytes, _storeBytes) => {
|
|
770
|
+
// Only a marker-owned key may be rewritten. Absent is fine — we create it.
|
|
771
|
+
const ownership = inspectOwnership(configBytes);
|
|
772
|
+
if (ownership.state === "external" || ownership.state === "owned-malformed") {
|
|
773
|
+
return { error: "developer_instructions_not_owned" };
|
|
774
|
+
}
|
|
775
|
+
const projection = composeProjection(normalizedLayers);
|
|
776
|
+
return {
|
|
777
|
+
nextConfig: setProjection(configBytes, projection.length > 0 ? projection : null),
|
|
778
|
+
nextStore: serializeStore(normalizedLayers),
|
|
779
|
+
};
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// ---------------------------------------------------------------------------
|
|
784
|
+
// Adoption — taking ownership of an externally authored key.
|
|
785
|
+
//
|
|
786
|
+
// Refusing alone was a dead end: the earlier answer was "delete your existing
|
|
787
|
+
// instructions by hand", which is not a feature. Adoption previews the raw line
|
|
788
|
+
// AND the exact body that will be committed, then imports on confirmation.
|
|
789
|
+
// ---------------------------------------------------------------------------
|
|
790
|
+
|
|
791
|
+
export interface AdoptPreview {
|
|
792
|
+
rawLine: string | null;
|
|
793
|
+
/** post-normalization body — byte-identical to what a confirm would store */
|
|
794
|
+
decodedBody: string | null;
|
|
795
|
+
reason: "ok" | "nothing_to_adopt" | "unsupported_form" | "invalid_characters";
|
|
796
|
+
path: string;
|
|
797
|
+
line: number | null;
|
|
798
|
+
detail?: string;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function newLayerId(existing: readonly CustomLayer[]): string {
|
|
802
|
+
const taken = new Set(existing.map(l => l.id));
|
|
803
|
+
for (;;) {
|
|
804
|
+
const id = randomBytes(4).toString("hex").slice(0, 6);
|
|
805
|
+
if (!taken.has(id)) return id;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Read-only. Runs the same five steps a confirm would, so preview and commit
|
|
811
|
+
* cannot disagree: decode -> normalize -> validate -> cap -> present.
|
|
812
|
+
*/
|
|
813
|
+
export function previewAdopt(opts?: Paths): AdoptPreview {
|
|
814
|
+
const configPath = activeConfigPath(opts);
|
|
815
|
+
const ownership = inspectOwnership(readFileOrNull(configPath));
|
|
816
|
+
|
|
817
|
+
if (ownership.state === "absent" || ownership.state === "owned") {
|
|
818
|
+
return { rawLine: null, decodedBody: null, reason: "nothing_to_adopt", path: configPath, line: null };
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const raw = ownership.raw;
|
|
822
|
+
const line = ownership.line;
|
|
823
|
+
const eq = raw.indexOf("=");
|
|
824
|
+
const literal = eq === -1 ? "" : raw.slice(eq + 1).trim().replace(/\s*#.*$/, "");
|
|
825
|
+
const decoded = decodeBasicString(literal);
|
|
826
|
+
if (decoded === null) {
|
|
827
|
+
return {
|
|
828
|
+
rawLine: raw,
|
|
829
|
+
decodedBody: null,
|
|
830
|
+
reason: "unsupported_form",
|
|
831
|
+
path: configPath,
|
|
832
|
+
line,
|
|
833
|
+
detail: "only a single-line basic string using \\\", \\\\ and \\n can be decoded safely",
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const normalized = normalizeBody(decoded);
|
|
838
|
+
const invalid = findInvalidCharacter(normalized);
|
|
839
|
+
if (invalid !== null) {
|
|
840
|
+
return {
|
|
841
|
+
rawLine: raw,
|
|
842
|
+
decodedBody: null,
|
|
843
|
+
reason: "invalid_characters",
|
|
844
|
+
path: configPath,
|
|
845
|
+
line,
|
|
846
|
+
detail: `code point ${invalid.position} is a ${invalid.reason}`,
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return { rawLine: raw, decodedBody: normalized, reason: "ok", path: configPath, line };
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/** Import the external value as one custom layer and take ownership of the key. */
|
|
854
|
+
export function adoptDeveloperInstructions(revision: string, opts?: Paths): WriteResult {
|
|
855
|
+
const preview = previewAdopt(opts);
|
|
856
|
+
if (preview.reason !== "ok" || preview.decodedBody === null) {
|
|
857
|
+
return {
|
|
858
|
+
ok: false,
|
|
859
|
+
error: preview.reason === "invalid_characters" ? "invalid_characters" : "developer_instructions_not_owned",
|
|
860
|
+
detail: preview.detail,
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
const body = preview.decodedBody;
|
|
864
|
+
return commit(opts, revision, (snapshot, configBytes) => {
|
|
865
|
+
const existing = snapshot.custom;
|
|
866
|
+
const adopted: CustomLayer = {
|
|
867
|
+
id: newLayerId(existing),
|
|
868
|
+
title: "Imported from config.toml",
|
|
869
|
+
body,
|
|
870
|
+
enabled: true,
|
|
871
|
+
};
|
|
872
|
+
const layers = [adopted, ...existing];
|
|
873
|
+
// Drop the unowned line first, then write the canonical owned block.
|
|
874
|
+
const stripped = removeUnownedProjection(configBytes ?? "");
|
|
875
|
+
return {
|
|
876
|
+
nextConfig: setProjection(stripped, composeProjection(layers)),
|
|
877
|
+
nextStore: serializeStore(layers),
|
|
878
|
+
};
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/** Remove an unowned or reshaped `developer_instructions` from the root scope. */
|
|
883
|
+
function removeUnownedProjection(content: string): string {
|
|
884
|
+
const eol = dominantEol(content);
|
|
885
|
+
const lines = splitLines(content);
|
|
886
|
+
const limit = firstTableIndex(lines);
|
|
887
|
+
for (let i = 0; i < limit; i += 1) {
|
|
888
|
+
if (!ANY_DEV_INSTRUCTIONS.test(lines[i]!)) continue;
|
|
889
|
+
const marked = i > 0 && lines[i - 1]!.includes(OCX_SECTION_MARKER);
|
|
890
|
+
lines.splice(marked ? i - 1 : i, marked ? 2 : 1);
|
|
891
|
+
return joinLines(lines, eol);
|
|
892
|
+
}
|
|
893
|
+
return joinLines(lines, eol);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// ---------------------------------------------------------------------------
|
|
897
|
+
// Salvage — the store is gone while a live projection remains.
|
|
898
|
+
//
|
|
899
|
+
// This is salvage, NOT reconstruction. The projection is one concatenated
|
|
900
|
+
// string: layer boundaries, ids, titles, order, disabled layers, and whether a
|
|
901
|
+
// blank line separated two layers or was the user's own text are all gone.
|
|
902
|
+
// ---------------------------------------------------------------------------
|
|
903
|
+
|
|
904
|
+
export interface SalvagePreview {
|
|
905
|
+
body: string | null;
|
|
906
|
+
/** the DIRECTORY backups land in. A read-only preview reserves no filename. */
|
|
907
|
+
backupDir: string;
|
|
908
|
+
unrecoverable: readonly string[];
|
|
909
|
+
reason: "ok" | "nothing_to_salvage";
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const UNRECOVERABLE = Object.freeze([
|
|
913
|
+
"layer boundaries",
|
|
914
|
+
"layer ids",
|
|
915
|
+
"layer titles",
|
|
916
|
+
"row order",
|
|
917
|
+
"disabled layers and their bodies",
|
|
918
|
+
"whether a blank line separated two layers or was your own text",
|
|
919
|
+
]);
|
|
920
|
+
|
|
921
|
+
export function previewSalvage(opts?: Paths): SalvagePreview {
|
|
922
|
+
const configPath = activeConfigPath(opts);
|
|
923
|
+
const storePath = activeStorePath(opts);
|
|
924
|
+
const ownership = inspectOwnership(readFileOrNull(configPath));
|
|
925
|
+
const body = ownership.state === "owned" ? decodeBasicString(ownership.literal) : null;
|
|
926
|
+
return {
|
|
927
|
+
body,
|
|
928
|
+
// `dirname`, not a hand-rolled `lastIndexOf("/")`: a Windows store path is
|
|
929
|
+
// `D:\...\store.toml`, which contains no forward slash at all, so the
|
|
930
|
+
// slice returned `"."` and the preview named the wrong directory.
|
|
931
|
+
backupDir: dirname(storePath),
|
|
932
|
+
unrecoverable: UNRECOVERABLE,
|
|
933
|
+
reason: body !== null && body.length > 0 ? "ok" : "nothing_to_salvage",
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Adopt the live projection as ONE layer. A durable backup is written first and
|
|
939
|
+
* salvage aborts if it cannot be created — a destructive operation whose safety
|
|
940
|
+
* net failed should not proceed.
|
|
941
|
+
*/
|
|
942
|
+
export function salvageProjection(revision: string, opts?: Paths): WriteResult {
|
|
943
|
+
const preview = previewSalvage(opts);
|
|
944
|
+
if (preview.reason !== "ok" || preview.body === null) {
|
|
945
|
+
return { ok: false, error: "developer_instructions_not_owned", detail: "no live projection to salvage" };
|
|
946
|
+
}
|
|
947
|
+
const body = preview.body;
|
|
948
|
+
const storePath = activeStorePath(opts);
|
|
949
|
+
const backupPath = `${storePath.replace(/\.json$/, "")}.salvage-${Date.now()}-${randomBytes(3).toString("hex")}.txt`;
|
|
950
|
+
try {
|
|
951
|
+
durableWriteExclusive(backupPath, body);
|
|
952
|
+
} catch {
|
|
953
|
+
return { ok: false, error: "recovery_required", detail: `could not write a durable backup at ${backupPath}` };
|
|
954
|
+
}
|
|
955
|
+
return commit(opts, revision, (_snapshot, configBytes) => {
|
|
956
|
+
const salvaged: CustomLayer = {
|
|
957
|
+
id: newLayerId([]),
|
|
958
|
+
title: "Salvaged from config.toml",
|
|
959
|
+
body,
|
|
960
|
+
enabled: true,
|
|
961
|
+
};
|
|
962
|
+
return {
|
|
963
|
+
nextConfig: setProjection(configBytes, body),
|
|
964
|
+
nextStore: serializeStore([salvaged]),
|
|
965
|
+
};
|
|
966
|
+
});
|
|
967
|
+
}
|