@remodex/rmx 1.0.2
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 +91 -0
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/assets/architecture.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/claude-code-models.gif +0 -0
- package/assets/codex-app-picker.png +0 -0
- package/bin/ocx.mjs +584 -0
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-CZqebSPQ.css +1 -0
- package/gui/dist/assets/index-CkETtt7P.js +71 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/fonts/google-sans-cyrillic.woff2 +0 -0
- package/gui/dist/fonts/google-sans-latin.woff2 +0 -0
- package/gui/dist/icons.svg +24 -0
- package/gui/dist/index.html +25 -0
- package/gui/dist/logo.png +0 -0
- package/gui/dist/provider-icons/alibaba-color.svg +1 -0
- package/gui/dist/provider-icons/antigravity-color.svg +1 -0
- package/gui/dist/provider-icons/claude-color.svg +1 -0
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -0
- package/gui/dist/provider-icons/cursor-color.svg +2 -0
- package/gui/dist/provider-icons/deepseek-color.svg +1 -0
- package/gui/dist/provider-icons/discord.svg +1 -0
- package/gui/dist/provider-icons/firepass-color.svg +1 -0
- package/gui/dist/provider-icons/fireworks-color.svg +1 -0
- package/gui/dist/provider-icons/gemini-color.svg +1 -0
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
- package/gui/dist/provider-icons/grok.svg +1 -0
- package/gui/dist/provider-icons/groq-color.svg +1 -0
- package/gui/dist/provider-icons/huggingface-color.svg +1 -0
- package/gui/dist/provider-icons/kimi-color.svg +1 -0
- package/gui/dist/provider-icons/kiro-color.svg +15 -0
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
- package/gui/dist/provider-icons/mistral-color.svg +1 -0
- package/gui/dist/provider-icons/moonshot-color.svg +1 -0
- package/gui/dist/provider-icons/nvidia-color.svg +1 -0
- package/gui/dist/provider-icons/ollama-color.svg +1 -0
- package/gui/dist/provider-icons/openai.svg +1 -0
- package/gui/dist/provider-icons/opencode.svg +2 -0
- package/gui/dist/provider-icons/openrouter-color.svg +1 -0
- package/gui/dist/provider-icons/pi.svg +21 -0
- package/gui/dist/provider-icons/qianfan-color.svg +1 -0
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
- package/gui/dist/provider-icons/telegram.svg +1 -0
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/vllm-color.svg +1 -0
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
- package/package.json +118 -0
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic-image-guard.ts +251 -0
- package/src/adapters/anthropic-image-normalize.ts +518 -0
- package/src/adapters/anthropic.ts +1205 -0
- package/src/adapters/azure.ts +36 -0
- package/src/adapters/base.ts +83 -0
- package/src/adapters/client-fingerprint.ts +59 -0
- package/src/adapters/command-code.ts +453 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +104 -0
- package/src/adapters/cursor/cursor-errors.ts +165 -0
- package/src/adapters/cursor/discovery.ts +276 -0
- package/src/adapters/cursor/effort-map.ts +139 -0
- package/src/adapters/cursor/exec-policy.ts +88 -0
- package/src/adapters/cursor/framing.ts +250 -0
- package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
- package/src/adapters/cursor/kv-store.ts +52 -0
- package/src/adapters/cursor/live-models.ts +153 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +1235 -0
- package/src/adapters/cursor/mcp-config.ts +42 -0
- package/src/adapters/cursor/mcp-manager.ts +333 -0
- package/src/adapters/cursor/message-mapper.ts +49 -0
- package/src/adapters/cursor/native-exec-common.ts +59 -0
- package/src/adapters/cursor/native-exec-desktop.ts +184 -0
- package/src/adapters/cursor/native-exec-fs.ts +332 -0
- package/src/adapters/cursor/native-exec-mcp.ts +153 -0
- package/src/adapters/cursor/native-exec-network.ts +43 -0
- package/src/adapters/cursor/native-exec-shell.ts +548 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +604 -0
- package/src/adapters/cursor/protobuf-events.ts +735 -0
- package/src/adapters/cursor/protobuf-request.ts +719 -0
- package/src/adapters/cursor/request-builder.ts +280 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/tool-definitions.ts +621 -0
- package/src/adapters/cursor/transport-retry.ts +132 -0
- package/src/adapters/cursor/transport.ts +57 -0
- package/src/adapters/cursor/types.ts +59 -0
- package/src/adapters/cursor.ts +196 -0
- package/src/adapters/google-antigravity-replay.ts +520 -0
- package/src/adapters/google-antigravity-wire.ts +140 -0
- package/src/adapters/google-errors.ts +85 -0
- package/src/adapters/google-http.ts +100 -0
- package/src/adapters/google-tool-schema.ts +173 -0
- package/src/adapters/google-truncation.ts +24 -0
- package/src/adapters/google-wire-compiler.ts +232 -0
- package/src/adapters/google.ts +859 -0
- package/src/adapters/identity.ts +77 -0
- package/src/adapters/image.ts +23 -0
- package/src/adapters/kiro-constants.ts +16 -0
- package/src/adapters/kiro-errors.ts +208 -0
- package/src/adapters/kiro-events.ts +197 -0
- package/src/adapters/kiro-images.ts +129 -0
- package/src/adapters/kiro-retry.ts +312 -0
- package/src/adapters/kiro-thinking.ts +104 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +224 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +129 -0
- package/src/adapters/kiro.ts +1924 -0
- package/src/adapters/mimo-free.ts +263 -0
- package/src/adapters/openai-chat.ts +1265 -0
- package/src/adapters/openai-responses.ts +1309 -0
- package/src/adapters/run-turn-queue.ts +114 -0
- package/src/adapters/tool-catalog-nudge.ts +71 -0
- package/src/adapters/upstream-http-error.ts +48 -0
- package/src/android-remote/assets.ts +218 -0
- package/src/android-remote/attachments.ts +168 -0
- package/src/android-remote/auth.ts +237 -0
- package/src/android-remote/cloudflare-provisioning.ts +409 -0
- package/src/android-remote/cloudflare-secret.ts +106 -0
- package/src/android-remote/cloudflare-tunnel.ts +488 -0
- package/src/android-remote/cloudflared.ts +286 -0
- package/src/android-remote/codex-app-server.ts +565 -0
- package/src/android-remote/desktop-history-page.ts +936 -0
- package/src/android-remote/desktop-ipc.ts +3643 -0
- package/src/android-remote/desktop-ownership-store.ts +98 -0
- package/src/android-remote/desktop-project-registration.ts +129 -0
- package/src/android-remote/desktop-session-stream.ts +1110 -0
- package/src/android-remote/desktop-workspace-state.ts +443 -0
- package/src/android-remote/file-change-parser.ts +112 -0
- package/src/android-remote/gateway.ts +9108 -0
- package/src/android-remote/mutation-store.ts +299 -0
- package/src/android-remote/projection.ts +1780 -0
- package/src/android-remote/queued-turn-store.ts +248 -0
- package/src/android-remote/session-command-recovery.ts +1384 -0
- package/src/android-remote/store.ts +466 -0
- package/src/android-remote/thread-reconciliation.ts +133 -0
- package/src/android-remote/thread-source-paths.ts +307 -0
- package/src/android-remote/thread-stream.ts +546 -0
- package/src/android-remote/turn-activity.ts +235 -0
- package/src/android-remote/user-message-identity.ts +94 -0
- package/src/bridge.ts +1793 -0
- package/src/chat/inbound.ts +295 -0
- package/src/chat/outbound.ts +821 -0
- package/src/claude/agents-inject.ts +267 -0
- package/src/claude/alias.ts +149 -0
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/context-windows.ts +189 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +601 -0
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/gateway-cache.ts +70 -0
- package/src/claude/inbound-debug.ts +163 -0
- package/src/claude/inbound.ts +519 -0
- package/src/claude/model-info.ts +154 -0
- package/src/claude/outbound.ts +898 -0
- package/src/cli/access.ts +108 -0
- package/src/cli/account-api.ts +296 -0
- package/src/cli/account-auth.ts +250 -0
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +476 -0
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +297 -0
- package/src/cli/agent-driven.ts +70 -0
- package/src/cli/agent.ts +184 -0
- package/src/cli/catalog-prewarm.ts +27 -0
- package/src/cli/claude-desktop.ts +211 -0
- package/src/cli/claude.ts +302 -0
- package/src/cli/codex-shim-autorestore.ts +45 -0
- package/src/cli/codex-shim-readiness.ts +69 -0
- package/src/cli/combo.ts +124 -0
- package/src/cli/config-command.ts +183 -0
- package/src/cli/debug.ts +228 -0
- package/src/cli/desktop-first-run.ts +25 -0
- package/src/cli/doctor.ts +1022 -0
- package/src/cli/export-command.ts +201 -0
- package/src/cli/help.ts +370 -0
- package/src/cli/index.ts +1565 -0
- package/src/cli/init.ts +224 -0
- package/src/cli/integrations.ts +225 -0
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/internal-dispatch.ts +35 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/models-runtime.ts +224 -0
- package/src/cli/models.ts +340 -0
- package/src/cli/observe.ts +170 -0
- package/src/cli/opencode.ts +587 -0
- package/src/cli/provider-runtime.ts +179 -0
- package/src/cli/provider.ts +476 -0
- package/src/cli/ready.ts +301 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +328 -0
- package/src/cli/star-prompt.ts +211 -0
- package/src/cli/status-oauth.ts +78 -0
- package/src/cli/status.ts +321 -0
- package/src/cli/system-command.ts +196 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +205 -0
- package/src/cli/v2.ts +200 -0
- package/src/cli.ts +10 -0
- package/src/clients/config-export.ts +1109 -0
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-label.ts +34 -0
- package/src/codex/account-lifecycle.ts +172 -0
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +195 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/account-runtime-state.ts +31 -0
- package/src/codex/account-store.ts +517 -0
- package/src/codex/account-usability.ts +40 -0
- package/src/codex/admission.ts +263 -0
- package/src/codex/app-server-processes.ts +799 -0
- package/src/codex/auth-api.ts +2098 -0
- package/src/codex/auth-collision.ts +107 -0
- package/src/codex/auth-context.ts +480 -0
- package/src/codex/autostart-health.ts +156 -0
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +471 -0
- package/src/codex/catalog/bundled.ts +533 -0
- package/src/codex/catalog/effort.ts +432 -0
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +287 -0
- package/src/codex/catalog/native-models.ts +7 -0
- package/src/codex/catalog/parsing.ts +503 -0
- package/src/codex/catalog/provider-fetch.ts +2267 -0
- package/src/codex/catalog/sync.ts +1606 -0
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-refresh-status.ts +87 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +15 -0
- package/src/codex/codex-write-lock.ts +384 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +580 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/desktop-client-processes.ts +521 -0
- package/src/codex/exec-invocation.ts +22 -0
- package/src/codex/features.ts +1091 -0
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +347 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-migration-guardian.ts +115 -0
- package/src/codex/history-provider.ts +1075 -0
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +204 -0
- package/src/codex/home.ts +206 -0
- package/src/codex/inject-coordination.ts +257 -0
- package/src/codex/inject.ts +1857 -0
- package/src/codex/injected-marker.ts +79 -0
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +105 -0
- package/src/codex/journal.ts +172 -0
- package/src/codex/main-account-cache.ts +56 -0
- package/src/codex/main-account.ts +40 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/model-cache.ts +267 -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 +167 -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 +348 -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 +691 -0
- package/src/codex/paths.ts +78 -0
- package/src/codex/plugins-doctor.ts +242 -0
- package/src/codex/pool-rotation.ts +295 -0
- package/src/codex/project-config-warnings.ts +426 -0
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/provider-adoption.ts +242 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +494 -0
- package/src/codex/refresh.ts +60 -0
- package/src/codex/routing.ts +1855 -0
- package/src/codex/runtime.ts +659 -0
- package/src/codex/shim.ts +1215 -0
- package/src/codex/subagent-defaults.ts +557 -0
- package/src/codex/subagent-model-fallback.ts +560 -0
- package/src/codex/sync.ts +238 -0
- package/src/codex/transition-state.ts +612 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +374 -0
- package/src/codex/warmup.ts +192 -0
- package/src/codex/websocket-registry.ts +100 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +140 -0
- package/src/combos/index.ts +44 -0
- package/src/combos/request.ts +64 -0
- package/src/combos/resolve.ts +232 -0
- package/src/combos/types.ts +392 -0
- package/src/config.ts +3270 -0
- package/src/generated/model-metadata.ts +144 -0
- package/src/github/star-state.ts +203 -0
- package/src/grok/inject.ts +540 -0
- package/src/grok/inspect.ts +45 -0
- package/src/grok/status.ts +127 -0
- package/src/grok/sync.ts +66 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +149 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +922 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/index.ts +22 -0
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +108 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/abort.ts +146 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bounded-body.ts +242 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +184 -0
- package/src/lib/bun-stream-caps.ts +127 -0
- package/src/lib/config-ownership.ts +438 -0
- package/src/lib/crash-guard.ts +344 -0
- package/src/lib/debug-log-buffer.ts +83 -0
- package/src/lib/debug-settings.ts +108 -0
- package/src/lib/debug.ts +31 -0
- package/src/lib/destination-policy.ts +316 -0
- package/src/lib/errors.ts +364 -0
- package/src/lib/eventstream-decoder.ts +253 -0
- package/src/lib/gcp-adc.ts +341 -0
- package/src/lib/injection-debug-log.ts +58 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/open-url.ts +25 -0
- package/src/lib/pinned-http.ts +182 -0
- package/src/lib/privacy.ts +20 -0
- package/src/lib/process-control.ts +168 -0
- package/src/lib/provider-environment.ts +470 -0
- package/src/lib/provider-outbound.ts +203 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/redact.ts +510 -0
- package/src/lib/remodex-home.ts +616 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/service-secrets.ts +178 -0
- package/src/lib/shadow-call.ts +54 -0
- package/src/lib/sidecar-tracker.ts +52 -0
- package/src/lib/sse-decoder.ts +364 -0
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/test-home-guard.ts +98 -0
- package/src/lib/token-estimate.ts +69 -0
- package/src/lib/translator-budget.ts +366 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +508 -0
- package/src/lib/win-exec.ts +115 -0
- package/src/lib/win-paths.ts +68 -0
- package/src/lib/windows-elevation.ts +705 -0
- package/src/lib/windows-secret-acl.ts +817 -0
- package/src/lib/windows-user-principal.ts +283 -0
- package/src/lib/winsw.ts +402 -0
- package/src/model-sources.ts +73 -0
- package/src/oauth/anthropic-routing.ts +594 -0
- package/src/oauth/anthropic.ts +177 -0
- package/src/oauth/callback-server.ts +294 -0
- package/src/oauth/chatgpt.ts +150 -0
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +231 -0
- package/src/oauth/github-copilot.ts +428 -0
- package/src/oauth/google-antigravity.ts +230 -0
- package/src/oauth/health.ts +443 -0
- package/src/oauth/index.ts +1280 -0
- package/src/oauth/key-providers.ts +128 -0
- package/src/oauth/kimi.ts +213 -0
- package/src/oauth/kiro-credentials.ts +726 -0
- package/src/oauth/kiro.ts +621 -0
- package/src/oauth/local-token-detect.ts +121 -0
- package/src/oauth/log.ts +48 -0
- package/src/oauth/login-cli.ts +163 -0
- package/src/oauth/pkce.ts +15 -0
- package/src/oauth/store.ts +655 -0
- package/src/oauth/token-guardian.ts +309 -0
- package/src/oauth/types.ts +62 -0
- package/src/oauth/xai.ts +241 -0
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +156 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/antigravity-models.ts +317 -0
- package/src/providers/api-keys.ts +140 -0
- package/src/providers/base-url-choices.ts +64 -0
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/command-code-efforts.ts +85 -0
- package/src/providers/context-cap.ts +73 -0
- package/src/providers/derive.ts +451 -0
- package/src/providers/free-directory.ts +187 -0
- package/src/providers/github-copilot-transport.ts +56 -0
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +271 -0
- package/src/providers/kiro-models.ts +67 -0
- package/src/providers/label.ts +19 -0
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +361 -0
- package/src/providers/openai-sidecar.ts +235 -0
- package/src/providers/openai-tier-startup.ts +27 -0
- package/src/providers/openai-tiers.ts +301 -0
- package/src/providers/openai-virtual-models.ts +83 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +179 -0
- package/src/providers/quota.ts +1942 -0
- package/src/providers/reasoning-capabilities.ts +336 -0
- package/src/providers/registry.ts +2375 -0
- package/src/providers/slug-codec.ts +74 -0
- package/src/providers/xai-transport.ts +149 -0
- package/src/reasoning-effort.ts +243 -0
- package/src/responses/compaction.ts +124 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +714 -0
- package/src/responses/reasoning-envelope.ts +60 -0
- package/src/responses/reasoning-replay-cache.ts +106 -0
- package/src/responses/schema.ts +159 -0
- package/src/responses/spill-store.ts +431 -0
- package/src/responses/state.ts +1039 -0
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +802 -0
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +205 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -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 +424 -0
- package/src/routing/quota.ts +145 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/adapter-resolve.ts +83 -0
- package/src/server/auth-cors.ts +606 -0
- package/src/server/chat-completions.ts +379 -0
- package/src/server/claude-messages.ts +980 -0
- package/src/server/effort-policy.ts +251 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +152 -0
- package/src/server/image-retry.ts +42 -0
- package/src/server/images.ts +485 -0
- package/src/server/index.ts +1633 -0
- package/src/server/lifecycle.ts +482 -0
- package/src/server/live.ts +609 -0
- package/src/server/management/agent-settings-routes.ts +1180 -0
- package/src/server/management/android-remote-routes.ts +390 -0
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/combo-routes.ts +244 -0
- package/src/server/management/config-routes.ts +602 -0
- package/src/server/management/context.ts +88 -0
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +516 -0
- package/src/server/management/model-routes.ts +519 -0
- package/src/server/management/model-rows.ts +143 -0
- package/src/server/management/native-integration-routes.ts +781 -0
- package/src/server/management/oauth-account-routes.ts +573 -0
- package/src/server/management/provider-routes.ts +781 -0
- 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 +384 -0
- package/src/server/management/shared.ts +277 -0
- package/src/server/management/sidebar-routes.ts +106 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +433 -0
- package/src/server/management/system-routes.ts +141 -0
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +269 -0
- package/src/server/management-auth.ts +353 -0
- package/src/server/memory-watchdog.ts +156 -0
- package/src/server/port-reclaim.ts +307 -0
- package/src/server/ports.ts +156 -0
- package/src/server/proxy-liveness.ts +326 -0
- package/src/server/proxy-stop.ts +92 -0
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +353 -0
- package/src/server/relay.ts +1179 -0
- package/src/server/request-decompress.ts +132 -0
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +1072 -0
- package/src/server/responses/collaboration.ts +409 -0
- package/src/server/responses/compact.ts +710 -0
- package/src/server/responses/core.ts +3561 -0
- package/src/server/responses/encrypted-payload.ts +308 -0
- package/src/server/responses/fetch-helpers.ts +171 -0
- package/src/server/responses/passthrough-error.ts +78 -0
- package/src/server/responses/policy-fallback.ts +152 -0
- package/src/server/responses/terminal-guard.ts +230 -0
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/responses-image-gen-repair.ts +132 -0
- package/src/server/responses-item-id-repair.ts +272 -0
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses.ts +10 -0
- package/src/server/search.ts +181 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +263 -0
- package/src/server/startup-action-control.ts +308 -0
- package/src/server/startup-health-cache.ts +119 -0
- package/src/server/system-env.ts +418 -0
- package/src/server/windows-tcp-drop.ts +184 -0
- package/src/server/windows-tray-control.ts +41 -0
- package/src/server/ws-bridge.ts +470 -0
- package/src/service-manager-probe.ts +824 -0
- package/src/service.ts +3011 -0
- package/src/stall-timeout.ts +20 -0
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +3085 -0
- package/src/storage/policy-job.ts +457 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +59 -0
- package/src/storage/policy.ts +527 -0
- package/src/storage/restore-job.ts +299 -0
- package/src/storage/restore-worker.ts +58 -0
- package/src/storage/scanner.ts +238 -0
- package/src/storage/storage-mutation-coordinator.ts +139 -0
- package/src/storage/worker-lifecycle.ts +215 -0
- package/src/tray/assets/opencodex-tray-offline.ico +0 -0
- package/src/tray/assets/opencodex-tray-online.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning.ico +0 -0
- package/src/tray/assets/opencodex-tray.png +0 -0
- package/src/tray/windows-tray.ps1 +364 -0
- package/src/tray/windows.ts +738 -0
- package/src/types.ts +1531 -0
- package/src/update/badge.ts +72 -0
- package/src/update/desktop-release.ts +1620 -0
- package/src/update/index.ts +402 -0
- package/src/update/job.ts +1906 -0
- package/src/update/notify.ts +261 -0
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/update/tray-update-plan.d.mts +18 -0
- package/src/update/tray-update-plan.mjs +38 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/debug.ts +97 -0
- package/src/usage/expected-prices.ts +283 -0
- package/src/usage/log.ts +695 -0
- package/src/usage/summary.ts +585 -0
- package/src/usage/totals.ts +14 -0
- package/src/vision/anthropic-describe.ts +185 -0
- package/src/vision/describe.ts +127 -0
- package/src/vision/index.ts +558 -0
- package/src/vision/reasoning.ts +55 -0
- package/src/web-search/anthropic-executor.ts +189 -0
- package/src/web-search/executor.ts +105 -0
- package/src/web-search/format-result.ts +89 -0
- package/src/web-search/index.ts +196 -0
- package/src/web-search/loop.ts +791 -0
- package/src/web-search/parse.ts +235 -0
- package/src/web-search/progress-stream.ts +342 -0
- package/src/web-search/synthetic-tool.ts +47 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The filesystem seam shared by the reader and the writer.
|
|
3
|
+
*
|
|
4
|
+
* It lives in WP2 rather than beside the writer because `readIntegrationState`
|
|
5
|
+
* needs it too, and a reader that disagreed with the writer about what counts
|
|
6
|
+
* as an absent file is exactly how an unreadable config gets overwritten.
|
|
7
|
+
*
|
|
8
|
+
* Design of record: devlog/_fin/260802_client_toggle_api/021 §5-6.
|
|
9
|
+
*/
|
|
10
|
+
import { mkdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
11
|
+
import type { ConfigFormat } from "../clients/config-export";
|
|
12
|
+
import { atomicWriteFile } from "../config";
|
|
13
|
+
import type { JournalEntry } from "./journal";
|
|
14
|
+
import type { OwnershipRecord } from "./ownership";
|
|
15
|
+
import type { IntegrationClientId } from "./registry";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A parse failure sentinel. This is deliberately ONE symbol exported from ONE
|
|
19
|
+
* module: two `Symbol("parse-failed")` calls produce different values, so a
|
|
20
|
+
* second declaration would not duplicate code — it would make every comparison
|
|
21
|
+
* against it fail, and an unparseable config would be classified `absent` and
|
|
22
|
+
* then overwritten.
|
|
23
|
+
*/
|
|
24
|
+
export const PARSE_FAILED = Symbol("parse-failed");
|
|
25
|
+
|
|
26
|
+
/** Parse a client config, tolerating absence. PARSE_FAILED on garbage. */
|
|
27
|
+
export function parseConfig(text: string | null, format: ConfigFormat): unknown | typeof PARSE_FAILED {
|
|
28
|
+
if (text === null || text.trim().length === 0) return {};
|
|
29
|
+
try {
|
|
30
|
+
switch (format) {
|
|
31
|
+
case "json": return JSON.parse(text);
|
|
32
|
+
case "json5": return Bun.JSON5.parse(text);
|
|
33
|
+
case "yaml": return Bun.YAML.parse(text);
|
|
34
|
+
case "toml": {
|
|
35
|
+
/*
|
|
36
|
+
* Bun's TOML parser mangles the special floats before we ever see the
|
|
37
|
+
* document: `inf` comes back as the STRING "inf", `-inf` as the number
|
|
38
|
+
* 0, and `nan` as "nan". Re-serializing that would write those
|
|
39
|
+
* corruptions back to the user's file while reporting success — a
|
|
40
|
+
* silent value change is worse than a refusal, so a document
|
|
41
|
+
* containing them is treated as one we cannot safely rewrite.
|
|
42
|
+
*
|
|
43
|
+
* Detected on the raw text, because by the time it is parsed the
|
|
44
|
+
* evidence is gone.
|
|
45
|
+
*/
|
|
46
|
+
if (/(^|[\s,[=])[-+]?(?:inf|nan)(?=[\s,\]]|$)/mi.test(text)) return PARSE_FAILED;
|
|
47
|
+
return Bun.TOML.parse(text);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
return PARSE_FAILED;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type ReadResult =
|
|
56
|
+
| { kind: "text"; text: string }
|
|
57
|
+
| { kind: "missing" }
|
|
58
|
+
| { kind: "failed"; code?: string };
|
|
59
|
+
|
|
60
|
+
export type StatKind = "file" | "dir" | "other" | "missing" | "failed";
|
|
61
|
+
|
|
62
|
+
export interface IntegrationIO {
|
|
63
|
+
/**
|
|
64
|
+
* ONLY a missing file yields `missing`. Every other failure (EACCES, EPERM,
|
|
65
|
+
* EISDIR, …) yields `failed`, which callers must treat as unsafe.
|
|
66
|
+
*/
|
|
67
|
+
readText: (path: string) => ReadResult;
|
|
68
|
+
/** `failed` is distinct from `missing` for the same reason. */
|
|
69
|
+
statKind: (path: string) => StatKind;
|
|
70
|
+
writeText: (path: string, text: string) => void;
|
|
71
|
+
removeFile: (path: string) => void;
|
|
72
|
+
mkdirp: (path: string) => void;
|
|
73
|
+
now: () => number;
|
|
74
|
+
/** Bookkeeping seams, bound to one store so a test can redirect them. */
|
|
75
|
+
appendJournal: (entry: JournalEntry) => void;
|
|
76
|
+
putRecord: (record: OwnershipRecord) => void;
|
|
77
|
+
dropRecord: (clientId: IntegrationClientId) => void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type TargetState =
|
|
81
|
+
| { ok: true; before: string | null }
|
|
82
|
+
| { ok: false; why: "not-regular-file" | "read-failed" };
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Read the target and classify the three failure shapes correctly.
|
|
86
|
+
*
|
|
87
|
+
* The subtle case is a `stat` that succeeds as a file and a `read` that then
|
|
88
|
+
* fails: that is a real file we cannot see, and treating it as absence is how
|
|
89
|
+
* an unreadable config gets clobbered.
|
|
90
|
+
*/
|
|
91
|
+
export function loadTarget(io: IntegrationIO, configPath: string): TargetState {
|
|
92
|
+
const kind = io.statKind(configPath);
|
|
93
|
+
if (kind === "missing") return { ok: true, before: null };
|
|
94
|
+
if (kind === "failed") return { ok: false, why: "read-failed" };
|
|
95
|
+
if (kind !== "file") return { ok: false, why: "not-regular-file" };
|
|
96
|
+
const read = io.readText(configPath);
|
|
97
|
+
if (read.kind === "text") return { ok: true, before: read.text };
|
|
98
|
+
if (read.kind === "failed") return { ok: false, why: "read-failed" };
|
|
99
|
+
// Raced deletion between the stat and the read.
|
|
100
|
+
return { ok: true, before: null };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Filesystem half of the seam, split from the store-bound half so there is one
|
|
105
|
+
* place that decides what a failed read or stat MEANS, and one place that
|
|
106
|
+
* decides WHERE bookkeeping goes.
|
|
107
|
+
*/
|
|
108
|
+
export function fileIO(): Omit<IntegrationIO, "appendJournal" | "putRecord" | "dropRecord"> {
|
|
109
|
+
return {
|
|
110
|
+
readText: path => {
|
|
111
|
+
try {
|
|
112
|
+
return { kind: "text", text: readFileSync(path, "utf8") };
|
|
113
|
+
} catch (error) {
|
|
114
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
115
|
+
return code === "ENOENT" ? { kind: "missing" } : { kind: "failed", ...(code ? { code } : {}) };
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
statKind: path => {
|
|
119
|
+
try {
|
|
120
|
+
const stats = statSync(path);
|
|
121
|
+
return stats.isFile() ? "file" : stats.isDirectory() ? "dir" : "other";
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return (error as NodeJS.ErrnoException).code === "ENOENT" ? "missing" : "failed";
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
writeText: (path, text) => atomicWriteFile(path, text),
|
|
127
|
+
removeFile: path => rmSync(path, { force: true }),
|
|
128
|
+
mkdirp: path => mkdirSync(path, { recursive: true, mode: 0o700 }),
|
|
129
|
+
now: () => Date.now(),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The full seam: filesystem behavior plus bookkeeping bound to ONE store.
|
|
135
|
+
*
|
|
136
|
+
* Kept together so the IO a writer uses and the store it journals into can
|
|
137
|
+
* never point at different roots — that split is what let an "isolated"
|
|
138
|
+
* operation mutate the real state while a test believed otherwise.
|
|
139
|
+
*/
|
|
140
|
+
export function defaultIntegrationIO(store: {
|
|
141
|
+
appendJournal: IntegrationIO["appendJournal"];
|
|
142
|
+
putRecord: IntegrationIO["putRecord"];
|
|
143
|
+
dropRecord: IntegrationIO["dropRecord"];
|
|
144
|
+
}): IntegrationIO {
|
|
145
|
+
return {
|
|
146
|
+
...fileIO(),
|
|
147
|
+
appendJournal: entry => store.appendJournal(entry),
|
|
148
|
+
putRecord: record => store.putRecord(record),
|
|
149
|
+
dropRecord: clientId => store.dropRecord(clientId),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The operation log, its snapshots, and the retention bookkeeping around them.
|
|
3
|
+
*
|
|
4
|
+
* Two ordering rules carry the safety here:
|
|
5
|
+
*
|
|
6
|
+
* 1. `appendOperation` commits the row and NOTHING else. Pruning runs after,
|
|
7
|
+
* best-effort, because a GC failure that looked like an append failure would
|
|
8
|
+
* make the writer compensate for an operation that already succeeded.
|
|
9
|
+
* 2. Snapshots can hold the user's own credentials — we copy their file
|
|
10
|
+
* verbatim — so they go through `atomicWriteFile`, which applies 0600 plus
|
|
11
|
+
* Windows ACL hardening.
|
|
12
|
+
*
|
|
13
|
+
* Design of record: devlog/_fin/260802_client_toggle_api/021 §4.
|
|
14
|
+
*/
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
import { appendFileSync, existsSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
17
|
+
import { join, resolve, sep } from "node:path";
|
|
18
|
+
import { atomicWriteFile } from "../config";
|
|
19
|
+
import { ensureDir, fingerprint, integrationsDir, type OwnershipRecord } from "./ownership";
|
|
20
|
+
import { isIntegrationClientId, type IntegrationClientId } from "./registry";
|
|
21
|
+
|
|
22
|
+
export type OperationKind = "apply" | "disable" | "refresh" | "restore";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Tagged so "the file did not exist" and "the snapshot was collected" stay
|
|
26
|
+
* distinguishable. `null` conflated them, and only one of the two is
|
|
27
|
+
* recoverable — restoring an operation that created a file means deleting it.
|
|
28
|
+
*/
|
|
29
|
+
export type SnapshotRef =
|
|
30
|
+
| { kind: "none" }
|
|
31
|
+
| { kind: "stored"; relPath: string }
|
|
32
|
+
| { kind: "expired" };
|
|
33
|
+
|
|
34
|
+
export interface JournalEntry {
|
|
35
|
+
opId: string;
|
|
36
|
+
clientId: IntegrationClientId;
|
|
37
|
+
kind: OperationKind;
|
|
38
|
+
at: string;
|
|
39
|
+
configPath: string;
|
|
40
|
+
snapshot: SnapshotRef;
|
|
41
|
+
/** Fingerprint of the file AFTER this op; "" when the op left no file. */
|
|
42
|
+
resultFingerprint: string;
|
|
43
|
+
/** True when the op's result was file absence — restore then means "delete". */
|
|
44
|
+
resultAbsent: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Ownership as it stood BEFORE this operation. Restore puts this back
|
|
47
|
+
* alongside the bytes, so provenance always describes the file it came with
|
|
48
|
+
* and is never re-derived from a provider-id prefix — which would silently
|
|
49
|
+
* adopt a user's own `opencodex/...` entry.
|
|
50
|
+
*/
|
|
51
|
+
priorRecord: OwnershipRecord | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const SNAPSHOT_RETENTION = 10;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Does the file on disk still hold what this operation left behind?
|
|
58
|
+
*
|
|
59
|
+
* The one place that answers it, because two places answered differently. An
|
|
60
|
+
* operation whose result was ABSENCE records `resultAbsent: true` and an empty
|
|
61
|
+
* `resultFingerprint`; the route compared a missing file to `""` and called it
|
|
62
|
+
* a match, while restore hashed `""` into a real digest and called the same
|
|
63
|
+
* unchanged absence a drift. So the journal offered Undo and the restore
|
|
64
|
+
* route then demanded a confirmation for edits nobody had made.
|
|
65
|
+
*
|
|
66
|
+
* `currentText` is `null` for a missing file — not `""`, which is a file that
|
|
67
|
+
* exists and is empty. The distinction is the whole point.
|
|
68
|
+
*/
|
|
69
|
+
export function matchesOperationResult(entry: JournalEntry, currentText: string | null): boolean {
|
|
70
|
+
if (entry.resultAbsent) return currentText === null;
|
|
71
|
+
if (currentText === null) return false;
|
|
72
|
+
return fingerprint(currentText) === entry.resultFingerprint;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function newOpId(): string {
|
|
76
|
+
return randomUUID();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function journalPath(dir: string): string {
|
|
80
|
+
return join(dir, "journal.jsonl");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A snapshot file name must be ONE path component we produced, never a value
|
|
85
|
+
* that walks anywhere. `opId` comes from `randomUUID()` in normal operation,
|
|
86
|
+
* but it also arrives from a persisted journal row, and `relPath` is read
|
|
87
|
+
* straight off disk — a row carrying `../../escaped` would otherwise let
|
|
88
|
+
* capture write outside the store and read-back read an arbitrary file.
|
|
89
|
+
*/
|
|
90
|
+
function assertSafeComponent(value: string, what: string): string {
|
|
91
|
+
if (value.length === 0 || value === "." || value === ".."
|
|
92
|
+
|| value.includes("/") || value.includes("\\") || value.includes("\0")) {
|
|
93
|
+
throw new Error(`unsafe ${what}: ${JSON.stringify(value)}`);
|
|
94
|
+
}
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Resolve `relative` under `root`, refusing anything that escapes it. */
|
|
99
|
+
function containedPath(root: string, ...segments: string[]): string {
|
|
100
|
+
const target = resolve(root, ...segments);
|
|
101
|
+
const base = resolve(root);
|
|
102
|
+
if (target !== base && !target.startsWith(base + sep)) {
|
|
103
|
+
throw new Error(`path escapes the integration store: ${target}`);
|
|
104
|
+
}
|
|
105
|
+
return target;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function snapshotDir(clientId: IntegrationClientId, dir: string): string {
|
|
109
|
+
return join(dir, "snapshots", clientId);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function captureSnapshot(
|
|
113
|
+
clientId: IntegrationClientId,
|
|
114
|
+
opId: string,
|
|
115
|
+
text: string | null,
|
|
116
|
+
dir: string = integrationsDir(),
|
|
117
|
+
): SnapshotRef {
|
|
118
|
+
if (text === null) return { kind: "none" };
|
|
119
|
+
const target = containedPath(dir, "snapshots", assertSafeComponent(clientId, "clientId"), assertSafeComponent(opId, "opId"));
|
|
120
|
+
ensureDir(target);
|
|
121
|
+
atomicWriteFile(target, text);
|
|
122
|
+
return { kind: "stored", relPath: join("snapshots", clientId, opId) };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Commit the row. Pruning is post-commit and can never fail the append. */
|
|
126
|
+
export function appendOperation(entry: JournalEntry, dir: string = integrationsDir()): void {
|
|
127
|
+
ensureDir(journalPath(dir));
|
|
128
|
+
appendFileSync(journalPath(dir), `${JSON.stringify(entry)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
129
|
+
try {
|
|
130
|
+
const pruned = pruneSnapshots(entry.clientId, dir);
|
|
131
|
+
if (pruned.ok) clearPruneFailure(entry.clientId, dir);
|
|
132
|
+
else markPruneFailure(entry.clientId, pruned.error, dir);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error(`[integrations] post-commit maintenance failed: ${String(error)}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Newest first. A torn final line (crash mid-append) is skipped, not thrown. */
|
|
139
|
+
export function listOperations(
|
|
140
|
+
clientId?: IntegrationClientId,
|
|
141
|
+
limit = 50,
|
|
142
|
+
dir: string = integrationsDir(),
|
|
143
|
+
): JournalEntry[] {
|
|
144
|
+
let raw: string;
|
|
145
|
+
try {
|
|
146
|
+
raw = readFileSync(journalPath(dir), "utf8");
|
|
147
|
+
} catch {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
const rows: JournalEntry[] = [];
|
|
151
|
+
for (const line of raw.split("\n")) {
|
|
152
|
+
if (!line.trim()) continue;
|
|
153
|
+
try {
|
|
154
|
+
const parsed = JSON.parse(line) as JournalEntry;
|
|
155
|
+
if (!clientId || parsed.clientId === clientId) rows.push(parsed);
|
|
156
|
+
} catch {
|
|
157
|
+
// Torn line from an interrupted append; the rest of the log is still good.
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return rows.reverse().slice(0, limit);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function findOperation(opId: string, dir: string = integrationsDir()): JournalEntry | null {
|
|
164
|
+
return listOperations(undefined, Number.MAX_SAFE_INTEGER, dir).find(row => row.opId === opId) ?? null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Resolves the tag against what is actually on disk now. */
|
|
168
|
+
export function readSnapshot(
|
|
169
|
+
entry: JournalEntry,
|
|
170
|
+
dir: string = integrationsDir(),
|
|
171
|
+
): { kind: "none" } | { kind: "stored"; text: string; path: string } | { kind: "expired" } {
|
|
172
|
+
if (entry.snapshot.kind === "none") return { kind: "none" };
|
|
173
|
+
if (entry.snapshot.kind === "expired") return { kind: "expired" };
|
|
174
|
+
// Derive the path from validated identifiers rather than trusting the
|
|
175
|
+
// persisted relPath, then verify containment either way.
|
|
176
|
+
const abs = containedPath(dir, entry.snapshot.relPath);
|
|
177
|
+
if (!existsSync(abs)) return { kind: "expired" };
|
|
178
|
+
return { kind: "stored", text: readFileSync(abs, "utf8"), path: abs };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Snapshot files retained right now — the witness for `retentionDegraded`.
|
|
183
|
+
*
|
|
184
|
+
* `null` means "cannot inspect", which is NOT the same as zero: reporting an
|
|
185
|
+
* unreadable snapshot directory as a healthy empty one would hide the
|
|
186
|
+
* unbounded, credential-bearing pile this field exists to disclose.
|
|
187
|
+
*/
|
|
188
|
+
export function countSnapshots(
|
|
189
|
+
clientId: IntegrationClientId,
|
|
190
|
+
dir: string = integrationsDir(),
|
|
191
|
+
): number | null {
|
|
192
|
+
try {
|
|
193
|
+
return readdirSync(snapshotDir(clientId, dir)).length;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
return (error as NodeJS.ErrnoException).code === "ENOENT" ? 0 : null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Keep the newest N snapshot files per client; journal rows always survive.
|
|
201
|
+
* Structured rather than throwing or swallowing: a swallowed failure would let
|
|
202
|
+
* credential-bearing snapshots pile up while every operation reported success.
|
|
203
|
+
*/
|
|
204
|
+
export function pruneSnapshots(
|
|
205
|
+
clientId: IntegrationClientId,
|
|
206
|
+
dir: string = integrationsDir(),
|
|
207
|
+
): { ok: true } | { ok: false; error: string } {
|
|
208
|
+
/*
|
|
209
|
+
* Filter to rows that HAVE a snapshot first, then take the newest N.
|
|
210
|
+
*
|
|
211
|
+
* Taking the newest N operations and filtering afterwards counted rows that
|
|
212
|
+
* never stored anything: an apply-to-absent records `snapshot: none`, so a
|
|
213
|
+
* client whose history alternates stored and none kept only half the backups
|
|
214
|
+
* the contract promises. The docs say ten backups per client, and this is
|
|
215
|
+
* the code that has to make that true.
|
|
216
|
+
*/
|
|
217
|
+
const keep = new Set(
|
|
218
|
+
listOperations(clientId, Number.MAX_SAFE_INTEGER, dir)
|
|
219
|
+
.filter(row => row.snapshot.kind === "stored")
|
|
220
|
+
.slice(0, SNAPSHOT_RETENTION)
|
|
221
|
+
.map(row => row.opId),
|
|
222
|
+
);
|
|
223
|
+
let names: string[];
|
|
224
|
+
try {
|
|
225
|
+
names = readdirSync(snapshotDir(clientId, dir));
|
|
226
|
+
} catch (error) {
|
|
227
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
228
|
+
if (code === "ENOENT") return { ok: true };
|
|
229
|
+
return { ok: false, error: String(error) };
|
|
230
|
+
}
|
|
231
|
+
for (const name of names) {
|
|
232
|
+
if (keep.has(name)) continue;
|
|
233
|
+
try {
|
|
234
|
+
rmSync(join(snapshotDir(clientId, dir), name), { force: true });
|
|
235
|
+
} catch (error) {
|
|
236
|
+
return { ok: false, error: String(error) };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return { ok: true };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface MaintenanceState {
|
|
243
|
+
pruneFailures: Partial<Record<IntegrationClientId, { at: string; error: string }>>;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function maintenancePath(dir: string): string {
|
|
247
|
+
return join(dir, "maintenance.json");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Validates rather than casts: `{}` parses fine and would leave `pruneFailures`
|
|
252
|
+
* undefined, so the next mark/clear would throw AFTER the journal row was
|
|
253
|
+
* committed — producing the phantom row the write ordering exists to prevent.
|
|
254
|
+
*/
|
|
255
|
+
export function readMaintenance(dir: string = integrationsDir()): MaintenanceState {
|
|
256
|
+
try {
|
|
257
|
+
const parsed: unknown = JSON.parse(readFileSync(maintenancePath(dir), "utf8"));
|
|
258
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
259
|
+
const raw = (parsed as { pruneFailures?: unknown }).pruneFailures;
|
|
260
|
+
const failures: MaintenanceState["pruneFailures"] = {};
|
|
261
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
262
|
+
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
263
|
+
if (!isIntegrationClientId(key)) continue;
|
|
264
|
+
if (!value || typeof value !== "object") continue;
|
|
265
|
+
const { at, error } = value as { at?: unknown; error?: unknown };
|
|
266
|
+
if (typeof at !== "string" || typeof error !== "string") continue;
|
|
267
|
+
failures[key] = { at, error };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return { pruneFailures: failures };
|
|
271
|
+
}
|
|
272
|
+
} catch {
|
|
273
|
+
// Absent or corrupt: no pending retries known.
|
|
274
|
+
}
|
|
275
|
+
return { pruneFailures: {} };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function writeMaintenance(state: MaintenanceState, dir: string): void {
|
|
279
|
+
try {
|
|
280
|
+
ensureDir(maintenancePath(dir));
|
|
281
|
+
atomicWriteFile(maintenancePath(dir), `${JSON.stringify(state, null, 2)}\n`);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
// The marker is an optimization for scheduling retries. `retentionDegraded`
|
|
284
|
+
// is derived from the snapshot count, so losing it costs a retry, not the
|
|
285
|
+
// claim — a durable promise must not depend on a write that can fail.
|
|
286
|
+
console.error(`[integrations] could not record maintenance state: ${String(error)}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function markPruneFailure(
|
|
291
|
+
clientId: IntegrationClientId,
|
|
292
|
+
error: string,
|
|
293
|
+
dir: string = integrationsDir(),
|
|
294
|
+
): void {
|
|
295
|
+
const state = readMaintenance(dir);
|
|
296
|
+
state.pruneFailures[clientId] = { at: new Date().toISOString(), error };
|
|
297
|
+
writeMaintenance(state, dir);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function clearPruneFailure(
|
|
301
|
+
clientId: IntegrationClientId,
|
|
302
|
+
dir: string = integrationsDir(),
|
|
303
|
+
): void {
|
|
304
|
+
const state = readMaintenance(dir);
|
|
305
|
+
if (!(clientId in state.pruneFailures)) return;
|
|
306
|
+
delete state.pruneFailures[clientId];
|
|
307
|
+
writeMaintenance(state, dir);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Re-attempt every marked client. */
|
|
311
|
+
export function retryPendingPrunes(dir: string = integrationsDir()): void {
|
|
312
|
+
for (const clientId of Object.keys(readMaintenance(dir).pruneFailures) as IntegrationClientId[]) {
|
|
313
|
+
if (pruneSnapshots(clientId, dir).ok) clearPruneFailure(clientId, dir);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Additive merge and surgical removal of the fragments Remodex owns.
|
|
3
|
+
*
|
|
4
|
+
* The rule that shapes this file: we insert exactly the paths our builder
|
|
5
|
+
* named, and we delete exactly the paths a record says we wrote. Nothing here
|
|
6
|
+
* ever scans for a prefix — a user's own `opencodex/...` entry is not ours to
|
|
7
|
+
* remove, and inferring ownership from a name is how a config editor destroys
|
|
8
|
+
* work it did not create.
|
|
9
|
+
*
|
|
10
|
+
* Design of record: devlog/_fin/260802_client_toggle_api/031_wp3_writer_impl.md.
|
|
11
|
+
*/
|
|
12
|
+
import type { ManagedContribution } from "../clients/config-export";
|
|
13
|
+
|
|
14
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
15
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Structured clone via JSON: our documents are plain data by construction. */
|
|
19
|
+
function clone<T>(value: T): T {
|
|
20
|
+
return value === undefined ? value : (JSON.parse(JSON.stringify(value)) as T);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Write `value` at `path`, creating intermediate objects. Returns a new document. */
|
|
24
|
+
export function setPath(doc: unknown, path: readonly string[], value: unknown): unknown {
|
|
25
|
+
if (path.length === 0) throw new Error("setPath needs a non-empty path");
|
|
26
|
+
const root: Record<string, unknown> = isPlainRecord(doc) ? clone(doc) : {};
|
|
27
|
+
let cursor = root;
|
|
28
|
+
for (const key of path.slice(0, -1)) {
|
|
29
|
+
const next = cursor[key];
|
|
30
|
+
if (!isPlainRecord(next)) cursor[key] = {};
|
|
31
|
+
cursor = cursor[key] as Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
cursor[path[path.length - 1]!] = clone(value);
|
|
34
|
+
return root;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Delete `path`, optionally pruning containers that WE created.
|
|
39
|
+
*
|
|
40
|
+
* Two properties have to hold at once, and they pull in opposite directions.
|
|
41
|
+
* A user who wrote `providers: {}` before we existed still has it after a
|
|
42
|
+
* disable — pruning it because our entry left it empty would delete their
|
|
43
|
+
* line. But a container we conjured ourselves (Kimi's `models` map exists only
|
|
44
|
+
* because our aliases need somewhere to live) must not survive as empty
|
|
45
|
+
* residue in a file the user never asked us to restructure.
|
|
46
|
+
*
|
|
47
|
+
* `createdContainers` is the difference: paths recorded at apply time as
|
|
48
|
+
* absent beforehand. Anything not in that set is treated as the user's.
|
|
49
|
+
*/
|
|
50
|
+
export function deletePath(
|
|
51
|
+
doc: unknown,
|
|
52
|
+
path: readonly string[],
|
|
53
|
+
createdContainers: ReadonlySet<string> = new Set(),
|
|
54
|
+
): { doc: unknown; removed: boolean } {
|
|
55
|
+
if (!isPlainRecord(doc) || path.length === 0) return { doc, removed: false };
|
|
56
|
+
const root = clone(doc) as Record<string, unknown>;
|
|
57
|
+
const chain: Record<string, unknown>[] = [root];
|
|
58
|
+
let cursor: Record<string, unknown> = root;
|
|
59
|
+
for (const key of path.slice(0, -1)) {
|
|
60
|
+
const next = cursor[key];
|
|
61
|
+
if (!isPlainRecord(next)) return { doc: root, removed: false };
|
|
62
|
+
cursor = next;
|
|
63
|
+
chain.push(cursor);
|
|
64
|
+
}
|
|
65
|
+
const leaf = path[path.length - 1]!;
|
|
66
|
+
if (!(leaf in cursor)) return { doc: root, removed: false };
|
|
67
|
+
delete cursor[leaf];
|
|
68
|
+
/*
|
|
69
|
+
* Walk back up, pruning only containers this deletion emptied AND that we
|
|
70
|
+
* created. The root is never pruned.
|
|
71
|
+
*/
|
|
72
|
+
for (let index = chain.length - 1; index >= 1; index -= 1) {
|
|
73
|
+
const container = chain[index]!;
|
|
74
|
+
if (Object.keys(container).length > 0) break;
|
|
75
|
+
const containerPath = path.slice(0, index).join("\u0000");
|
|
76
|
+
if (!createdContainers.has(containerPath)) break;
|
|
77
|
+
delete chain[index - 1]![path[index - 1]!];
|
|
78
|
+
}
|
|
79
|
+
return { doc: root, removed: true };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Insert every fragment. Everything else in the document is preserved. */
|
|
83
|
+
export function mergeContribution(doc: unknown, contribution: ManagedContribution): unknown {
|
|
84
|
+
let next = doc;
|
|
85
|
+
for (const fragment of contribution.fragments) next = setPath(next, fragment.path, fragment.value);
|
|
86
|
+
return next;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Remove exactly the RECORDED paths — never a prefix scan. An entry that
|
|
91
|
+
* matches our naming but has no recorded path is not ours: it reads as
|
|
92
|
+
* `conflict` and survives untouched.
|
|
93
|
+
*/
|
|
94
|
+
export function removeFragments(
|
|
95
|
+
doc: unknown,
|
|
96
|
+
paths: readonly (readonly string[])[],
|
|
97
|
+
createdContainers: ReadonlySet<string> = new Set(),
|
|
98
|
+
): { doc: unknown; removed: boolean } {
|
|
99
|
+
let next = doc;
|
|
100
|
+
let removed = false;
|
|
101
|
+
for (const path of paths) {
|
|
102
|
+
const result = deletePath(next, path, createdContainers);
|
|
103
|
+
next = result.doc;
|
|
104
|
+
removed = removed || result.removed;
|
|
105
|
+
}
|
|
106
|
+
return { doc: next, removed };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Container paths a contribution would have to CREATE in `doc`.
|
|
111
|
+
*
|
|
112
|
+
* Computed before the merge, because afterwards every container exists and the
|
|
113
|
+
* question is unanswerable. Recorded on the ownership record so a later
|
|
114
|
+
* disable can tell its own scaffolding from the user's structure.
|
|
115
|
+
*/
|
|
116
|
+
export function createdContainerPaths(
|
|
117
|
+
doc: unknown,
|
|
118
|
+
contribution: ManagedContribution,
|
|
119
|
+
): string[] {
|
|
120
|
+
const created = new Set<string>();
|
|
121
|
+
for (const fragment of contribution.fragments) {
|
|
122
|
+
let cursor: unknown = doc;
|
|
123
|
+
for (let depth = 0; depth < fragment.path.length - 1; depth += 1) {
|
|
124
|
+
const key = fragment.path[depth]!;
|
|
125
|
+
const next = isPlainRecord(cursor) ? cursor[key] : undefined;
|
|
126
|
+
if (!isPlainRecord(next)) {
|
|
127
|
+
created.add(fragment.path.slice(0, depth + 1).join("\u0000"));
|
|
128
|
+
cursor = undefined;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
cursor = next;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return [...created];
|
|
135
|
+
}
|