@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,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The service-home preflight for native-client teardown (WP2, audit r1 #5;
|
|
3
|
+
* devlog 260803_integrations_toggle_all/012).
|
|
4
|
+
*
|
|
5
|
+
* `rmx stop` has honored `ServiceOwnershipError` since it started catching it
|
|
6
|
+
* (src/cli/index.ts:464); nothing on the HTTP side ever did. A route that calls
|
|
7
|
+
* `stripGrokConfig` directly would otherwise pull the fence out from under a
|
|
8
|
+
* service running from another CODEX_HOME/OPENCODEX_HOME — the installed
|
|
9
|
+
* service is still live and the shared state belongs to it.
|
|
10
|
+
*
|
|
11
|
+
* ENABLE is not gated by this: writing our own fence is not a shared teardown,
|
|
12
|
+
* and `injectGrokConfig` already runs unguarded from `rmx start`/`ensure`.
|
|
13
|
+
*/
|
|
14
|
+
import {
|
|
15
|
+
assertServiceEnvironmentMatchesInstall,
|
|
16
|
+
isServiceOwnershipError,
|
|
17
|
+
} from "../../service";
|
|
18
|
+
import {
|
|
19
|
+
currentServiceHomes,
|
|
20
|
+
inspectServiceStateEvidence,
|
|
21
|
+
serviceHomeMatches,
|
|
22
|
+
type ServiceStateEvidence,
|
|
23
|
+
} from "../../service";
|
|
24
|
+
import {
|
|
25
|
+
inspectServiceManagerInstallation,
|
|
26
|
+
type ProbeDeps,
|
|
27
|
+
type ServiceManagerClaim,
|
|
28
|
+
} from "../../service-manager-probe";
|
|
29
|
+
|
|
30
|
+
export type NativeTeardownOwnership = { ok: true } | { ok: false; message: string };
|
|
31
|
+
|
|
32
|
+
export function assertNativeTeardownOwned(): NativeTeardownOwnership {
|
|
33
|
+
try {
|
|
34
|
+
assertServiceEnvironmentMatchesInstall();
|
|
35
|
+
return { ok: true };
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (isServiceOwnershipError(error)) {
|
|
38
|
+
// The message names both the recorded and the current home — that is the
|
|
39
|
+
// refusal text, verbatim, because the user has to act on it.
|
|
40
|
+
return { ok: false, message: error.message };
|
|
41
|
+
}
|
|
42
|
+
// Unrelated failure (corrupt state file, IO): mirror
|
|
43
|
+
// `serviceEnvironmentOwnedHere` and fail open rather than wedging the route
|
|
44
|
+
// behind a check whose own input is broken.
|
|
45
|
+
return { ok: true };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Tri-state ownership for UNATTENDED writes.
|
|
51
|
+
*
|
|
52
|
+
* Deliberately not `assertNativeTeardownOwned`. That one fails OPEN — a corrupt
|
|
53
|
+
* state file yields `{ok:true}` — which is right for a teardown route a human
|
|
54
|
+
* just invoked, and catastrophic as an authority for automatic convergence:
|
|
55
|
+
* "could not read" would become "belongs to me".
|
|
56
|
+
*
|
|
57
|
+
* `owned` here means NO PERSISTENT SERVICE CLAIM WAS OBSERVED. It does not mean
|
|
58
|
+
* this process is the only writer. Two foreground `rmx start` processes on one
|
|
59
|
+
* home both read `owned`, and correctly so, because neither installs a service —
|
|
60
|
+
* keeping them apart is the write lock's job, not this function's.
|
|
61
|
+
*/
|
|
62
|
+
export type NativeCodexOwnership = "owned" | "foreign" | "unknown";
|
|
63
|
+
|
|
64
|
+
export interface OwnershipInspection {
|
|
65
|
+
readonly ownership: NativeCodexOwnership;
|
|
66
|
+
/** Why, in the words a refusal message can use. */
|
|
67
|
+
readonly reason: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function claimNamesDifferentHome(
|
|
71
|
+
claim: ServiceManagerClaim,
|
|
72
|
+
current: { codexHome: string; opencodexHome: string },
|
|
73
|
+
): boolean {
|
|
74
|
+
// A definition that OMITS a home is not a definition that disagrees about it:
|
|
75
|
+
// an install run without CODEX_HOME set writes no such key at all.
|
|
76
|
+
if (claim.homes.codexHome !== null && !serviceHomeMatches(claim.homes.codexHome, current.codexHome)) return true;
|
|
77
|
+
if (claim.homes.opencodexHome !== null && !serviceHomeMatches(claim.homes.opencodexHome, current.opencodexHome)) return true;
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Map a service-manager claim backend to the `ServiceInstallState.backend`
|
|
83
|
+
* value it corresponds to. `scheduler` (Task Scheduler) and `winsw` (native)
|
|
84
|
+
* are the two Windows manager backends; launchd/systemd claims have no Windows
|
|
85
|
+
* backend and can never mismatch a v2 state file.
|
|
86
|
+
*/
|
|
87
|
+
function claimBackendToStateBackend(backend: ServiceManagerClaim["backend"]): "scheduler" | "native" | null {
|
|
88
|
+
if (backend === "scheduler") return "scheduler";
|
|
89
|
+
if (backend === "winsw") return "native";
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** True when the recorded state backend disagrees with the manager claim. Legacy v1 means scheduler. */
|
|
94
|
+
function claimBackendMismatchesState(claim: ServiceManagerClaim, state: { backend?: "scheduler" | "native" }): boolean {
|
|
95
|
+
const expected = claimBackendToStateBackend(claim.backend);
|
|
96
|
+
if (expected === null) return false;
|
|
97
|
+
return (state.backend ?? "scheduler") !== expected;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface OwnershipDeps extends ProbeDeps {
|
|
101
|
+
/**
|
|
102
|
+
* Which state paths to consult. Injectable because the default set includes
|
|
103
|
+
* the DEFAULT home mirror, resolved from `homedir()` — which no test sandbox
|
|
104
|
+
* moves. Without this a fixture reads the developer's real installation and
|
|
105
|
+
* calls their own machine foreign.
|
|
106
|
+
*/
|
|
107
|
+
readonly statePaths?: readonly string[];
|
|
108
|
+
readonly currentHomes?: { codexHome: string; opencodexHome: string };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function inspectNativeCodexOwnership(deps: OwnershipDeps = {}): OwnershipInspection {
|
|
112
|
+
const current = deps.currentHomes ?? currentServiceHomes();
|
|
113
|
+
const evidence = deps.statePaths
|
|
114
|
+
? inspectServiceStateEvidence(deps.statePaths)
|
|
115
|
+
: inspectServiceStateEvidence();
|
|
116
|
+
|
|
117
|
+
const unreadable = evidence.find((e): e is Extract<ServiceStateEvidence, { kind: "unreadable" }> => e.kind === "unreadable");
|
|
118
|
+
if (unreadable) {
|
|
119
|
+
return { ownership: "unknown", reason: `service state at ${unreadable.path} could not be read (${unreadable.reason})` };
|
|
120
|
+
}
|
|
121
|
+
const invalid = evidence.find(e => e.kind === "invalid");
|
|
122
|
+
if (invalid) {
|
|
123
|
+
return { ownership: "unknown", reason: `service state at ${invalid.path} is malformed` };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const valid = evidence.filter((e): e is Extract<ServiceStateEvidence, { kind: "valid" }> => e.kind === "valid");
|
|
127
|
+
// Mirrors that disagree with each other are not a majority vote.
|
|
128
|
+
for (const one of valid) {
|
|
129
|
+
for (const other of valid) {
|
|
130
|
+
if (!serviceHomeMatches(one.state.codexHome, other.state.codexHome)
|
|
131
|
+
|| !serviceHomeMatches(one.state.opencodexHome, other.state.opencodexHome)) {
|
|
132
|
+
return { ownership: "unknown", reason: "two service state files disagree about which homes are installed" };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const foreign = valid.find(e =>
|
|
138
|
+
!serviceHomeMatches(e.state.codexHome, current.codexHome)
|
|
139
|
+
|| !serviceHomeMatches(e.state.opencodexHome, current.opencodexHome));
|
|
140
|
+
if (foreign) {
|
|
141
|
+
return {
|
|
142
|
+
ownership: "foreign",
|
|
143
|
+
reason: `a service is installed for CODEX_HOME=${foreign.state.codexHome} / OPENCODEX_HOME=${foreign.state.opencodexHome}`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// The manager assets live under the effective OPENCODEX_HOME. Production
|
|
148
|
+
// callers do not inject ProbeDeps.configDir, so derive it from the same
|
|
149
|
+
// current-home snapshot used for ownership comparison rather than silently
|
|
150
|
+
// falling back to <homedir>/.opencodex.
|
|
151
|
+
const manager = inspectServiceManagerInstallation({
|
|
152
|
+
...deps,
|
|
153
|
+
configDir: deps.configDir ?? current.opencodexHome,
|
|
154
|
+
});
|
|
155
|
+
if (manager.kind === "unknown") {
|
|
156
|
+
return { ownership: "unknown", reason: manager.reason };
|
|
157
|
+
}
|
|
158
|
+
if (manager.kind === "conflict") {
|
|
159
|
+
return { ownership: "unknown", reason: "more than one service manager holds a registration for this proxy" };
|
|
160
|
+
}
|
|
161
|
+
if (manager.kind === "present") {
|
|
162
|
+
const disagreeing = manager.claims.find(claim => claimNamesDifferentHome(claim, current));
|
|
163
|
+
if (disagreeing) {
|
|
164
|
+
/*
|
|
165
|
+
* The state file says this home and the definition says another. An
|
|
166
|
+
* interrupted reinstall looks exactly like this — installation writes the
|
|
167
|
+
* definition BEFORE the state file — and picking a winner unattended would
|
|
168
|
+
* be guessing which half of a half-finished operation to believe.
|
|
169
|
+
*/
|
|
170
|
+
return {
|
|
171
|
+
ownership: "unknown",
|
|
172
|
+
reason: `${disagreeing.backend} is installed from ${disagreeing.definitionPath}, which names different homes than the recorded service state`,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
// A manager backend that disagrees with the recorded state (e.g. state says
|
|
176
|
+
// native/WinSW but a scheduler task is found) is an interrupted backend
|
|
177
|
+
// switch: it does not prove which manager owns the installation. v1 state
|
|
178
|
+
// predates the field and is scheduler by contract.
|
|
179
|
+
const stateBackendMismatch = valid.find(state => manager.claims.some(claim => claimBackendMismatchesState(claim, state.state)));
|
|
180
|
+
if (stateBackendMismatch) {
|
|
181
|
+
return {
|
|
182
|
+
ownership: "unknown",
|
|
183
|
+
reason: `the service state records backend ${stateBackendMismatch.state.backend ?? "scheduler"} but ${manager.claims[0]?.backend ?? "a service manager"} is installed`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
// Definition agrees. Valid state agreeing with it is ownership; no state at
|
|
187
|
+
// all beside an installed definition is not, because the definition is the
|
|
188
|
+
// claim and nothing here recorded making it.
|
|
189
|
+
if (valid.length === 0) {
|
|
190
|
+
return {
|
|
191
|
+
ownership: "unknown",
|
|
192
|
+
reason: `${manager.claims[0]?.backend ?? "a service manager"} holds a registration that no service state file accounts for`,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return { ownership: "owned", reason: "the installed service names these homes" };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// manager.kind === "absent"
|
|
199
|
+
return valid.length === 0
|
|
200
|
+
? { ownership: "owned", reason: "no service state and no service manager claim" }
|
|
201
|
+
: { ownership: "owned", reason: "the recorded service state names these homes" };
|
|
202
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Remodex remembers about a client between operations.
|
|
3
|
+
*
|
|
4
|
+
* Two hashes, because the two questions are genuinely independent: the FILE
|
|
5
|
+
* hash answers "did anyone touch this after us", and the BLOCK hash answers "is
|
|
6
|
+
* our content still what we would write today". One hash cannot do both, and
|
|
7
|
+
* conflating them is what lets a foreign edit read as ordinary drift — which
|
|
8
|
+
* would then be silently overwritten.
|
|
9
|
+
*
|
|
10
|
+
* Design of record: devlog/_fin/260802_client_toggle_api/021 §2.
|
|
11
|
+
*/
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import { mkdirSync, readFileSync } from "node:fs";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import type { ManagedContribution, ManagedFragment } from "../clients/config-export";
|
|
16
|
+
import { atomicWriteFile, getConfigDir } from "../config";
|
|
17
|
+
import type { IntegrationClientId } from "./registry";
|
|
18
|
+
|
|
19
|
+
/** 16 hex chars — the same shape as the Claude Desktop applied fingerprint. */
|
|
20
|
+
export function fingerprint(text: string): string {
|
|
21
|
+
return createHash("sha256").update(text).digest("hex").slice(0, 16);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Canonical bytes of a contribution. Fragments are sorted by path so two builds
|
|
26
|
+
* of the same contribution hash identically regardless of emission order.
|
|
27
|
+
*/
|
|
28
|
+
export function canonicalContribution(contribution: ManagedContribution): string {
|
|
29
|
+
const sorted = [...contribution.fragments].sort((a, b) => {
|
|
30
|
+
const left = a.path.join("\u0000");
|
|
31
|
+
const right = b.path.join("\u0000");
|
|
32
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
33
|
+
});
|
|
34
|
+
return JSON.stringify(sorted.map(fragment => [fragment.path, fragment.value]));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface OwnershipRecord {
|
|
38
|
+
clientId: IntegrationClientId;
|
|
39
|
+
configPath: string;
|
|
40
|
+
/** Hash of the WHOLE file as we left it — detects foreign edits after us. */
|
|
41
|
+
fileFingerprint: string;
|
|
42
|
+
/** Hash of our contribution — detects catalog/port drift. */
|
|
43
|
+
blockFingerprint: string;
|
|
44
|
+
/** The exact paths we own. Removal touches these and nothing else. */
|
|
45
|
+
fragmentPaths: readonly (readonly string[])[];
|
|
46
|
+
/**
|
|
47
|
+
* Containers this apply had to CREATE, `\0`-joined.
|
|
48
|
+
*
|
|
49
|
+
* Kimi's `models` map exists only because our aliases need somewhere to
|
|
50
|
+
* live. Without this, disable left it behind as an empty container in a file
|
|
51
|
+
* the user never asked us to restructure — while a `providers: {}` the user
|
|
52
|
+
* wrote themselves must survive. Optional because records written before
|
|
53
|
+
* this field existed simply prune nothing, which is the old behavior.
|
|
54
|
+
*/
|
|
55
|
+
createdContainers?: readonly string[];
|
|
56
|
+
appliedAt: string;
|
|
57
|
+
opId: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The integrations directory itself. Every primitive takes THIS path, never a
|
|
62
|
+
* config root, so a caller cannot accidentally produce
|
|
63
|
+
* `<root>/integrations/integrations` by passing an already-resolved value.
|
|
64
|
+
*/
|
|
65
|
+
export function integrationsDir(configDir: string = getConfigDir()): string {
|
|
66
|
+
return join(configDir, "integrations");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** `atomicWriteFile` does not create parents. */
|
|
70
|
+
export function ensureDir(filePath: string): void {
|
|
71
|
+
mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function recordsPath(dir: string): string {
|
|
75
|
+
return join(dir, "records.json");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function readRecords(
|
|
79
|
+
dir: string = integrationsDir(),
|
|
80
|
+
): Partial<Record<IntegrationClientId, OwnershipRecord>> {
|
|
81
|
+
try {
|
|
82
|
+
const parsed: unknown = JSON.parse(readFileSync(recordsPath(dir), "utf8"));
|
|
83
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
84
|
+
return parsed as Partial<Record<IntegrationClientId, OwnershipRecord>>;
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// Missing or corrupt records mean "we remember nothing", which the
|
|
88
|
+
// classifier reads as a conflict for an existing block. Fail closed: an
|
|
89
|
+
// unreadable memory is never permission to delete.
|
|
90
|
+
}
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function writeRecord(record: OwnershipRecord, dir: string = integrationsDir()): void {
|
|
95
|
+
const all = readRecords(dir);
|
|
96
|
+
all[record.clientId] = record;
|
|
97
|
+
ensureDir(recordsPath(dir));
|
|
98
|
+
atomicWriteFile(recordsPath(dir), `${JSON.stringify(all, null, 2)}\n`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function deleteRecord(clientId: IntegrationClientId, dir: string = integrationsDir()): void {
|
|
102
|
+
const all = readRecords(dir);
|
|
103
|
+
if (!(clientId in all)) return;
|
|
104
|
+
delete all[clientId];
|
|
105
|
+
ensureDir(recordsPath(dir));
|
|
106
|
+
atomicWriteFile(recordsPath(dir), `${JSON.stringify(all, null, 2)}\n`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function fragmentPathsOf(contribution: ManagedContribution): readonly (readonly string[])[] {
|
|
110
|
+
return contribution.fragments.map((fragment: ManagedFragment) => fragment.path);
|
|
111
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where each file-toggle client keeps its config, and what we are allowed to do
|
|
3
|
+
* to it.
|
|
4
|
+
*
|
|
5
|
+
* The export registry (src/clients/config-export.ts) says how to RENDER a
|
|
6
|
+
* client's config. This one says where it lives, how to tell whether the client
|
|
7
|
+
* is installed at all, and whether a remote bind is safe for it.
|
|
8
|
+
*
|
|
9
|
+
* Design of record: devlog/_fin/260802_client_toggle_api/021 §1.
|
|
10
|
+
*/
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import {
|
|
14
|
+
EXPORT_CLIENTS,
|
|
15
|
+
gajaeConfigPath,
|
|
16
|
+
gajaeHomeDir,
|
|
17
|
+
hermesConfigPath,
|
|
18
|
+
hermesHomeDir,
|
|
19
|
+
kimiConfigPath,
|
|
20
|
+
kimiHomeDir,
|
|
21
|
+
ompAgentDir,
|
|
22
|
+
ompModelsConfigPath,
|
|
23
|
+
opencodeGlobalConfigPath,
|
|
24
|
+
openclawConfigPath,
|
|
25
|
+
openclawHomeDir,
|
|
26
|
+
type ExportClientId,
|
|
27
|
+
} from "../clients/config-export";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Readability alias. WP1 owns the type; this never introduces a second one, so
|
|
31
|
+
* the dependency only ever points backwards.
|
|
32
|
+
*/
|
|
33
|
+
export type IntegrationClientId = ExportClientId;
|
|
34
|
+
|
|
35
|
+
export interface IntegrationClientSpec {
|
|
36
|
+
id: IntegrationClientId;
|
|
37
|
+
/** The client's config file, honoring that client's own environment override. */
|
|
38
|
+
configPath: (env?: NodeJS.ProcessEnv, home?: string) => string;
|
|
39
|
+
/** Directory whose existence is the cheap "is it installed?" signal. */
|
|
40
|
+
detectDir: (env?: NodeJS.ProcessEnv, home?: string) => string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* True when the client has nowhere to put the dedicated admission header a
|
|
45
|
+
* non-loopback bind requires, so a generated config would simply be rejected.
|
|
46
|
+
*
|
|
47
|
+
* Read from the export registry rather than restated here: two lists of the
|
|
48
|
+
* same fact drift, and this one decides whether we write a file that 401s.
|
|
49
|
+
*/
|
|
50
|
+
export function isLoopbackOnly(clientId: IntegrationClientId): boolean {
|
|
51
|
+
return EXPORT_CLIENTS[clientId].loopbackOnly;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function xdgConfigHome(env: NodeJS.ProcessEnv, home: string): string {
|
|
55
|
+
const xdg = env.XDG_CONFIG_HOME;
|
|
56
|
+
return xdg && xdg.length > 0 ? xdg : join(home, ".config");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const INTEGRATION_CLIENTS: Record<IntegrationClientId, IntegrationClientSpec> = {
|
|
60
|
+
opencode: {
|
|
61
|
+
id: "opencode",
|
|
62
|
+
// These take `home` explicitly. The export registry's `destination` reads
|
|
63
|
+
// the real home directory, which is right for telling a user where their
|
|
64
|
+
// file lives and wrong for a writer that a test must be able to redirect.
|
|
65
|
+
configPath: (env = process.env, home = homedir()) => opencodeGlobalConfigPath(env, home),
|
|
66
|
+
detectDir: (env = process.env, home = homedir()) => join(xdgConfigHome(env, home), "opencode"),
|
|
67
|
+
},
|
|
68
|
+
pi: {
|
|
69
|
+
id: "pi",
|
|
70
|
+
configPath: (_env = process.env, home = homedir()) => join(home, ".pi", "agent", "models.json"),
|
|
71
|
+
detectDir: (_env = process.env, home = homedir()) => join(home, ".pi"),
|
|
72
|
+
},
|
|
73
|
+
omp: {
|
|
74
|
+
id: "omp",
|
|
75
|
+
configPath: (env = process.env, home = homedir()) => ompModelsConfigPath(env, home),
|
|
76
|
+
detectDir: (env = process.env, home = homedir()) => ompAgentDir(env, home),
|
|
77
|
+
},
|
|
78
|
+
hermes: {
|
|
79
|
+
id: "hermes",
|
|
80
|
+
configPath: (env = process.env, home = homedir()) => hermesConfigPath(env, home),
|
|
81
|
+
detectDir: (env = process.env, home = homedir()) => hermesHomeDir(env, home),
|
|
82
|
+
},
|
|
83
|
+
openclaw: {
|
|
84
|
+
id: "openclaw",
|
|
85
|
+
configPath: (env = process.env, home = homedir()) => openclawConfigPath(env, home),
|
|
86
|
+
detectDir: (env = process.env, home = homedir()) => openclawHomeDir(env, home),
|
|
87
|
+
},
|
|
88
|
+
kimi: {
|
|
89
|
+
id: "kimi",
|
|
90
|
+
configPath: (env = process.env, home = homedir()) => kimiConfigPath(env, home),
|
|
91
|
+
detectDir: (env = process.env, home = homedir()) => kimiHomeDir(env, home),
|
|
92
|
+
// Kimi reads credentials only from config.toml — it never consults the
|
|
93
|
+
// environment — so there is no way to point it at a remote bind without
|
|
94
|
+
// serializing the user's key.
|
|
95
|
+
},
|
|
96
|
+
gajae: {
|
|
97
|
+
id: "gajae",
|
|
98
|
+
configPath: (env = process.env, home = homedir()) => gajaeConfigPath(env, home),
|
|
99
|
+
detectDir: (env = process.env, home = homedir()) => gajaeHomeDir(env, home),
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const INTEGRATION_CLIENT_IDS: readonly IntegrationClientId[] =
|
|
104
|
+
Object.keys(INTEGRATION_CLIENTS) as IntegrationClientId[];
|
|
105
|
+
|
|
106
|
+
export function isIntegrationClientId(value: string): value is IntegrationClientId {
|
|
107
|
+
return Object.prototype.hasOwnProperty.call(INTEGRATION_CLIENTS, value);
|
|
108
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text serializers for client config documents.
|
|
3
|
+
*
|
|
4
|
+
* Bun 1.3.14 gives us JSON and JSON5 stringify natively, but the two formats a
|
|
5
|
+
* client config actually needs beyond JSON are hand-rendered here:
|
|
6
|
+
*
|
|
7
|
+
* - **TOML** has no `Bun.TOML.stringify` at all.
|
|
8
|
+
* - **YAML** has `Bun.YAML.stringify`, but it emits flow style with no trailing
|
|
9
|
+
* newline (`{a: 1,b: {c: d}}`). That is valid YAML and completely wrong for a
|
|
10
|
+
* file a user opens, so we render block style ourselves.
|
|
11
|
+
*
|
|
12
|
+
* The renderers originally covered only the shallow shapes our builders emit.
|
|
13
|
+
* That was fine while they only ever saw generated documents — but the
|
|
14
|
+
* integration writer parses the USER's whole config and re-serializes it, so a
|
|
15
|
+
* legitimate `null` or a numeric array in a file we never wrote threw straight
|
|
16
|
+
* out of the writer and surfaced as a 500. They now cover the value domains
|
|
17
|
+
* their formats actually allow.
|
|
18
|
+
*
|
|
19
|
+
* What they still refuse is genuinely unrenderable: a function, a symbol, a
|
|
20
|
+
* non-finite number. Those must fail loudly rather than produce bytes a client
|
|
21
|
+
* might misread — and the writer turns the failure into a structured refusal
|
|
22
|
+
* rather than an exception.
|
|
23
|
+
*
|
|
24
|
+
* Design of record: devlog/_fin/260802_client_toggle_api/011_wp1_builders.md.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export type ConfigFormat = "json" | "yaml" | "toml" | "json5";
|
|
28
|
+
|
|
29
|
+
/** Thrown when a document holds a value its format cannot represent. */
|
|
30
|
+
export class UnserializableValueError extends Error {}
|
|
31
|
+
|
|
32
|
+
export const FORMAT_MEDIA_TYPE: Record<ConfigFormat, string> = {
|
|
33
|
+
json: "application/json",
|
|
34
|
+
yaml: "application/yaml",
|
|
35
|
+
toml: "application/toml",
|
|
36
|
+
json5: "application/json5",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Type guard, not a boolean check — the renderers walk `unknown`, so without
|
|
41
|
+
* the predicate every narrowed branch stays `unknown`.
|
|
42
|
+
*/
|
|
43
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
44
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isYamlScalar(value: unknown): value is string | number | boolean | null {
|
|
48
|
+
return value === null
|
|
49
|
+
|| typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Keep only unambiguous, single-token strings plain. Everything else uses a
|
|
54
|
+
* JSON-compatible double-quoted scalar, which gives deterministic escaping —
|
|
55
|
+
* JSON string escaping is valid YAML 1.2 string escaping.
|
|
56
|
+
*/
|
|
57
|
+
function yamlString(value: string): string {
|
|
58
|
+
const plainSafe = value.length > 0
|
|
59
|
+
&& value.trim() === value
|
|
60
|
+
&& /^[A-Za-z_./][A-Za-z0-9_./-]*$/u.test(value)
|
|
61
|
+
&& !/^(?:null|true|false|yes|no|on|off|~|\.nan|[-+]?\.inf)$/iu.test(value);
|
|
62
|
+
return plainSafe ? value : JSON.stringify(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function yamlScalar(value: string | number | boolean | null): string {
|
|
66
|
+
// `null` renders as the explicit `null` token rather than an empty value:
|
|
67
|
+
// a bare `key:` is also null in YAML, but round-tripping it through other
|
|
68
|
+
// parsers is less predictable than saying so.
|
|
69
|
+
if (value === null) return "null";
|
|
70
|
+
if (typeof value === "string") return yamlString(value);
|
|
71
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
72
|
+
if (Number.isFinite(value)) return String(value);
|
|
73
|
+
throw new UnserializableValueError(`YAML cannot represent the number ${String(value)}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function yamlEmptyCollection(value: unknown): "[]" | "{}" | undefined {
|
|
77
|
+
if (Array.isArray(value) && value.length === 0) return "[]";
|
|
78
|
+
if (isPlainRecord(value) && Object.keys(value).length === 0) return "{}";
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function yamlMapEntryLines(key: string, value: unknown, indent: number): string[] {
|
|
83
|
+
const padding = " ".repeat(indent);
|
|
84
|
+
const renderedKey = yamlString(key);
|
|
85
|
+
if (isYamlScalar(value)) return [`${padding}${renderedKey}: ${yamlScalar(value)}`];
|
|
86
|
+
const empty = yamlEmptyCollection(value);
|
|
87
|
+
if (empty !== undefined) return [`${padding}${renderedKey}: ${empty}`];
|
|
88
|
+
if (Array.isArray(value) || isPlainRecord(value)) {
|
|
89
|
+
return [`${padding}${renderedKey}:`, ...yamlLines(value, indent + 2)];
|
|
90
|
+
}
|
|
91
|
+
throw new UnserializableValueError(`YAML cannot represent the value at key ${JSON.stringify(key)}: ${describeValue(value)}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A map inside a sequence: the first key rides the dash, the rest indent under it. */
|
|
95
|
+
function yamlArrayMapLines(value: Record<string, unknown>, indent: number): string[] {
|
|
96
|
+
const entries = Object.entries(value);
|
|
97
|
+
const padding = " ".repeat(indent);
|
|
98
|
+
if (entries.length === 0) return [`${padding}- {}`];
|
|
99
|
+
|
|
100
|
+
const [first, ...rest] = entries;
|
|
101
|
+
const [firstKey, firstValue] = first!;
|
|
102
|
+
const renderedFirstKey = yamlString(firstKey);
|
|
103
|
+
const firstEmpty = yamlEmptyCollection(firstValue);
|
|
104
|
+
const lines: string[] = [];
|
|
105
|
+
if (isYamlScalar(firstValue)) {
|
|
106
|
+
lines.push(`${padding}- ${renderedFirstKey}: ${yamlScalar(firstValue)}`);
|
|
107
|
+
} else if (firstEmpty !== undefined) {
|
|
108
|
+
lines.push(`${padding}- ${renderedFirstKey}: ${firstEmpty}`);
|
|
109
|
+
} else if (Array.isArray(firstValue) || isPlainRecord(firstValue)) {
|
|
110
|
+
lines.push(`${padding}- ${renderedFirstKey}:`);
|
|
111
|
+
lines.push(...yamlLines(firstValue, indent + 4));
|
|
112
|
+
} else {
|
|
113
|
+
throw new UnserializableValueError(`YAML cannot represent the value at key ${JSON.stringify(firstKey)}: ${describeValue(firstValue)}`);
|
|
114
|
+
}
|
|
115
|
+
for (const [key, child] of rest) lines.push(...yamlMapEntryLines(key, child, indent + 2));
|
|
116
|
+
return lines;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function yamlLines(value: unknown, indent: number): string[] {
|
|
120
|
+
const padding = " ".repeat(indent);
|
|
121
|
+
if (isYamlScalar(value)) return [`${padding}${yamlScalar(value)}`];
|
|
122
|
+
if (Array.isArray(value)) {
|
|
123
|
+
if (value.length === 0) return [`${padding}[]`];
|
|
124
|
+
return value.flatMap(item => {
|
|
125
|
+
if (isYamlScalar(item)) return [`${padding}- ${yamlScalar(item)}`];
|
|
126
|
+
if (isPlainRecord(item)) return yamlArrayMapLines(item, indent);
|
|
127
|
+
/*
|
|
128
|
+
* A nested sequence is ordinary YAML (`matrix: [[1,2],[3,4]]`). This
|
|
129
|
+
* used to throw a plain Error carrying `String(item)` — untyped, so the
|
|
130
|
+
* writer could not turn it into a refusal and it surfaced as a 500, and
|
|
131
|
+
* carrying the value's CONTENTS into the message, which a config may
|
|
132
|
+
* not want repeated.
|
|
133
|
+
*/
|
|
134
|
+
if (Array.isArray(item)) {
|
|
135
|
+
if (item.length === 0) return [`${padding}- []`];
|
|
136
|
+
return [`${padding}-`, ...yamlLines(item, indent + 2)];
|
|
137
|
+
}
|
|
138
|
+
throw new UnserializableValueError(
|
|
139
|
+
`YAML cannot represent this sequence item: ${describeValue(item)}`,
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (isPlainRecord(value)) {
|
|
144
|
+
const entries = Object.entries(value);
|
|
145
|
+
if (entries.length === 0) return [`${padding}{}`];
|
|
146
|
+
return entries.flatMap(([key, child]) => yamlMapEntryLines(key, child, indent));
|
|
147
|
+
}
|
|
148
|
+
throw new UnserializableValueError(`YAML cannot represent this value: ${describeValue(value)}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Block-style YAML for the shallow shapes we emit. Throws on anything else. */
|
|
152
|
+
export function renderYaml(value: unknown, indent = 0): string {
|
|
153
|
+
if (!Number.isInteger(indent) || indent < 0) {
|
|
154
|
+
throw new Error(`YAML indent must be a non-negative integer: ${String(indent)}`);
|
|
155
|
+
}
|
|
156
|
+
return `${yamlLines(value, indent).join("\n")}\n`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** A short, safe description for an error message — never the value's contents. */
|
|
160
|
+
function describeValue(value: unknown): string {
|
|
161
|
+
if (value === null) return "null";
|
|
162
|
+
if (Array.isArray(value)) return "an array";
|
|
163
|
+
if (typeof value === "object") return "an object";
|
|
164
|
+
return typeof value;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** TOML basic-string escape. Mirrors `tomlString` in src/grok/inject.ts. */
|
|
168
|
+
export function tomlString(value: string): string {
|
|
169
|
+
return JSON.stringify(value);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Bare key when safe, basic-string key otherwise (TOML 1.0 §Keys). */
|
|
173
|
+
export function quoteTomlKey(key: string): string {
|
|
174
|
+
return /^[A-Za-z0-9_-]+$/.test(key) ? key : tomlString(key);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function tomlScalar(value: unknown): string {
|
|
178
|
+
if (typeof value === "string") return tomlString(value);
|
|
179
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
180
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
181
|
+
/*
|
|
182
|
+
* Arrays of ANY scalar, not just strings. The string-only check was written
|
|
183
|
+
* against our own builder output; a user's config legitimately holds
|
|
184
|
+
* `ports = [1, 2]`, and refusing it threw out of the writer as a 500.
|
|
185
|
+
* TOML permits mixed-type arrays since 1.0, so the elements are rendered
|
|
186
|
+
* individually rather than required to share a type.
|
|
187
|
+
*/
|
|
188
|
+
if (Array.isArray(value)) {
|
|
189
|
+
return `[${value.map(item => tomlScalar(item)).join(", ")}]`;
|
|
190
|
+
}
|
|
191
|
+
/*
|
|
192
|
+
* An inline table is valid TOML (`items = [{ x = 1 }]`), and rendering one
|
|
193
|
+
* inline is straightforward — but only where the whole array can be
|
|
194
|
+
* rendered, so this stays a scalar-position renderer rather than silently
|
|
195
|
+
* flattening a table into a string.
|
|
196
|
+
*/
|
|
197
|
+
if (isPlainRecord(value)) {
|
|
198
|
+
const entries = Object.entries(value);
|
|
199
|
+
return `{ ${entries.map(([k, v]) => `${quoteTomlKey(k)} = ${tomlScalar(v)}`).join(", ")} }`;
|
|
200
|
+
}
|
|
201
|
+
throw new UnserializableValueError(`TOML cannot represent this value: ${describeValue(value)}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Render `{ [table]: { key: scalar } }` as TOML. Scalars come first, then
|
|
206
|
+
* tables in insertion order, so two calls with the same document produce
|
|
207
|
+
* identical bytes — the stability guarantee `normalizeExportModels` exists to
|
|
208
|
+
* protect.
|
|
209
|
+
*/
|
|
210
|
+
export function renderToml(document: Record<string, unknown>, prefix = ""): string {
|
|
211
|
+
const scalars: string[] = [];
|
|
212
|
+
const tables: string[] = [];
|
|
213
|
+
for (const [key, value] of Object.entries(document)) {
|
|
214
|
+
const path = prefix ? `${prefix}.${quoteTomlKey(key)}` : quoteTomlKey(key);
|
|
215
|
+
if (isPlainRecord(value)) {
|
|
216
|
+
tables.push(`[${path}]\n${renderToml(value, path)}`.trimEnd());
|
|
217
|
+
} else {
|
|
218
|
+
scalars.push(`${quoteTomlKey(key)} = ${tomlScalar(value)}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return `${[scalars.join("\n"), tables.join("\n\n")].filter(Boolean).join("\n\n")}\n`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Every serializer returns text ending in exactly one newline. */
|
|
225
|
+
export function serializeDocument(document: unknown, format: ConfigFormat): string {
|
|
226
|
+
switch (format) {
|
|
227
|
+
case "json": return `${JSON.stringify(document, null, 2)}\n`;
|
|
228
|
+
case "json5": return `${Bun.JSON5.stringify(document, null, 2)}\n`;
|
|
229
|
+
case "yaml": return renderYaml(document);
|
|
230
|
+
case "toml": {
|
|
231
|
+
if (!isPlainRecord(document)) throw new Error("TOML root must be a table");
|
|
232
|
+
return renderToml(document);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|