@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005
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/bin/ocx.mjs +11 -10
- package/gui/dist/assets/App-CppMz-6w.js +54 -0
- package/gui/dist/assets/App-lCIslZ1L.css +1 -0
- package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
- package/gui/dist/assets/index-DQt9srNi.js +94 -0
- package/gui/dist/assets/index-DmQB8D_R.css +1 -0
- package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
- package/gui/dist/index.html +4 -3
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +19 -2
- package/src/adapters/anthropic/client-identity.ts +13 -4
- package/src/adapters/anthropic/native-client-preamble.ts +31 -0
- package/src/adapters/anthropic/passthrough.ts +103 -25
- package/src/adapters/antigravity-refusal.ts +28 -0
- package/src/adapters/cursor/protobuf-request.ts +6 -0
- package/src/adapters/cursor/request-builder.ts +3 -0
- package/src/adapters/cursor/structured-output.ts +13 -0
- package/src/adapters/cursor/types.ts +2 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -2
- package/src/adapters/devin/trajectory.ts +49 -0
- package/src/adapters/devin.ts +13 -0
- package/src/adapters/ollama-native.ts +62 -19
- package/src/adapters/openai-responses/passthrough.ts +133 -103
- package/src/adapters/openai-responses/request-strips.ts +10 -4
- package/src/adapters/zed.ts +26 -4
- package/src/bridge/response-json.ts +14 -14
- package/src/bridge/sse.ts +15 -5
- package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
- package/src/claude/agents-inject.ts +4 -43
- package/src/claude/gateway-cache.ts +16 -13
- package/src/claude/inbound-model-options.ts +9 -2
- package/src/claude/intercept/cli-catalog.ts +27 -16
- package/src/claude/intercept/client-hello.ts +103 -0
- package/src/claude/intercept/model-bindings.ts +3 -2
- package/src/claude/intercept/picker-bootstrap.ts +37 -16
- package/src/claude/intercept/picker-budget.ts +41 -0
- package/src/claude/intercept/picker-listener.ts +279 -66
- package/src/claude/outbound.ts +2 -0
- package/src/claude/subagent-force-status.ts +53 -0
- package/src/claude/subagent-model.ts +93 -0
- package/src/cli/access-audio-input.ts +84 -0
- package/src/cli/access-audio-live.ts +99 -0
- package/src/cli/access-audio.ts +123 -0
- package/src/cli/access-data-client.ts +127 -0
- package/src/cli/access-data-plane.ts +104 -0
- package/src/cli/access-data-response.ts +76 -0
- package/src/cli/access.ts +65 -2
- package/src/cli/account-api.ts +33 -6
- package/src/cli/account-auth.ts +111 -42
- package/src/cli/account-key-quota.ts +118 -0
- package/src/cli/account-main.ts +11 -4
- package/src/cli/account-next-actions.ts +63 -0
- package/src/cli/account-policy-dto.ts +68 -0
- package/src/cli/account-policy.ts +167 -0
- package/src/cli/account-target.ts +8 -0
- package/src/cli/account.ts +37 -9
- package/src/cli/agent-runtime-settings.ts +130 -0
- package/src/cli/agent-settings.ts +101 -0
- package/src/cli/agent.ts +29 -2
- package/src/cli/alias.ts +7 -2
- package/src/cli/aside-profiles.ts +2 -2
- package/src/cli/capabilities-access-remote.ts +147 -0
- package/src/cli/capabilities-accounts.ts +195 -0
- package/src/cli/capabilities-agents-routing.ts +414 -0
- package/src/cli/capabilities-base.ts +783 -0
- package/src/cli/capabilities-command.ts +1 -0
- package/src/cli/capabilities-integrations.ts +264 -0
- package/src/cli/capabilities-lab.ts +214 -0
- package/src/cli/capabilities-observe-system.ts +662 -0
- package/src/cli/capabilities-provider-models.ts +302 -0
- package/src/cli/capabilities.ts +30 -1190
- package/src/cli/capability-types.ts +54 -0
- package/src/cli/catalog-command-result.ts +57 -0
- package/src/cli/chatgpt-command.ts +35 -9
- package/src/cli/claude-agent-startup-sync.ts +17 -0
- package/src/cli/claude-desktop-profile.ts +100 -0
- package/src/cli/claude-desktop.ts +39 -6
- package/src/cli/claude.ts +22 -6
- package/src/cli/codex-shim-autorestore.ts +2 -1
- package/src/cli/codex-shim-readiness.ts +109 -0
- package/src/cli/combo-input.ts +88 -0
- package/src/cli/combo-result.ts +52 -0
- package/src/cli/combo-stats.ts +123 -0
- package/src/cli/combo.ts +37 -14
- package/src/cli/companion-timeline.ts +142 -0
- package/src/cli/companion-usage.ts +146 -0
- package/src/cli/companion.ts +12 -3
- package/src/cli/config-command.ts +24 -8
- package/src/cli/connect.ts +19 -0
- package/src/cli/dispatch.ts +55 -82
- package/src/cli/doctor.ts +22 -4
- package/src/cli/export-command.ts +20 -9
- package/src/cli/gui.ts +8 -3
- package/src/cli/help-catalog.ts +101 -0
- package/src/cli/help-models-context.ts +17 -0
- package/src/cli/help-navigation.ts +50 -0
- package/src/cli/help-recovery.ts +66 -0
- package/src/cli/help.ts +69 -11
- package/src/cli/index.ts +38 -27
- package/src/cli/injection-follow.ts +35 -0
- package/src/cli/inspect.ts +5 -0
- package/src/cli/integration-aside-sync.ts +69 -0
- package/src/cli/integration-cursor.ts +106 -0
- package/src/cli/integration-input.ts +60 -0
- package/src/cli/integration-journal.ts +49 -0
- package/src/cli/integration-plan-dto.ts +156 -0
- package/src/cli/integration-preview.ts +215 -0
- package/src/cli/integrations.ts +24 -18
- package/src/cli/json-input.ts +134 -0
- package/src/cli/link.ts +32 -4
- package/src/cli/local-sync-result.ts +41 -0
- package/src/cli/log-follow.ts +84 -0
- package/src/cli/log-view-filter.ts +146 -0
- package/src/cli/logout-command.ts +44 -0
- package/src/cli/model-picker-ordering.ts +137 -0
- package/src/cli/models-custom-input.ts +107 -0
- package/src/cli/models-custom-runtime.ts +112 -0
- package/src/cli/models-order.ts +194 -0
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +9 -1
- package/src/cli/models.ts +66 -81
- package/src/cli/observe-stream.ts +142 -0
- package/src/cli/observe.ts +75 -34
- package/src/cli/opencode.ts +54 -10
- package/src/cli/provider-batch.ts +77 -0
- package/src/cli/provider-lifecycle-runtime.ts +170 -0
- package/src/cli/provider-result.ts +124 -0
- package/src/cli/provider-runtime.ts +15 -12
- package/src/cli/provider-settings.ts +143 -0
- package/src/cli/provider.ts +116 -64
- package/src/cli/registry.ts +90 -24
- package/src/cli/remote-workspace-hub.ts +83 -0
- package/src/cli/remote-workspace.ts +6 -1
- package/src/cli/restart-failure.ts +27 -0
- package/src/cli/root.ts +42 -9
- package/src/cli/route-policy-write.ts +137 -0
- package/src/cli/route-policy.ts +8 -0
- package/src/cli/runtime-api.ts +57 -80
- package/src/cli/secret-args.ts +149 -0
- package/src/cli/settings-result.ts +30 -0
- package/src/cli/storage.ts +77 -7
- package/src/cli/system-command.ts +60 -2
- package/src/cli/system-restart-client.ts +20 -3
- package/src/cli/system-settings-parity.ts +83 -0
- package/src/cli/tray-proxy.ts +3 -2
- package/src/cli/update-restart-candidate.ts +21 -0
- package/src/cli/update-restart-child.ts +81 -0
- package/src/cli/update-restart-home.ts +38 -0
- package/src/cli/update-restart-transport.ts +193 -0
- package/src/cli/update-restart.ts +260 -0
- package/src/cli/usage-model-search.ts +58 -0
- package/src/cli/usage-report.ts +13 -5
- package/src/cli/v2-input.ts +66 -0
- package/src/cli/v2-local-output.ts +37 -0
- package/src/cli/v2-runtime.ts +90 -0
- package/src/cli/v2.ts +102 -144
- package/src/clients/config-export/contracts.ts +100 -0
- package/src/clients/config-export/droid.ts +54 -2
- package/src/clients/config-export/dsh.ts +21 -0
- package/src/clients/config-export/kilo.ts +12 -2
- package/src/clients/config-export/model-metadata.ts +10 -4
- package/src/clients/config-export/reasoning-metadata.ts +47 -0
- package/src/clients/config-export.ts +53 -67
- package/src/clients/omo-role-models.ts +17 -3
- package/src/codex/account-credit-use.ts +4 -2
- package/src/codex/account-runtime-state.ts +21 -0
- package/src/codex/account-store.ts +11 -47
- package/src/codex/account-usability.ts +11 -8
- package/src/codex/auth-api/account-list.ts +73 -63
- package/src/codex/auth-api/main-account-probe.ts +25 -12
- package/src/codex/auth-api/pool-mode-gate.ts +6 -9
- package/src/codex/auth-api/pool-quota-probe.ts +2 -4
- package/src/codex/auth-api/reset-credit-service.ts +3 -2
- package/src/codex/auth-context.ts +30 -11
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/build-entries.ts +1 -1
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/heal-observation.ts +66 -0
- package/src/codex/catalog/model-hints.ts +2 -1
- package/src/codex/catalog/model-visibility.ts +6 -3
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +13 -4
- package/src/codex/catalog/publication-observer.ts +19 -0
- package/src/codex/catalog/remote.ts +13 -15
- package/src/codex/catalog/restore.ts +19 -6
- package/src/codex/catalog/retained-sync.ts +60 -11
- package/src/codex/catalog/routed-removal.ts +116 -0
- package/src/codex/catalog/write-audit-contract.ts +21 -0
- package/src/codex/catalog/write-audit.ts +171 -0
- package/src/codex/catalog-self-heal.ts +284 -0
- package/src/codex/catalog-write-serialization.ts +71 -7
- package/src/codex/chatgpt-refresh-failure.ts +56 -0
- package/src/codex/codex-home-owner.ts +124 -0
- package/src/codex/convergence.ts +57 -16
- package/src/codex/features.ts +8 -1
- package/src/codex/inject/restore.ts +46 -4
- package/src/codex/inject.ts +11 -1
- package/src/codex/internal/catalog-writer.ts +120 -13
- package/src/codex/journal.ts +49 -13
- package/src/codex/main-account.ts +144 -11
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/quota-refresh-outcome.ts +17 -2
- package/src/codex/quota-types.ts +36 -1
- package/src/codex/quota.ts +67 -16
- package/src/codex/refresh.ts +20 -9
- package/src/codex/routing/cooldown-math.ts +10 -4
- package/src/codex/routing/selection.ts +11 -4
- package/src/codex/routing.ts +9 -9
- package/src/codex/runtime.ts +7 -1
- package/src/codex/shim-diagnostics.ts +80 -0
- package/src/codex/shim-fingerprint.ts +7 -2
- package/src/codex/shim-migration.ts +153 -0
- package/src/codex/shim-overlay.ts +509 -0
- package/src/codex/shim-path-resolution.ts +145 -0
- package/src/codex/shim-probe.ts +4 -3
- package/src/codex/shim-state-file.ts +93 -7
- package/src/codex/shim.ts +54 -107
- package/src/codex/subagent-model-fallback.ts +2 -1
- package/src/codex/sync.ts +24 -4
- package/src/combos/failover.ts +101 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/jev.ts +13 -3
- package/src/combos/resolve.ts +12 -0
- package/src/config/atomic-write.ts +13 -8
- package/src/config/diagnostics.ts +23 -1
- package/src/config/load-degrade.ts +7 -0
- package/src/config/provider-validation.ts +3 -0
- package/src/config/schema/chatgpt-desktop.ts +22 -0
- package/src/config/schema/compaction-triggers.ts +8 -0
- package/src/config/schema/config-schema.ts +43 -14
- package/src/config/schema/leaf-validators.ts +8 -3
- package/src/config/subagent-models.ts +12 -0
- package/src/generated/compatibility-version.json +645 -237
- package/src/grok/session-identity.ts +33 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/merge.ts +13 -5
- package/src/integrations/mutation-plan.ts +50 -5
- package/src/integrations/omp-yaml-source.ts +181 -1
- package/src/integrations/ownership-policy.ts +66 -0
- package/src/integrations/registry.ts +50 -1
- package/src/integrations/state.ts +106 -19
- package/src/integrations/target.ts +25 -2
- package/src/integrations/writer.ts +107 -35
- package/src/lib/gui-pair-capability.ts +17 -0
- package/src/lib/package-tree-integrity.ts +42 -1
- package/src/lib/provider-client-headers.ts +62 -0
- package/src/lib/proxy-env.ts +3 -0
- package/src/lib/test-home-guard.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +19 -8
- package/src/oauth/anthropic-continuity.ts +65 -0
- package/src/oauth/anthropic-identity.ts +75 -0
- package/src/oauth/anthropic-routing.ts +20 -15
- package/src/oauth/anthropic.ts +34 -3
- package/src/oauth/generic-account-failover.ts +21 -0
- package/src/oauth/health.ts +32 -5
- package/src/oauth/index.ts +42 -35
- package/src/oauth/local-token-detect.ts +7 -0
- package/src/oauth/pool-settings-capability.ts +9 -2
- package/src/oauth/refresh-policy.ts +2 -0
- package/src/oauth/store.ts +55 -6
- package/src/oauth/types.ts +9 -0
- package/src/protocols/encoders/messages.ts +4 -3
- package/src/protocols/plan-snapshot.ts +1 -1
- package/src/protocols/settings.ts +28 -4
- package/src/providers/antigravity-effort-families.ts +75 -0
- package/src/providers/antigravity-models.ts +54 -16
- package/src/providers/antigravity-wire-snapshot.ts +66 -0
- package/src/providers/derive.ts +2 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/new-model-policy.ts +48 -10
- package/src/providers/registry/entries-extended.ts +3 -0
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/model-seeds.ts +5 -0
- package/src/providers/registry/types.ts +4 -0
- package/src/responses/compaction.ts +19 -0
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +9 -0
- package/src/server/caller-session-identity.ts +32 -0
- package/src/server/chat-completions.ts +10 -0
- package/src/server/claude-messages.ts +29 -15
- package/src/server/droid-reasoning-default.ts +50 -0
- package/src/server/github-copilot-responses-repair.ts +3 -6
- package/src/server/grok-responses-control-frame.ts +2 -2
- package/src/server/grok-responses-snapshot-repair.ts +4 -5
- package/src/server/gui-session.ts +33 -11
- package/src/server/index/claude-intercept-lifecycle.ts +4 -0
- package/src/server/index/serve-options.ts +13 -7
- package/src/server/index.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +12 -134
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +8 -1
- package/src/server/management/cursor-integration-routes.ts +3 -3
- package/src/server/management/integration-routes.ts +69 -11
- package/src/server/management/link-routes.ts +20 -13
- package/src/server/management/model-routes.ts +3 -3
- package/src/server/management/model-row-export-metadata.ts +150 -0
- package/src/server/management/model-rows.ts +148 -9
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/management/oauth-account-routes.ts +96 -30
- package/src/server/management/route-registry.ts +29 -27
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/subagent-model-routes.ts +214 -0
- package/src/server/messages-native-eligibility.ts +3 -6
- package/src/server/messages-native-oauth.ts +82 -35
- package/src/server/messages-native.ts +78 -16
- package/src/server/relay.ts +14 -44
- package/src/server/request-log-generation-window.ts +38 -0
- package/src/server/request-log.ts +13 -0
- package/src/server/responses/adapter-dispatch.ts +152 -28
- package/src/server/responses/codex-auth-error.ts +10 -2
- package/src/server/responses/combo-stream-preflight.ts +29 -3
- package/src/server/responses/compaction-routing.ts +10 -1
- package/src/server/responses/core-auth.ts +67 -30
- package/src/server/responses/core-codex-account.ts +6 -3
- package/src/server/responses/core-combo-failure.ts +5 -1
- package/src/server/responses/core-combo-native.ts +7 -1
- package/src/server/responses/core-combo.ts +64 -7
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/fetch-helpers.ts +9 -0
- package/src/server/responses/passthrough-delivery.ts +1 -1
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/request-prepare.ts +5 -0
- package/src/server/responses/request-spend.ts +30 -30
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/responses-hosted-image-display.ts +70 -9
- package/src/server/responses-terminal-repair.ts +15 -2
- package/src/server/sse-frame-buffer.ts +38 -36
- package/src/server/sse-payload-rewrite.ts +78 -49
- package/src/server/ws-bridge.ts +1 -11
- package/src/service/cli.ts +6 -1
- package/src/service/ownership-mutation-lease.d.mts +26 -0
- package/src/service/ownership-mutation-lease.mjs +76 -1
- package/src/service/windows-taskxml.ts +1 -2
- package/src/service-manager-probe.ts +36 -2
- package/src/types/config.ts +6 -0
- package/src/types/provider.ts +28 -10
- package/src/update/index.ts +2 -1
- package/src/update/npm-invocation.mjs +35 -3
- package/src/update/proxy-liveness-probe.mjs +38 -8
- package/src/update/update-failure-guidance.d.mts +6 -0
- package/src/update/update-failure-guidance.mjs +30 -5
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +7 -0
- package/src/usage/summary.ts +82 -1
- package/src/web-search/loop.ts +11 -8
- package/src/web-search/run-turn-loop.ts +10 -7
- package/gui/dist/assets/App-CddorHdA.js +0 -51
- package/gui/dist/assets/App-DMudtISW.css +0 -1
- package/gui/dist/assets/index-Btze1I3T.css +0 -1
- package/gui/dist/assets/index-CvnY7E6i.js +0 -86
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grok Build sends its conversation identity as `x-grok-conv-id` (and the same value as the body
|
|
3
|
+
* `prompt_cache_key`), but never the `session_id` header Codex clients send. The ChatGPT Codex
|
|
4
|
+
* backend only reuses a warmed prompt prefix for requests that carry `session_id`, and opencodex
|
|
5
|
+
* reads the same header for thread affinity and conversation logging. Without it, every Grok turn
|
|
6
|
+
* routed to an OpenAI model was a cold `new_bind` with near-zero cached input.
|
|
7
|
+
*
|
|
8
|
+
* Only the managed Grok surface (`x-opencodex-grok: 1`, written by `ocx` into Grok's config) is
|
|
9
|
+
* promoted, and an explicit caller session header always wins.
|
|
10
|
+
*/
|
|
11
|
+
const GROK_SURFACE_HEADER = "x-opencodex-grok";
|
|
12
|
+
const GROK_CONVERSATION_HEADER = "x-grok-conv-id";
|
|
13
|
+
const SESSION_HEADERS = ["session_id", "session-id", "thread-id"] as const;
|
|
14
|
+
// Grok emits UUIDs and `turn-summary-<uuid>`; anything else is not promoted.
|
|
15
|
+
const SAFE_CONVERSATION_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
16
|
+
|
|
17
|
+
export function grokConversationSessionId(headers: Headers): string | undefined {
|
|
18
|
+
if (headers.get(GROK_SURFACE_HEADER) !== "1") return undefined;
|
|
19
|
+
if (SESSION_HEADERS.some(name => headers.has(name))) return undefined;
|
|
20
|
+
const conversationId = headers.get(GROK_CONVERSATION_HEADER)?.trim();
|
|
21
|
+
if (!conversationId || !SAFE_CONVERSATION_ID.test(conversationId)) return undefined;
|
|
22
|
+
return conversationId;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Returns `req` unchanged unless a Grok conversation id can stand in for the missing `session_id`. */
|
|
26
|
+
export function withGrokSessionIdentity(req: Request): Request {
|
|
27
|
+
const sessionId = grokConversationSessionId(req.headers);
|
|
28
|
+
if (!sessionId) return req;
|
|
29
|
+
const headers = new Headers(req.headers);
|
|
30
|
+
headers.set("session_id", sessionId);
|
|
31
|
+
// The clone keeps the unread body stream and the caller's abort signal.
|
|
32
|
+
return new Request(req, { headers });
|
|
33
|
+
}
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
/** Refresh only previously connected clients; a refused file never blocks its peers. */
|
|
12
12
|
export async function refreshOwnedCatalogIntegrations(
|
|
13
13
|
input: Omit<OwnedIntegrationRefreshInput, "clientId">,
|
|
14
|
-
clientIds: readonly IntegrationClientId[] = ["pi", "aside", "raycast", "omo", "droid"],
|
|
14
|
+
clientIds: readonly IntegrationClientId[] = ["pi", "aside", "raycast", "omo", "droid", "opencode", "kilo"],
|
|
15
15
|
): Promise<OwnedIntegrationRefreshOutcome[]> {
|
|
16
16
|
// Client files are shared with the live proxy a sibling instance runs beside; their entries
|
|
17
17
|
// point at the owner's port, and refreshing them here would re-point them at this one.
|
|
@@ -184,9 +184,11 @@ export function setPath(doc: unknown, path: readonly string[], value: unknown):
|
|
|
184
184
|
/*
|
|
185
185
|
* `parent[slot]` is the position the segment just consumed addresses. The
|
|
186
186
|
* root sits in a one-key holder so the first segment needs no special case:
|
|
187
|
-
* a
|
|
187
|
+
* a document that is neither a record nor a sequence is replaced by `{}`
|
|
188
|
+
* exactly as before. A sequence root survives so a leading selector can
|
|
189
|
+
* address one of its entries (a DSH profile patch is a top-level list).
|
|
188
190
|
*/
|
|
189
|
-
const holder: Record<string, unknown> = { root: isPlainRecord(doc) ? clone(doc) : {} };
|
|
191
|
+
const holder: Record<string, unknown> = { root: isPlainRecord(doc) || Array.isArray(doc) ? clone(doc) : {} };
|
|
190
192
|
let parent: Record<string, unknown> | unknown[] = holder;
|
|
191
193
|
let slot: string | number = "root";
|
|
192
194
|
const read = (): unknown => (Array.isArray(parent) ? parent[slot as number] : parent[slot as string]);
|
|
@@ -244,8 +246,8 @@ export function deletePath(
|
|
|
244
246
|
path: readonly string[],
|
|
245
247
|
createdContainers: ReadonlySet<string> = new Set(),
|
|
246
248
|
): { doc: unknown; removed: boolean } {
|
|
247
|
-
if (!isPlainRecord(doc) || path.length === 0) return { doc, removed: false };
|
|
248
|
-
const root = clone(doc) as Record<string, unknown
|
|
249
|
+
if ((!isPlainRecord(doc) && !Array.isArray(doc)) || path.length === 0) return { doc, removed: false };
|
|
250
|
+
const root = clone(doc) as Record<string, unknown> | unknown[];
|
|
249
251
|
// `chain[i]` is the container segment `i` is resolved against; `slots[i]` is
|
|
250
252
|
// the key or index it resolved to, so the prune walk can delete by position.
|
|
251
253
|
const chain: (Record<string, unknown> | unknown[])[] = [root];
|
|
@@ -288,7 +290,13 @@ export function deletePath(
|
|
|
288
290
|
*/
|
|
289
291
|
for (let index = path.length - 1; index >= 1; index -= 1) {
|
|
290
292
|
const container = chain[index]!;
|
|
291
|
-
|
|
293
|
+
// An element we pushed for a selector is empty once only the fields that seeded it remain.
|
|
294
|
+
const segment = parseSegment(path[index - 1]!);
|
|
295
|
+
const empty = Array.isArray(container)
|
|
296
|
+
? container.length === 0
|
|
297
|
+
: segment.kind === "select"
|
|
298
|
+
? Object.keys(container).length === segment.criteria.length && selectIndex([container], segment.criteria) === 0
|
|
299
|
+
: Object.keys(container).length === 0;
|
|
292
300
|
if (!empty) break;
|
|
293
301
|
const containerPath = path.slice(0, index).join("\u0000");
|
|
294
302
|
if (!createdContainers.has(containerPath)) break;
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* preview route.
|
|
15
15
|
*/
|
|
16
16
|
import { createHash } from "node:crypto";
|
|
17
|
+
import { dirname } from "node:path";
|
|
17
18
|
import { canonicalContribution, fingerprint, type OwnershipRecord } from "./ownership";
|
|
18
19
|
import { ClientPathError, EXPORT_CLIENTS, type ExportModel, type ManagedContribution } from "../clients/config-export";
|
|
19
20
|
import { OPENCODE_PROVIDER_ID } from "../clients/config-export/constants";
|
|
@@ -34,9 +35,9 @@ import {
|
|
|
34
35
|
restoreOwnershipCollision,
|
|
35
36
|
type IntegrationClientId,
|
|
36
37
|
} from "./registry";
|
|
37
|
-
import { declaredIntegrationTarget, resolveIntegrationTarget, type IntegrationTarget } from "./target";
|
|
38
|
+
import { declaredIntegrationTarget, resolveIntegrationTarget, type IneffectiveWrite, type IneffectiveWriteReason, type IntegrationTarget } from "./target";
|
|
38
39
|
import { shouldInjectApiAuthHeader } from "../codex/inject";
|
|
39
|
-
import { classifyIntegration, exportContextOf, readPath, type IntegrationState, type StateReason } from "./state";
|
|
40
|
+
import { buildIntegrationContribution, classifyIntegration, exportContextOf, readPath, type IntegrationState, type StateReason } from "./state";
|
|
40
41
|
import { inspectKiloCandidates } from "./kilo-candidates";
|
|
41
42
|
import { InvalidSelectorError } from "./merge";
|
|
42
43
|
import { createIntegrationStateStore, type IntegrationStateStore } from "./store";
|
|
@@ -99,6 +100,13 @@ export interface IntegrationMutationPlan {
|
|
|
99
100
|
*/
|
|
100
101
|
readonly willChange: boolean;
|
|
101
102
|
readonly refusalReason?: RefusalReason;
|
|
103
|
+
/**
|
|
104
|
+
* `superseded_store` only: why the store the client reads is not written. The store's location
|
|
105
|
+
* stays on the status row (`supersededBy`); the plan names no file.
|
|
106
|
+
*/
|
|
107
|
+
readonly supersededReason?: IneffectiveWriteReason;
|
|
108
|
+
/** `missing-store` only: the document that recreates the store, so the remedy can name it. */
|
|
109
|
+
readonly missingStoreDocument?: string;
|
|
102
110
|
readonly profileId?: number;
|
|
103
111
|
}
|
|
104
112
|
|
|
@@ -129,7 +137,7 @@ const CLIENT_MANAGED_PATHS = {
|
|
|
129
137
|
openclaw: [["models", "providers", OPENCODE_PROVIDER_ID]],
|
|
130
138
|
kimi: [["providers", OPENCODE_PROVIDER_ID], ["models", DYNAMIC_SEGMENT]],
|
|
131
139
|
gajae: [["providers", OPENCODE_PROVIDER_ID]],
|
|
132
|
-
dsh: [["llm-pi-ai", "providers", OPENCODE_PROVIDER_ID]],
|
|
140
|
+
dsh: [["llm-pi-ai", "providers", OPENCODE_PROVIDER_ID], ["[id=llm-pi-ai]", "config", "providers", OPENCODE_PROVIDER_ID]],
|
|
133
141
|
mcode: [["custom_provider", OPENCODE_PROVIDER_ID]],
|
|
134
142
|
zcode: [
|
|
135
143
|
["provider", OPENCODE_PROVIDER_ID],
|
|
@@ -245,6 +253,11 @@ export interface PlanFingerprintInput {
|
|
|
245
253
|
* flip as a store appearing.
|
|
246
254
|
*/
|
|
247
255
|
readonly ineffectiveWrite: string | null;
|
|
256
|
+
/**
|
|
257
|
+
* The same finding as `ineffectiveWrite`, unflattened, for the refusal's description. It is
|
|
258
|
+
* descriptive only: the bound token above is what decides and what the fingerprint covers.
|
|
259
|
+
*/
|
|
260
|
+
readonly ineffective?: IneffectiveWrite | null;
|
|
248
261
|
/** Exact current bytes, or null when the target is missing. Missing and empty are not equal. */
|
|
249
262
|
readonly before: string | null;
|
|
250
263
|
readonly contribution: ManagedContribution | null;
|
|
@@ -497,6 +510,12 @@ export function buildMutationPlan(input: PlanInput): IntegrationMutationPlan {
|
|
|
497
510
|
canApply: outcome.kind !== "refuse",
|
|
498
511
|
willChange: outcome.kind === "change",
|
|
499
512
|
...(outcome.kind === "refuse" ? { refusalReason: outcome.reason } : {}),
|
|
513
|
+
...(outcome.kind === "refuse" && outcome.reason === "superseded_store" && input.ineffective
|
|
514
|
+
? {
|
|
515
|
+
supersededReason: input.ineffective.why,
|
|
516
|
+
...(input.ineffective.emptyDocument === undefined ? {} : { missingStoreDocument: input.ineffective.emptyDocument }),
|
|
517
|
+
}
|
|
518
|
+
: {}),
|
|
500
519
|
...(input.profileId === undefined ? {} : { profileId: input.profileId }),
|
|
501
520
|
});
|
|
502
521
|
}
|
|
@@ -531,6 +550,25 @@ function unboundPlan(
|
|
|
531
550
|
});
|
|
532
551
|
}
|
|
533
552
|
|
|
553
|
+
/** A restore must not recreate a declared store without its client's lock. */
|
|
554
|
+
export function restoreStoreDirectoryRefusal(
|
|
555
|
+
input: IntegrationWriteInput,
|
|
556
|
+
configPath: string,
|
|
557
|
+
io: IntegrationIO,
|
|
558
|
+
): string | null {
|
|
559
|
+
const declared = INTEGRATION_CLIENTS[input.clientId].currentStore;
|
|
560
|
+
if (!declared?.lockFile) return null;
|
|
561
|
+
let storePath: string;
|
|
562
|
+
try {
|
|
563
|
+
storePath = declared.path(input.env, input.home);
|
|
564
|
+
} catch (error) {
|
|
565
|
+
if (error instanceof ClientPathError) return null;
|
|
566
|
+
throw error;
|
|
567
|
+
}
|
|
568
|
+
return configPath === storePath && io.statKind(dirname(storePath)) !== "dir"
|
|
569
|
+
? "the client store directory is missing; restore will not create it" : null;
|
|
570
|
+
}
|
|
571
|
+
|
|
534
572
|
/**
|
|
535
573
|
* Restore reads a different specification, so it gets its own observation.
|
|
536
574
|
*
|
|
@@ -592,6 +630,10 @@ export function observeRestore(
|
|
|
592
630
|
failed: observationFailure("conflict", "conflict", "that operation was recorded for a different location"),
|
|
593
631
|
} as const;
|
|
594
632
|
}
|
|
633
|
+
const directoryRefusal = restoreStoreDirectoryRefusal(input, configPath, io);
|
|
634
|
+
if (directoryRefusal !== null) {
|
|
635
|
+
return { failed: observationFailure("unsafe", "unsafe", directoryRefusal) } as const;
|
|
636
|
+
}
|
|
595
637
|
/*
|
|
596
638
|
* A legal historical path is not enough. Another candidate can already own
|
|
597
639
|
* the single record, and committing this row's prior record would orphan the
|
|
@@ -678,6 +720,7 @@ export function previewIntegration(input: IntegrationWriteInput, request: Previe
|
|
|
678
720
|
// Loopback-only clients cannot carry the admission header a non-loopback bind requires.
|
|
679
721
|
admissionBlocked: isLoopbackOnly(observed.clientId) && shouldInjectApiAuthHeader(input.config),
|
|
680
722
|
ineffectiveWrite: observed.ineffectiveWrite,
|
|
723
|
+
ineffective: observed.target.ineffective,
|
|
681
724
|
before: observed.before,
|
|
682
725
|
contribution: observed.contribution,
|
|
683
726
|
record: observed.record,
|
|
@@ -791,6 +834,8 @@ export interface IntegrationWriteInput {
|
|
|
791
834
|
models: readonly ExportModel[];
|
|
792
835
|
config: OcxConfig;
|
|
793
836
|
port: number;
|
|
837
|
+
/** Explicit Droid-only defaults; omitted means retain matching owned rows. */
|
|
838
|
+
droidReasoningDefaults?: Record<string, string>;
|
|
794
839
|
env?: NodeJS.ProcessEnv;
|
|
795
840
|
home?: string;
|
|
796
841
|
store?: IntegrationStateStore;
|
|
@@ -936,13 +981,13 @@ export function observeIntegration(
|
|
|
936
981
|
* reads a different document there, and a write in the config file's shape
|
|
937
982
|
* would be as unread as a write to the config file itself.
|
|
938
983
|
*/
|
|
939
|
-
const contribution = effective.buildContribution(exportContextOf(input));
|
|
940
984
|
// A record proves ownership of the file it was written FOR. Matching only by
|
|
941
985
|
// client id let a record for one home authorize a write to another whose
|
|
942
986
|
// bytes happened to hash the same — which deleted a config we never touched.
|
|
943
987
|
const record = stored && stored.clientId === clientId && stored.configPath === configPath
|
|
944
988
|
? stored
|
|
945
989
|
: null;
|
|
990
|
+
const contribution = buildIntegrationContribution(input, effective, parsed, record);
|
|
946
991
|
if (clientId === "droid" && input.models.length > 0 && contribution.fragments.length === 0 && !record) {
|
|
947
992
|
return { failed: observationFailure("unsafe", "unsafe", "Factory Droid has no addressable models in the selected catalog") } as const;
|
|
948
993
|
}
|
|
@@ -952,7 +997,7 @@ export function observeIntegration(
|
|
|
952
997
|
// ownership here and disable would delete fragments it never wrote.
|
|
953
998
|
const classified = classifyIntegration({
|
|
954
999
|
fileText: before, fileIsRegular: true, parsed, record, contribution, configPath, clientId,
|
|
955
|
-
format: effective.format,
|
|
1000
|
+
format: effective.format, sourcePreservingYaml: effective.sourcePreservingYaml !== null,
|
|
956
1001
|
});
|
|
957
1002
|
if (clientId === "droid" && record && (classified.state === "current" || classified.state === "stale")) {
|
|
958
1003
|
try { assertDroidRecordedSettingsUnambiguous(detectDir, parsed, record); }
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* expected document; unsupported YAML fails closed and never falls back to a
|
|
8
8
|
* whole-document serializer.
|
|
9
9
|
*/
|
|
10
|
+
import { parseSegment, selectIndex, type SelectorCriterion } from "./merge";
|
|
10
11
|
import { renderYaml } from "./serialize";
|
|
11
12
|
|
|
12
13
|
interface SourceLine {
|
|
@@ -442,6 +443,7 @@ function upsertSource(
|
|
|
442
443
|
path: readonly string[],
|
|
443
444
|
value: unknown,
|
|
444
445
|
): string | null {
|
|
446
|
+
if (leadingSelector(path) !== null) return upsertSequenceEntry(text, parsed, path, value);
|
|
445
447
|
const located = locatePath(text, parsed, path);
|
|
446
448
|
if (located === null || located.kind === "unsupported-style") return null;
|
|
447
449
|
const eol = lineEnding(text);
|
|
@@ -467,6 +469,178 @@ function upsertSource(
|
|
|
467
469
|
return preserveFinalNewline(candidate, text, eol);
|
|
468
470
|
}
|
|
469
471
|
|
|
472
|
+
/**
|
|
473
|
+
* The criteria of a path whose first segment selects one entry of a top-level
|
|
474
|
+
* sequence, or null for an ordinary block-map path.
|
|
475
|
+
*
|
|
476
|
+
* DSH 0.1.7+ keeps settings in a profile patch: a YAML list of loader rows,
|
|
477
|
+
* `- id: llm-pi-ai` among them. The entry is patched as the block map it is
|
|
478
|
+
* once its `- ` indicator and two-space continuation indent are set aside, and
|
|
479
|
+
* both are put back byte for byte. Every other list shape fails closed.
|
|
480
|
+
*/
|
|
481
|
+
function leadingSelector(path: readonly string[]): readonly SelectorCriterion[] | null {
|
|
482
|
+
if (path.length === 0) return null;
|
|
483
|
+
const segment = parseSegment(path[0]!);
|
|
484
|
+
return segment.kind === "select" ? segment.criteria : null;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Line ranges of a block sequence's top-level entries, or null for any other document shape. */
|
|
488
|
+
function topLevelEntries(lines: readonly SourceLine[]): Array<{ start: number; end: number }> | null {
|
|
489
|
+
const entries: Array<{ start: number; end: number }> = [];
|
|
490
|
+
let open = false;
|
|
491
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
492
|
+
const body = lines[index]!.body;
|
|
493
|
+
if (isBlank(body)) continue;
|
|
494
|
+
const spaces = leadingSpaces(body);
|
|
495
|
+
if (spaces === null) return null;
|
|
496
|
+
if (spaces > 0) {
|
|
497
|
+
// Continuation of the open entry; deeper text anywhere else is not a list we can place.
|
|
498
|
+
if (!open) return null;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
if (open) entries[entries.length - 1]!.end = index;
|
|
502
|
+
open = false;
|
|
503
|
+
if (isComment(body)) continue;
|
|
504
|
+
if (!/^- \S/u.test(body)) return null;
|
|
505
|
+
entries.push({ start: index, end: lines.length });
|
|
506
|
+
open = true;
|
|
507
|
+
}
|
|
508
|
+
return entries;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** The entry's text as the block map it holds, or null when it is not in the two-space form. */
|
|
512
|
+
function entrySource(text: string, lines: readonly SourceLine[], start: number, end: number): string | null {
|
|
513
|
+
let source = "";
|
|
514
|
+
for (let index = start; index < end; index += 1) {
|
|
515
|
+
const line = lines[index]!;
|
|
516
|
+
const raw = text.slice(line.start, line.end);
|
|
517
|
+
if (index === start) source += raw.slice(2);
|
|
518
|
+
else if (isBlank(line.body)) source += raw;
|
|
519
|
+
else if ((leadingSpaces(line.body) ?? 0) >= 2) source += raw.slice(2);
|
|
520
|
+
else return null;
|
|
521
|
+
}
|
|
522
|
+
return source;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** Put an entry back: `- ` on its first line, two spaces on every other non-blank line. */
|
|
526
|
+
function reindentEntry(source: string): string {
|
|
527
|
+
return sourceLines(source).map((line, index) => {
|
|
528
|
+
const raw = source.slice(line.start, line.end);
|
|
529
|
+
if (index === 0) return `- ${raw}`;
|
|
530
|
+
return isBlank(line.body) ? raw : ` ${raw}`;
|
|
531
|
+
}).join("");
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function firstLine(text: string): string {
|
|
535
|
+
return text.split(/\r?\n/u, 1)[0] ?? "";
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
type SequenceEntry =
|
|
539
|
+
| {
|
|
540
|
+
kind: "found";
|
|
541
|
+
criteria: readonly SelectorCriterion[];
|
|
542
|
+
rest: readonly string[];
|
|
543
|
+
/** Byte range of the entry, trailing blank lines included. */
|
|
544
|
+
startOffset: number;
|
|
545
|
+
endOffset: number;
|
|
546
|
+
source: string;
|
|
547
|
+
parsed: Record<string, unknown>;
|
|
548
|
+
/** Entries the list keeps if this one is removed. */
|
|
549
|
+
siblings: number;
|
|
550
|
+
}
|
|
551
|
+
| {
|
|
552
|
+
kind: "missing";
|
|
553
|
+
criteria: readonly SelectorCriterion[];
|
|
554
|
+
rest: readonly string[];
|
|
555
|
+
/** Where a new entry goes, and the end of the `[]` line it replaces, if any. */
|
|
556
|
+
insertAt: number;
|
|
557
|
+
replaceEnd: number | null;
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
function sequenceEntry(text: string, parsed: unknown, path: readonly string[]): SequenceEntry | null {
|
|
561
|
+
const criteria = leadingSelector(path);
|
|
562
|
+
if (criteria === null || !Array.isArray(parsed)) return null;
|
|
563
|
+
const lines = sourceLines(text);
|
|
564
|
+
const rest = path.slice(1);
|
|
565
|
+
const found = selectIndex(parsed, criteria);
|
|
566
|
+
const entries = topLevelEntries(lines);
|
|
567
|
+
if (entries === null) {
|
|
568
|
+
// `[]` is how DSH writes a profile patch with no rows yet: the one flow form adopted.
|
|
569
|
+
const content = lines.filter(line => !isBlank(line.body) && !isComment(line.body));
|
|
570
|
+
if (found >= 0 || parsed.length !== 0 || content.length !== 1 || content[0]!.body.trimEnd() !== "[]") return null;
|
|
571
|
+
return { kind: "missing", criteria, rest, insertAt: content[0]!.start, replaceEnd: content[0]!.end };
|
|
572
|
+
}
|
|
573
|
+
if (entries.length !== parsed.length) return null;
|
|
574
|
+
if (found < 0) return { kind: "missing", criteria, rest, insertAt: text.length, replaceEnd: null };
|
|
575
|
+
const element = parsed[found];
|
|
576
|
+
const { start, end } = entries[found]!;
|
|
577
|
+
const source = entrySource(text, lines, start, end);
|
|
578
|
+
if (!isPlainRecord(element) || source === null) return null;
|
|
579
|
+
return {
|
|
580
|
+
kind: "found",
|
|
581
|
+
criteria,
|
|
582
|
+
rest,
|
|
583
|
+
startOffset: lines[start]!.start,
|
|
584
|
+
endOffset: end < lines.length ? lines[end]!.start : text.length,
|
|
585
|
+
source,
|
|
586
|
+
parsed: element,
|
|
587
|
+
siblings: entries.length - 1,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function spliceEntry(text: string, entry: Extract<SequenceEntry, { kind: "found" }>, replacement: string): string {
|
|
592
|
+
return `${text.slice(0, entry.startOffset)}${replacement}${text.slice(entry.endOffset)}`;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function upsertSequenceEntry(text: string, parsed: unknown, path: readonly string[], value: unknown): string | null {
|
|
596
|
+
const entry = sequenceEntry(text, parsed, path);
|
|
597
|
+
if (entry === null) return null;
|
|
598
|
+
const eol = lineEnding(text);
|
|
599
|
+
if (entry.kind === "found") {
|
|
600
|
+
if (entry.rest.length === 0) return null;
|
|
601
|
+
const patched = upsertSource(entry.source, entry.parsed, entry.rest, value);
|
|
602
|
+
// The `- ` belongs to the entry's first line; a patch that rewrote that line cannot take it back.
|
|
603
|
+
if (patched === null || firstLine(patched) !== firstLine(entry.source)) return null;
|
|
604
|
+
return preserveFinalNewline(spliceEntry(text, entry, reindentEntry(patched)), text, eol);
|
|
605
|
+
}
|
|
606
|
+
// The same element `setPath` seeds: the selector's own fields, then the fragment beneath them.
|
|
607
|
+
const seeded = Object.fromEntries(entry.criteria.map(criterion => [criterion.field, criterion.value]));
|
|
608
|
+
const element = entry.rest.length === 0 ? value : { ...seeded, ...nestedValue(entry.rest, value) };
|
|
609
|
+
if (!isPlainRecord(element)) return null;
|
|
610
|
+
const item = reindentEntry(rendered(element, 0, eol));
|
|
611
|
+
if (entry.replaceEnd !== null) {
|
|
612
|
+
return preserveFinalNewline(`${text.slice(0, entry.insertAt)}${item}${text.slice(entry.replaceEnd)}`, text, eol);
|
|
613
|
+
}
|
|
614
|
+
const prefix = text.length > 0 && !text.endsWith("\n") ? eol : "";
|
|
615
|
+
return preserveFinalNewline(`${text}${prefix}${item}`, text, eol);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function removeSequenceEntryPath(
|
|
619
|
+
text: string,
|
|
620
|
+
parsed: unknown,
|
|
621
|
+
path: readonly string[],
|
|
622
|
+
requireEmpty: boolean,
|
|
623
|
+
): string | null {
|
|
624
|
+
const entry = sequenceEntry(text, parsed, path);
|
|
625
|
+
if (entry === null || entry.kind !== "found") return null;
|
|
626
|
+
if (entry.rest.length > 0) {
|
|
627
|
+
const patched = removeExactPath(entry.source, entry.rest, requireEmpty);
|
|
628
|
+
if (patched === null || firstLine(patched) !== firstLine(entry.source)) return null;
|
|
629
|
+
return spliceEntry(text, entry, reindentEntry(patched));
|
|
630
|
+
}
|
|
631
|
+
if (requireEmpty) {
|
|
632
|
+
// A pruned entry may still hold what its selector seeded, and nothing else.
|
|
633
|
+
const seededOnly = Object.keys(entry.parsed).length === entry.criteria.length
|
|
634
|
+
&& selectIndex([entry.parsed], entry.criteria) === 0;
|
|
635
|
+
if (!seededOnly || sourceLines(entry.source).some(line => hasInlineComment(line.body) || isComment(line.body))) {
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// A list left with no entries goes back to the `[]` DSH writes for an empty one.
|
|
640
|
+
const eol = lineEnding(text);
|
|
641
|
+
return preserveFinalNewline(spliceEntry(text, entry, entry.siblings === 0 ? `[]${eol}` : ""), text, eol);
|
|
642
|
+
}
|
|
643
|
+
|
|
470
644
|
function removeExactPath(text: string, path: readonly string[], requireEmpty: boolean): string | null {
|
|
471
645
|
let parsed: unknown;
|
|
472
646
|
try {
|
|
@@ -474,6 +648,7 @@ function removeExactPath(text: string, path: readonly string[], requireEmpty: bo
|
|
|
474
648
|
} catch {
|
|
475
649
|
return null;
|
|
476
650
|
}
|
|
651
|
+
if (leadingSelector(path) !== null) return removeSequenceEntryPath(text, parsed, path, requireEmpty);
|
|
477
652
|
const located = locatePath(text, parsed, path);
|
|
478
653
|
if (located === null || located.kind !== "existing") return null;
|
|
479
654
|
const { lines, index, endIndex } = located.entry;
|
|
@@ -506,7 +681,7 @@ function planSourceRemoval(
|
|
|
506
681
|
next = pruned;
|
|
507
682
|
prunedContainers.push(encoded);
|
|
508
683
|
}
|
|
509
|
-
return { text: next, prunedContainers };
|
|
684
|
+
return { text: preserveFinalNewline(next, text, lineEnding(text)), prunedContainers };
|
|
510
685
|
}
|
|
511
686
|
|
|
512
687
|
/** Containers whose source ranges are still empty and safe to prune. */
|
|
@@ -560,6 +735,11 @@ export function yamlFragmentUnsupportedStyle(text: string, path: readonly string
|
|
|
560
735
|
} catch {
|
|
561
736
|
return false;
|
|
562
737
|
}
|
|
738
|
+
if (leadingSelector(path) !== null) {
|
|
739
|
+
const entry = sequenceEntry(text, parsed, path);
|
|
740
|
+
return entry?.kind === "found" && entry.rest.length > 0
|
|
741
|
+
&& locatePath(entry.source, entry.parsed, entry.rest)?.kind === "unsupported-style";
|
|
742
|
+
}
|
|
563
743
|
return locatePath(text, parsed, path)?.kind === "unsupported-style";
|
|
564
744
|
}
|
|
565
745
|
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "../clients/config-export";
|
|
15
15
|
import { canonicalContribution, fingerprint, semanticContribution, type OwnershipRecord } from "./ownership";
|
|
16
16
|
import { readPath } from "./merge";
|
|
17
|
+
import { serializeDocument } from "./serialize";
|
|
17
18
|
|
|
18
19
|
type JsonObject = Record<string, unknown>;
|
|
19
20
|
|
|
@@ -51,6 +52,71 @@ export function isHermesAffinityUpgrade(
|
|
|
51
52
|
&& fingerprint(semanticContribution(predecessor)) === record.semanticBlockFingerprint);
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
function removeDroidNormalizationFields(row: Record<string, unknown>): boolean {
|
|
56
|
+
let projected = false;
|
|
57
|
+
if (Object.hasOwn(row, "id")) {
|
|
58
|
+
delete row.id;
|
|
59
|
+
projected = true;
|
|
60
|
+
}
|
|
61
|
+
if (Object.hasOwn(row, "index")) {
|
|
62
|
+
delete row.index;
|
|
63
|
+
projected = true;
|
|
64
|
+
}
|
|
65
|
+
return projected;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function projectObservedDroidContribution(
|
|
69
|
+
contribution: ManagedContribution,
|
|
70
|
+
): ManagedContribution {
|
|
71
|
+
if (contribution.clientId !== "droid") return contribution;
|
|
72
|
+
let projected = false;
|
|
73
|
+
const fragments = contribution.fragments.map(fragment => {
|
|
74
|
+
if (
|
|
75
|
+
fragment.path.length !== 2
|
|
76
|
+
|| fragment.path[0] !== "customModels"
|
|
77
|
+
|| !isObject(fragment.value)
|
|
78
|
+
) return fragment;
|
|
79
|
+
const value = { ...fragment.value };
|
|
80
|
+
if (!removeDroidNormalizationFields(value)) return fragment;
|
|
81
|
+
projected = true;
|
|
82
|
+
return { ...fragment, value };
|
|
83
|
+
});
|
|
84
|
+
return projected ? { ...contribution, fragments } : contribution;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function droidNormalizedContributionMatchesRecord(
|
|
88
|
+
observed: ManagedContribution,
|
|
89
|
+
record: OwnershipRecord,
|
|
90
|
+
): boolean {
|
|
91
|
+
const projectedObserved = projectObservedDroidContribution(observed);
|
|
92
|
+
return projectedObserved !== observed
|
|
93
|
+
&& (
|
|
94
|
+
fingerprint(canonicalContribution(projectedObserved)) === record.blockFingerprint
|
|
95
|
+
|| (typeof record.semanticBlockFingerprint === "string"
|
|
96
|
+
&& fingerprint(semanticContribution(projectedObserved)) === record.semanticBlockFingerprint)
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function droidNormalizedFileMatchesRecord(
|
|
101
|
+
document: unknown,
|
|
102
|
+
record: OwnershipRecord,
|
|
103
|
+
): boolean {
|
|
104
|
+
if (record.clientId !== "droid") return false;
|
|
105
|
+
try {
|
|
106
|
+
const normalized = structuredClone(document);
|
|
107
|
+
let projected = false;
|
|
108
|
+
for (const path of record.fragmentPaths) {
|
|
109
|
+
if (path.length !== 2 || path[0] !== "customModels") return false;
|
|
110
|
+
const row = readPath(normalized, path);
|
|
111
|
+
if (!isObject(row)) return false;
|
|
112
|
+
if (removeDroidNormalizationFields(row)) projected = true;
|
|
113
|
+
}
|
|
114
|
+
return projected && fingerprint(serializeDocument(normalized, "json")) === record.fileFingerprint;
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
54
120
|
function pathStartsWith(path: readonly string[], prefix: readonly string[]): boolean {
|
|
55
121
|
return prefix.length <= path.length && prefix.every((part, index) => path[index] === part);
|
|
56
122
|
}
|
|
@@ -12,7 +12,7 @@ import { homedir } from "node:os";
|
|
|
12
12
|
import { assertDroidSettingsUnambiguous } from "./droid-settings";
|
|
13
13
|
import { readPath } from "./merge";
|
|
14
14
|
import type { OwnershipRecord } from "./ownership";
|
|
15
|
-
import { join } from "node:path";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
16
|
import {
|
|
17
17
|
ClientPathError,
|
|
18
18
|
buildDroidContribution,
|
|
@@ -52,6 +52,10 @@ import {
|
|
|
52
52
|
zcodeProviderStorePath,
|
|
53
53
|
buildZcodeStoreContribution,
|
|
54
54
|
zcodeStoreSchemaEstablished,
|
|
55
|
+
dshProfilePatchPath,
|
|
56
|
+
dshProfilePatchEstablished,
|
|
57
|
+
buildDshProfilePatchContribution,
|
|
58
|
+
DSH_PROFILE_PROVIDER_PATH,
|
|
55
59
|
type BuildContribution,
|
|
56
60
|
type ConfigFormat,
|
|
57
61
|
kiloConfigPath,
|
|
@@ -96,6 +100,29 @@ export interface IntegrationClientSpec {
|
|
|
96
100
|
format: ConfigFormat;
|
|
97
101
|
establishes: (parsed: unknown) => boolean;
|
|
98
102
|
buildContribution: BuildContribution;
|
|
103
|
+
/** Patch only this leaf of the store, as `sourcePreservingYaml` does for the config file. */
|
|
104
|
+
sourcePreservingYaml?: { path: readonly string[] };
|
|
105
|
+
/**
|
|
106
|
+
* The file whose `<file>.lock` sibling the client's own writer holds while
|
|
107
|
+
* it rewrites the store. Taken after the config file's lock, whenever the
|
|
108
|
+
* store's directory exists.
|
|
109
|
+
*/
|
|
110
|
+
lockFile?: (storePath: string) => string;
|
|
111
|
+
/**
|
|
112
|
+
* A missing store the client already manages. Writing the config file instead would not stay
|
|
113
|
+
* where opencodex records it, so the target is reported as an ineffective write with this
|
|
114
|
+
* remedy rather than silently writing the config file.
|
|
115
|
+
*/
|
|
116
|
+
missingStore?: {
|
|
117
|
+
readsStore: (storePath: string, statKind: (path: string) => string) => boolean;
|
|
118
|
+
/**
|
|
119
|
+
* The document the client writes into a new store. Creating the store with exactly this is
|
|
120
|
+
* the remedy, and it is published on its own so a surface that localizes the remedy text
|
|
121
|
+
* can still name what to write.
|
|
122
|
+
*/
|
|
123
|
+
emptyDocument: string;
|
|
124
|
+
remedy: string;
|
|
125
|
+
};
|
|
99
126
|
};
|
|
100
127
|
/** Patch only this block-map YAML leaf; never re-render the shared file. */
|
|
101
128
|
sourcePreservingYaml?: { path: readonly string[] };
|
|
@@ -297,6 +324,28 @@ export const INTEGRATION_CLIENTS: Record<IntegrationClientId, IntegrationClientS
|
|
|
297
324
|
detectDir: (env = process.env, home = homedir()) => dshHomeDir(env, home),
|
|
298
325
|
sourcePreservingYaml: { path: ["llm-pi-ai", "providers", "opencodex"] },
|
|
299
326
|
writerLock: { suffix: ".lock" },
|
|
327
|
+
/*
|
|
328
|
+
* DSH 0.1.7+ imports `settings.yaml` once into the first profile that boots
|
|
329
|
+
* and renames it; what it reads afterwards is the Desktop profile's patch.
|
|
330
|
+
*/
|
|
331
|
+
currentStore: {
|
|
332
|
+
path: (env = process.env, home = homedir()) => dshProfilePatchPath(env, home),
|
|
333
|
+
format: "yaml",
|
|
334
|
+
establishes: dshProfilePatchEstablished,
|
|
335
|
+
buildContribution: buildDshProfilePatchContribution,
|
|
336
|
+
sourcePreservingYaml: { path: DSH_PROFILE_PROVIDER_PATH },
|
|
337
|
+
// DSH's config editor serializes profile edits on the profile manifest's lock.
|
|
338
|
+
lockFile: store => join(dirname(store), "package.json"),
|
|
339
|
+
// A Desktop profile manifest without its patch is a profile DSH manages. On each startup
|
|
340
|
+
// DSH's importLegacyDocument renames `settings.yaml` to `settings.yaml.imported` and imports
|
|
341
|
+
// it into the active profile, so a block written there would leave opencodex's ownership
|
|
342
|
+
// record pointing at a file that no longer exists. Refuse it and name the remedy instead.
|
|
343
|
+
missingStore: {
|
|
344
|
+
readsStore: (store, statKind) => statKind(join(dirname(store), "package.json")) === "file",
|
|
345
|
+
emptyDocument: "[]",
|
|
346
|
+
remedy: "Create it containing `[]` (the empty patch DSH writes for a new profile), then enable the integration again.",
|
|
347
|
+
},
|
|
348
|
+
},
|
|
300
349
|
},
|
|
301
350
|
mcode: {
|
|
302
351
|
id: "mcode",
|