@bitkyc08/opencodex 2.81.0 → 2.83.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ocx.mjs +166 -73
- package/gui/dist/assets/App-Ba8uG696.js +54 -0
- package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-IImbNPRy.js} +1 -1
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/{index-RsoaAxXY.js → index-BHRZdJuv.js} +13 -13
- package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DiJO6sYY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/cursor/catalog.ts +2 -2
- package/src/adapters/cursor/effort-map.ts +1 -1
- package/src/adapters/devin/live-models.ts +2 -2
- package/src/adapters/google-truncation.ts +22 -0
- package/src/adapters/google.ts +7 -3
- package/src/adapters/ollama-native.ts +18 -14
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +11 -4
- package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
- package/src/adapters/openai-responses/reasoning.ts +11 -1
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/bridge/errors.ts +7 -1
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +22 -20
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-picker.ts +1 -1
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound.ts +77 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- package/src/cli/account-auth.ts +20 -12
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +2 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +5 -0
- package/src/cli/doctor.ts +16 -3
- package/src/cli/index.ts +14 -9
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integrations.ts +16 -3
- package/src/cli/resolve.ts +26 -3
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- package/src/clients/config-export/model-metadata.ts +16 -0
- package/src/clients/config-export/zcode-store.ts +46 -11
- package/src/clients/config-export/zcode.ts +3 -5
- package/src/clients/config-export.ts +1 -1
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/pool-mode-gate.ts +4 -5
- package/src/codex/auth-api/routes.ts +10 -49
- package/src/codex/auth-context.ts +28 -4
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/build-entries.ts +18 -0
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/effort.ts +26 -5
- package/src/codex/catalog/gather-capture.ts +5 -0
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/native-models.ts +1 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/provider-models.ts +68 -35
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/desktop-app/darwin.ts +1 -1
- package/src/codex/desktop-app/handoff.ts +1 -1
- package/src/codex/desktop-app/linux.ts +1 -1
- package/src/codex/desktop-app/lock.ts +1 -1
- package/src/codex/desktop-app/types.ts +1 -1
- package/src/codex/desktop-app/windows.ts +1 -1
- package/src/codex/desktop-app-restart.ts +1 -1
- package/src/codex/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/main-account.ts +6 -7
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -220
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +8 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/generated/compatibility-version.json +375 -227
- package/src/generated/model-metadata.ts +2 -2
- package/src/grok/inject.ts +276 -206
- package/src/grok/reset-coupon-ledger.ts +223 -57
- package/src/grok/reset-coupons.ts +13 -6
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +29 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +18 -2
- package/src/integrations/cursor-detect.ts +1 -1
- package/src/integrations/writer.ts +9 -3
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/errors.ts +25 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/request-execution-budget.ts +8 -0
- package/src/lib/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- package/src/oauth/anthropic-account-refusal.ts +39 -1
- package/src/oauth/anthropic-routing.ts +1 -1
- package/src/oauth/anthropic-send-ownership.ts +3 -3
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/index.ts +7 -3
- package/src/oauth/meta-muse-device.ts +1 -1
- package/src/oauth/store.ts +1 -1
- package/src/oauth/types.ts +1 -1
- package/src/providers/anthropic-fast.ts +1 -1
- package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +21 -0
- package/src/providers/devin-cli-authmode-migration.ts +1 -1
- package/src/providers/devin-provider-merge-migration.ts +1 -1
- package/src/providers/github-copilot-auto.ts +169 -0
- package/src/providers/model-presets.ts +1 -1
- package/src/providers/muse-subscription-usage.ts +1 -1
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota.ts +1 -1
- package/src/providers/registry/entries-core.ts +6 -6
- package/src/providers/registry/entries-extended.ts +4 -0
- package/src/providers/registry/model-seeds.ts +3 -3
- package/src/providers/xai-fast-model.ts +1 -1
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +15 -2
- package/src/responses/state/metrics.ts +8 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +12 -5
- package/src/responses/state.ts +18 -10
- package/src/responses/task-input.ts +17 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +1 -0
- package/src/server/chat-native.ts +28 -12
- package/src/server/claude-messages.ts +43 -6
- package/src/server/inbound-body-admission.ts +25 -7
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/link-listener.ts +30 -17
- package/src/server/index/serve-options.ts +18 -4
- package/src/server/index/spend-ledger-lifecycle.ts +2 -1
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +2 -1
- package/src/server/lifecycle.ts +31 -4
- package/src/server/management/agent-settings-routes.ts +93 -32
- package/src/server/management/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +59 -8
- package/src/server/management/context.ts +2 -2
- package/src/server/management/grok-coupon-routes.ts +277 -52
- package/src/server/management/link-routes.ts +42 -20
- package/src/server/management/route-registry.ts +2 -2
- package/src/server/management/shared.ts +3 -3
- package/src/server/messages-native-oauth.ts +9 -8
- package/src/server/messages-native.ts +49 -13
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +18 -7
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log-failure-attribution.ts +3 -0
- package/src/server/request-log-terminal-status.ts +186 -0
- package/src/server/request-log.ts +15 -89
- package/src/server/responses/adapter-continuation.ts +11 -3
- package/src/server/responses/adapter-dispatch.ts +99 -19
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/compact.ts +3 -2
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +20 -3
- package/src/server/responses/core-combo.ts +22 -14
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-opaque-recovery.ts +5 -5
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +73 -0
- package/src/server/responses/core.ts +9 -9
- package/src/server/responses/empty-completion-guard.ts +18 -2
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/jev-model-invoke.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +24 -2
- package/src/server/responses/passthrough-dispatch.ts +134 -62
- package/src/server/responses/passthrough-execution.ts +3 -3
- package/src/server/responses/request-prepare.ts +4 -0
- package/src/server/responses/request-send-budget.ts +112 -49
- package/src/server/responses/request-transport.ts +125 -5
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +15 -5
- package/src/server/responses/sidecar-execution.ts +30 -16
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/spend-storage-error.ts +33 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +3 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/config.ts +21 -0
- package/src/types/request.ts +10 -0
- package/src/types/wire.ts +1 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- package/src/usage/expected-prices.ts +1 -1
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-7yhoys8a.js +0 -54
package/src/grok/inject.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { constants, copyFileSync, existsSync, readFileSync, statSync } from "node:fs";
|
|
1
|
+
import { constants, copyFileSync, existsSync, readFileSync, lstatSync, statSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { atomicWriteFile } from "../config";
|
|
5
|
+
import { atomicWriteFileNoFollow } from "../config/atomic-write";
|
|
6
|
+
import { assertConfigWriteDestination, publishConfigWrite, withConfigWriteLock, type ConfigWriteLockOutcome } from "../codex/config-write-lock";
|
|
5
7
|
import { applyEol, dominantEol, isLoopbackHostname, providerBaseHost } from "../codex/inject";
|
|
6
8
|
import {
|
|
7
9
|
grokDefaultReasoningEffort,
|
|
@@ -22,7 +24,18 @@ export interface GrokInjectResult {
|
|
|
22
24
|
ok: boolean;
|
|
23
25
|
changed: boolean;
|
|
24
26
|
message: string;
|
|
25
|
-
skippedReason?: "no-grok-home" | "orphaned-marker" | "non-loopback";
|
|
27
|
+
skippedReason?: "no-grok-home" | "orphaned-marker" | "non-loopback" | "refresh-only" | "locked" | "unsafe";
|
|
28
|
+
retryable?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function grokConfigLockRefusal(failure: Exclude<ConfigWriteLockOutcome<unknown>, { ok: true }>): GrokInjectResult {
|
|
32
|
+
const retryable = failure.error === "locked";
|
|
33
|
+
return {
|
|
34
|
+
ok: false, changed: false, retryable,
|
|
35
|
+
skippedReason: retryable ? "locked" : "unsafe",
|
|
36
|
+
message: retryable ? "Grok config write refused: another OpenCodex writer is active; retry shortly."
|
|
37
|
+
: "Grok config write refused: its destination or lock cannot be verified; files were preserved.",
|
|
38
|
+
};
|
|
26
39
|
}
|
|
27
40
|
|
|
28
41
|
const BEGIN_MARKER = "# >>> opencodex managed block — do not edit (removed by `ocx stop`) >>>";
|
|
@@ -78,6 +91,18 @@ export function findManagedRegion(content: string): ManagedRegion | null {
|
|
|
78
91
|
return { start: begin.index, end: endMatch.index + endMatch[0].length, orphaned: false };
|
|
79
92
|
}
|
|
80
93
|
|
|
94
|
+
/** A complete managed fence is the existing opt-in required by refresh-only callers. */
|
|
95
|
+
export function grokManagedBlockPresent(grokHome?: string): boolean {
|
|
96
|
+
try {
|
|
97
|
+
const path = join(resolveGrokHome(grokHome), "config.toml");
|
|
98
|
+
if (!lstatSync(path).isFile()) return false;
|
|
99
|
+
const region = findManagedRegion(applyEol(readFileSync(path, "utf8"), "\n"));
|
|
100
|
+
return region != null && !region.orphaned;
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
81
106
|
/**
|
|
82
107
|
* A TOML key segment as it may be spelled in a table header: bare, basic string, or literal
|
|
83
108
|
* string. All three spellings of the same key address the SAME table, so both segments of a
|
|
@@ -1074,12 +1099,27 @@ export function buildGrokManagedBlock(
|
|
|
1074
1099
|
return lines.join("\n");
|
|
1075
1100
|
}
|
|
1076
1101
|
|
|
1102
|
+
class GrokRefreshOnlyRefusal extends Error {}
|
|
1103
|
+
|
|
1104
|
+
function refreshOnlySkipped(): GrokInjectResult {
|
|
1105
|
+
return { ok: true, changed: false, message: "Grok refresh skipped", skippedReason: "refresh-only" };
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function isSymlink(path: string): boolean {
|
|
1109
|
+
try { return lstatSync(path).isSymbolicLink(); }
|
|
1110
|
+
catch (error) {
|
|
1111
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
1112
|
+
throw error;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1077
1116
|
export function injectGrokConfig(
|
|
1078
1117
|
port: number,
|
|
1079
1118
|
models: GrokInjectModel[],
|
|
1080
1119
|
opts: {
|
|
1081
1120
|
grokHome?: string;
|
|
1082
1121
|
hostname?: string;
|
|
1122
|
+
refreshOnly?: { admit: () => boolean };
|
|
1083
1123
|
excluded?: ReadonlySet<string>;
|
|
1084
1124
|
/** Unfiltered known ids used only to distinguish hidden current models from retired ones. */
|
|
1085
1125
|
catalogModelIds?: ReadonlySet<string>;
|
|
@@ -1108,6 +1148,7 @@ export function injectGrokConfig(
|
|
|
1108
1148
|
// at all here; the user configures models manually, outside our fence, where nothing we do
|
|
1109
1149
|
// can clobber their credential.
|
|
1110
1150
|
if (!isLoopbackHostname(opts.hostname)) {
|
|
1151
|
+
if (opts.refreshOnly) return refreshOnlySkipped();
|
|
1111
1152
|
const removed = stripGrokConfig({ ...(opts.grokHome !== undefined ? { grokHome: opts.grokHome } : {}) });
|
|
1112
1153
|
const cleanup = removed.changed
|
|
1113
1154
|
? " Removed the previously generated block, which pointed at a loopback address."
|
|
@@ -1126,130 +1167,154 @@ export function injectGrokConfig(
|
|
|
1126
1167
|
const configPath = join(grokHome, "config.toml");
|
|
1127
1168
|
const backupPath = join(grokHome, "config.toml.bak-opencodex");
|
|
1128
1169
|
try {
|
|
1129
|
-
const
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
// Adopt our own pre-fence entries (#511) BEFORE reserving user aliases, so the stale
|
|
1140
|
-
// duplicate is replaced instead of routed around forever. Runs inside the normalized
|
|
1141
|
-
// window so the user's dominant EOL is still restored below.
|
|
1142
|
-
// Durably marked rows use the full UNFILTERED catalog: explicitly excluded and otherwise
|
|
1143
|
-
// hidden current models must still lose stale generated tables. Ambiguous pre-marker legacy
|
|
1144
|
-
// rows are migrated only when this write emits their replacement. Direct callers that do not
|
|
1145
|
-
// have a separate catalog keep the historical `models` behavior.
|
|
1146
|
-
const catalogModelIds = opts.catalogModelIds ?? new Set(models.map(model => model.id));
|
|
1147
|
-
const emittedModelIds = new Set(models
|
|
1148
|
-
.filter(model => !opts.excluded?.has(model.id))
|
|
1149
|
-
.map(model => model.id));
|
|
1150
|
-
const orphans = findOpencodexOrphans(originalContent, originalRegion)
|
|
1151
|
-
.filter(orphan =>
|
|
1152
|
-
// A provider table carries no alias and no model id: its strict predicate (our key
|
|
1153
|
-
// + loopback + durable marker) is itself the deletion authority, and a leftover
|
|
1154
|
-
// collides with the regenerated provider table (duplicate key).
|
|
1155
|
-
orphan.alias === ""
|
|
1156
|
-
|| (orphan.ownership === "legacy"
|
|
1157
|
-
// A legacy fingerprint is not durable deletion authority. Migrate it only when this
|
|
1158
|
-
// same write will replace the row with a marked managed table.
|
|
1159
|
-
? emittedModelIds.has(orphan.modelId)
|
|
1160
|
-
: catalogModelIds.has(orphan.modelId)
|
|
1161
|
-
|| isDisabledProviderModelId(
|
|
1162
|
-
orphan.modelId,
|
|
1163
|
-
opts.disabledProviderNamespaces,
|
|
1164
|
-
opts.comboPublicModelIds,
|
|
1165
|
-
)));
|
|
1166
|
-
const content = removeOrphanTables(originalContent, orphans);
|
|
1167
|
-
// Removing bytes above the fence MOVES it: recompute rather than adjust arithmetic,
|
|
1168
|
-
// so the splice below cannot cut the file in the wrong place.
|
|
1169
|
-
const region = orphans.length > 0 ? findManagedRegion(content) : originalRegion;
|
|
1170
|
-
|
|
1171
|
-
const buildCandidate = (reservedAliases: ReadonlySet<string>): string => {
|
|
1172
|
-
const block = buildGrokManagedBlock(port, models, opts.hostname, reservedAliases, opts.excluded);
|
|
1173
|
-
let candidate: string;
|
|
1174
|
-
if (region) {
|
|
1175
|
-
candidate = content.slice(0, region.start) + block + content.slice(region.end);
|
|
1176
|
-
} else if (content.length === 0) {
|
|
1177
|
-
candidate = `${block}\n`;
|
|
1178
|
-
} else {
|
|
1179
|
-
// One separator newline preserves the user's original terminator for stripping.
|
|
1180
|
-
candidate = `${content}\n${block}\n`;
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
// Repoint selectors after allocation, so parsing below checks the actual write bytes.
|
|
1184
|
-
const nextManagedModels = managedModelAliases(candidate, findManagedRegion(candidate));
|
|
1185
|
-
const survivors = new Map<string, string>();
|
|
1186
|
-
for (const [alias, modelId] of nextManagedModels) {
|
|
1187
|
-
if (!survivors.has(modelId)) survivors.set(modelId, alias);
|
|
1170
|
+
const locked = withConfigWriteLock(configPath, (held): GrokInjectResult => {
|
|
1171
|
+
const configExisted = existsSync(configPath);
|
|
1172
|
+
const rawContent = configExisted ? readFileSync(configPath, "utf8") : "";
|
|
1173
|
+
const eol = dominantEol(rawContent);
|
|
1174
|
+
const originalContent = applyEol(rawContent, "\n");
|
|
1175
|
+
const originalRegion = findManagedRegion(originalContent);
|
|
1176
|
+
if (opts.refreshOnly && (!configExisted || !originalRegion || originalRegion.orphaned
|
|
1177
|
+
|| !opts.refreshOnly.admit() || isSymlink(configPath) || isSymlink(backupPath))) {
|
|
1178
|
+
return refreshOnlySkipped();
|
|
1188
1179
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1180
|
+
// Ambiguous fence: refuse before the sweep, or "outside the region" could mean the
|
|
1181
|
+
// entire file.
|
|
1182
|
+
if (originalRegion?.orphaned) return orphanedMarkerResult("injection");
|
|
1183
|
+
const previousManagedModels = managedModelAliases(originalContent, originalRegion);
|
|
1184
|
+
|
|
1185
|
+
// Adopt our own pre-fence entries (#511) BEFORE reserving user aliases, so the stale
|
|
1186
|
+
// duplicate is replaced instead of routed around forever. Runs inside the normalized
|
|
1187
|
+
// window so the user's dominant EOL is still restored below.
|
|
1188
|
+
// Durably marked rows use the full UNFILTERED catalog: explicitly excluded and otherwise
|
|
1189
|
+
// hidden current models must still lose stale generated tables. Ambiguous pre-marker legacy
|
|
1190
|
+
// rows are migrated only when this write emits their replacement. Direct callers that do not
|
|
1191
|
+
// have a separate catalog keep the historical `models` behavior.
|
|
1192
|
+
const catalogModelIds = opts.catalogModelIds ?? new Set(models.map(model => model.id));
|
|
1193
|
+
const emittedModelIds = new Set(models
|
|
1194
|
+
.filter(model => !opts.excluded?.has(model.id))
|
|
1195
|
+
.map(model => model.id));
|
|
1196
|
+
const orphans = findOpencodexOrphans(originalContent, originalRegion)
|
|
1197
|
+
.filter(orphan =>
|
|
1198
|
+
// A provider table carries no alias and no model id: its strict predicate (our key
|
|
1199
|
+
// + loopback + durable marker) is itself the deletion authority, and a leftover
|
|
1200
|
+
// collides with the regenerated provider table (duplicate key).
|
|
1201
|
+
orphan.alias === ""
|
|
1202
|
+
|| (orphan.ownership === "legacy"
|
|
1203
|
+
// A legacy fingerprint is not durable deletion authority. Migrate it only when this
|
|
1204
|
+
// same write will replace the row with a marked managed table.
|
|
1205
|
+
? emittedModelIds.has(orphan.modelId)
|
|
1206
|
+
: catalogModelIds.has(orphan.modelId)
|
|
1207
|
+
|| isDisabledProviderModelId(
|
|
1208
|
+
orphan.modelId,
|
|
1209
|
+
opts.disabledProviderNamespaces,
|
|
1210
|
+
opts.comboPublicModelIds,
|
|
1211
|
+
)));
|
|
1212
|
+
const content = removeOrphanTables(originalContent, orphans);
|
|
1213
|
+
// Removing bytes above the fence MOVES it: recompute rather than adjust arithmetic,
|
|
1214
|
+
// so the splice below cannot cut the file in the wrong place.
|
|
1215
|
+
const region = orphans.length > 0 ? findManagedRegion(content) : originalRegion;
|
|
1216
|
+
|
|
1217
|
+
const buildCandidate = (reservedAliases: ReadonlySet<string>): string => {
|
|
1218
|
+
const block = buildGrokManagedBlock(port, models, opts.hostname, reservedAliases, opts.excluded);
|
|
1215
1219
|
let candidate: string;
|
|
1216
|
-
|
|
1217
|
-
candidate =
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1220
|
+
if (region) {
|
|
1221
|
+
candidate = content.slice(0, region.start) + block + content.slice(region.end);
|
|
1222
|
+
} else if (content.length === 0) {
|
|
1223
|
+
candidate = `${block}\n`;
|
|
1224
|
+
} else {
|
|
1225
|
+
// One separator newline preserves the user's original terminator for stripping.
|
|
1226
|
+
candidate = `${content}\n${block}\n`;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// Repoint selectors after allocation, so parsing below checks the actual write bytes.
|
|
1230
|
+
const nextManagedModels = managedModelAliases(candidate, findManagedRegion(candidate));
|
|
1231
|
+
const survivors = new Map<string, string>();
|
|
1232
|
+
for (const [alias, modelId] of nextManagedModels) {
|
|
1233
|
+
if (!survivors.has(modelId)) survivors.set(modelId, alias);
|
|
1221
1234
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1235
|
+
const replacements = new Map<string, string | null>();
|
|
1236
|
+
for (const removed of [
|
|
1237
|
+
...orphans.filter(orphan => orphan.alias !== "")
|
|
1238
|
+
.map(orphan => ({ alias: orphan.alias, modelId: orphan.modelId })),
|
|
1239
|
+
...[...previousManagedModels].map(([alias, modelId]) => ({ alias, modelId })),
|
|
1240
|
+
]) {
|
|
1241
|
+
if (nextManagedModels.get(removed.alias) === removed.modelId) continue;
|
|
1242
|
+
const replacement = survivors.get(removed.modelId) ?? null;
|
|
1243
|
+
if (replacement !== removed.alias) replacements.set(removed.alias, replacement);
|
|
1227
1244
|
}
|
|
1245
|
+
return rewriteAliasReferences(candidate, replacements);
|
|
1228
1246
|
};
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1247
|
+
|
|
1248
|
+
const userContent = region ? content.slice(0, region.start) + content.slice(region.end) : content;
|
|
1249
|
+
let userContentValid = false;
|
|
1250
|
+
try {
|
|
1251
|
+
Bun.TOML.parse(userContent);
|
|
1252
|
+
userContentValid = true;
|
|
1253
|
+
} catch {
|
|
1254
|
+
// Preserve the old, conservative reservation and write behavior for malformed user TOML.
|
|
1255
|
+
}
|
|
1256
|
+
let nextContent: string;
|
|
1257
|
+
if (!userContentValid) {
|
|
1258
|
+
nextContent = buildCandidate(userModelAliases(content, region, true));
|
|
1259
|
+
} else {
|
|
1260
|
+
const validCandidate = (includeNested: boolean): string | null => {
|
|
1261
|
+
let candidate: string;
|
|
1262
|
+
try {
|
|
1263
|
+
candidate = buildCandidate(userModelAliases(content, region, includeNested));
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
if (error instanceof Error && error.message === "Grok config rewrite refused: Bun could not parse the TOML document safely.") return null;
|
|
1266
|
+
throw error;
|
|
1267
|
+
}
|
|
1268
|
+
try {
|
|
1269
|
+
Bun.TOML.parse(applyEol(candidate, eol));
|
|
1270
|
+
return candidate;
|
|
1271
|
+
} catch {
|
|
1272
|
+
return null;
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
nextContent = validCandidate(false) ?? validCandidate(true) ?? "";
|
|
1276
|
+
if (nextContent === "") {
|
|
1277
|
+
throw new Error("Grok config injection refused: neither alias choice produced valid TOML after model-reference rewriting.");
|
|
1278
|
+
}
|
|
1232
1279
|
}
|
|
1233
|
-
}
|
|
1234
1280
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1281
|
+
const output = applyEol(nextContent, eol);
|
|
1282
|
+
if (output === rawContent) {
|
|
1283
|
+
return { ok: true, changed: false, message: "Grok config already contains the current opencodex managed block." };
|
|
1284
|
+
}
|
|
1285
|
+
// Back up before a first-time fence write AND before any sweep, since adopting an orphan
|
|
1286
|
+
// deletes a table the user has in their file. Previously the adjacent-orphan layout got a
|
|
1287
|
+
// backup only as a side effect of the fence being destroyed (which made `region` falsy);
|
|
1288
|
+
// preserving the fence must not silently drop that safety net.
|
|
1289
|
+
if (opts.refreshOnly) {
|
|
1290
|
+
const refreshOnly = opts.refreshOnly;
|
|
1291
|
+
publishConfigWrite(configPath, held, (destination, hooks) => atomicWriteFileNoFollow(destination, output, undefined, {
|
|
1292
|
+
...hooks,
|
|
1293
|
+
validateBeforeRename: () => {
|
|
1294
|
+
try {
|
|
1295
|
+
hooks.validateBeforeRename?.(destination);
|
|
1296
|
+
if (!refreshOnly.admit() || !lstatSync(configPath).isFile()
|
|
1297
|
+
|| readFileSync(configPath, "utf8") !== rawContent) throw new Error();
|
|
1298
|
+
} catch { throw new GrokRefreshOnlyRefusal(); }
|
|
1299
|
+
},
|
|
1300
|
+
}));
|
|
1301
|
+
} else {
|
|
1302
|
+
const destination = assertConfigWriteDestination(configPath, held);
|
|
1303
|
+
if (configExisted && (!region || orphans.length > 0)) copyBackupOnce(destination, backupPath);
|
|
1304
|
+
publishConfigWrite(configPath, held, (target, hooks) => atomicWriteFile(target, output, undefined, hooks));
|
|
1305
|
+
}
|
|
1306
|
+
return {
|
|
1307
|
+
ok: true,
|
|
1308
|
+
changed: true,
|
|
1309
|
+
message: region
|
|
1310
|
+
? "Updated the opencodex managed block in Grok config."
|
|
1311
|
+
: "Added the opencodex managed block to Grok config.",
|
|
1312
|
+
};
|
|
1313
|
+
});
|
|
1314
|
+
if (!locked.ok) return grokConfigLockRefusal(locked);
|
|
1315
|
+
return locked.value;
|
|
1252
1316
|
} catch (error) {
|
|
1317
|
+
if (error instanceof GrokRefreshOnlyRefusal) return refreshOnlySkipped();
|
|
1253
1318
|
return errorResult("inject", error);
|
|
1254
1319
|
}
|
|
1255
1320
|
}
|
|
@@ -1271,95 +1336,100 @@ export function stripGrokConfig(opts: { grokHome?: string } = {}): GrokInjectRes
|
|
|
1271
1336
|
}
|
|
1272
1337
|
|
|
1273
1338
|
try {
|
|
1274
|
-
const
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1339
|
+
const locked = withConfigWriteLock(configPath, (held): GrokInjectResult => {
|
|
1340
|
+
const rawContent = readFileSync(configPath, "utf8");
|
|
1341
|
+
const eol = dominantEol(rawContent);
|
|
1342
|
+
const content = applyEol(rawContent, "\n");
|
|
1343
|
+
const originalRegion = findManagedRegion(content);
|
|
1344
|
+
if (originalRegion?.orphaned) return orphanedMarkerResult("cleanup");
|
|
1345
|
+
|
|
1346
|
+
// Remove the fence against its ORIGINAL offsets first. A pre-fence orphan's span is clamped
|
|
1347
|
+
// at the fence start and can include the separator newline injection added. Sweeping that
|
|
1348
|
+
// orphan first and then applying this separator undo would remove one additional USER newline.
|
|
1349
|
+
let stripped: string;
|
|
1350
|
+
let orphanCount = 0;
|
|
1351
|
+
if (originalRegion) {
|
|
1352
|
+
const fullOrphans = findOpencodexOrphans(content, originalRegion)
|
|
1353
|
+
.filter(orphan => orphan.ownership === "explicit");
|
|
1354
|
+
let removalEnd = originalRegion.end;
|
|
1355
|
+
if (content.startsWith("\n", removalEnd)) removalEnd += 1;
|
|
1356
|
+
let prefix = content.slice(0, originalRegion.start);
|
|
1357
|
+
const restOfFile = content.slice(removalEnd);
|
|
1358
|
+
// Undo the single separator newline injection added. Two cases, mirroring inject:
|
|
1359
|
+
// "X\n" -> "X\n" + "\n" + block => prefix ends "\n\n", drop one.
|
|
1360
|
+
// "X" -> "X" + "\n" + block => prefix ends "\n" at EOF, drop it.
|
|
1361
|
+
// A block the user has appended content after is left alone: we never shrink their bytes.
|
|
1362
|
+
if (prefix.endsWith("\n\n")) prefix = prefix.slice(0, -1);
|
|
1363
|
+
else if (restOfFile.length === 0 && prefix.endsWith("\n")) prefix = prefix.slice(0, -1);
|
|
1364
|
+
// Keep the old fence boundary while sweeping. Concatenating first would let the last
|
|
1365
|
+
// pre-fence orphan absorb comment-only or bare-key user content appended after the fence.
|
|
1366
|
+
const prefixOrphans = findOpencodexOrphans(prefix, null)
|
|
1367
|
+
.filter(orphan => orphan.ownership === "explicit");
|
|
1368
|
+
const tailOrphans = findOpencodexOrphans(restOfFile, null)
|
|
1369
|
+
.filter(orphan => orphan.ownership === "explicit");
|
|
1370
|
+
const removedAliases = new Set(
|
|
1371
|
+
[...fullOrphans, ...prefixOrphans, ...tailOrphans]
|
|
1372
|
+
.map(orphan => orphan.alias)
|
|
1373
|
+
// Provider orphans carry no alias; their ranges above already removed the table.
|
|
1374
|
+
.filter(alias => alias !== ""),
|
|
1375
|
+
);
|
|
1376
|
+
// The backup must cover every removed TABLE, not just aliased rows: provider-only
|
|
1377
|
+
// orphans carry no alias and would otherwise be swept without any backup.
|
|
1378
|
+
orphanCount = new Set(
|
|
1379
|
+
[...fullOrphans, ...prefixOrphans, ...tailOrphans]
|
|
1380
|
+
.flatMap(orphan => orphanRanges([orphan])),
|
|
1381
|
+
).size;
|
|
1382
|
+
const fullRanges = orphanRanges(fullOrphans);
|
|
1383
|
+
const prefixRanges = [
|
|
1384
|
+
...orphanRanges(prefixOrphans),
|
|
1385
|
+
...fullRanges.filter(range => range.end <= originalRegion.start),
|
|
1386
|
+
];
|
|
1387
|
+
const tailRanges = [
|
|
1388
|
+
...orphanRanges(tailOrphans),
|
|
1389
|
+
...fullRanges
|
|
1390
|
+
.filter(range => range.start >= removalEnd)
|
|
1391
|
+
.map(range => ({ start: range.start - removalEnd, end: range.end - removalEnd })),
|
|
1392
|
+
];
|
|
1393
|
+
// Preserve the original fence as a structural boundary while cleaning references too.
|
|
1394
|
+
// Joining first can re-parent a headerless tail under the last table in `prefix`.
|
|
1395
|
+
stripped = removeAliasReferences(
|
|
1396
|
+
removeTableRanges(prefix, prefixRanges),
|
|
1397
|
+
removedAliases,
|
|
1398
|
+
) + removeAliasReferences(
|
|
1399
|
+
removeTableRanges(restOfFile, tailRanges),
|
|
1400
|
+
removedAliases,
|
|
1401
|
+
false,
|
|
1402
|
+
);
|
|
1403
|
+
} else {
|
|
1404
|
+
// Retired or otherwise non-emitted OpenCodex tables may intentionally remain outside the
|
|
1405
|
+
// fence while the integration is enabled. Teardown owns those strictly identified tables
|
|
1406
|
+
// even after Grok has re-serialized the file and dropped our marker comments.
|
|
1407
|
+
const orphans = findOpencodexOrphans(content, null)
|
|
1408
|
+
.filter(orphan => orphan.ownership === "explicit");
|
|
1409
|
+
if (orphans.length === 0) {
|
|
1410
|
+
return { ok: true, changed: false, message: "No opencodex managed block found in Grok config." };
|
|
1411
|
+
}
|
|
1412
|
+
orphanCount = orphans.length;
|
|
1413
|
+
stripped = removeOrphanTables(content, orphans);
|
|
1414
|
+
stripped = removeAliasReferences(
|
|
1415
|
+
stripped,
|
|
1416
|
+
new Set(orphans.map(orphan => orphan.alias).filter(alias => alias !== "")),
|
|
1417
|
+
);
|
|
1345
1418
|
}
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
stripped
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
? "Removed the opencodex managed block from Grok config."
|
|
1361
|
-
: "Removed stale opencodex-managed model entries from Grok config.",
|
|
1362
|
-
};
|
|
1419
|
+
const destination = assertConfigWriteDestination(configPath, held);
|
|
1420
|
+
if (orphanCount > 0) copyBackupOnce(destination, join(grokHome, "config.toml.bak-opencodex"));
|
|
1421
|
+
publishConfigWrite(configPath, held, (target, hooks) => atomicWriteFile(target, applyEol(stripped, eol), undefined, hooks));
|
|
1422
|
+
|
|
1423
|
+
return {
|
|
1424
|
+
ok: true,
|
|
1425
|
+
changed: true,
|
|
1426
|
+
message: originalRegion
|
|
1427
|
+
? "Removed the opencodex managed block from Grok config."
|
|
1428
|
+
: "Removed stale opencodex-managed model entries from Grok config.",
|
|
1429
|
+
};
|
|
1430
|
+
});
|
|
1431
|
+
if (!locked.ok) return grokConfigLockRefusal(locked);
|
|
1432
|
+
return locked.value;
|
|
1363
1433
|
} catch (error) {
|
|
1364
1434
|
return errorResult("strip", error);
|
|
1365
1435
|
}
|