@bitkyc08/opencodex 2.82.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/gui/dist/assets/{App-Ds_qL7o8.js → App-Ba8uG696.js} +4 -4
- package/gui/dist/assets/{Tray-BqHF5Trd.js → Tray-IImbNPRy.js} +1 -1
- package/gui/dist/assets/{index-D3FBCNiP.js → index-BHRZdJuv.js} +12 -12
- package/gui/dist/assets/{quota-summary-DTBC8n7-.js → quota-summary-DiJO6sYY.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +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/passthrough.ts +2 -0
- package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
- package/src/adapters/openai-responses/reasoning.ts +11 -1
- package/src/bridge/errors.ts +7 -1
- package/src/claude/context-windows.ts +3 -3
- package/src/claude/desktop-picker.ts +1 -1
- package/src/cli/account-auth.ts +20 -12
- package/src/cli/resolve.ts +1 -1
- 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/codex/auth-api/pool-mode-gate.ts +4 -5
- package/src/codex/auth-context.ts +8 -3
- package/src/codex/catalog/native-models.ts +1 -1
- package/src/codex/catalog/provider-models.ts +25 -0
- 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/main-account.ts +6 -7
- package/src/generated/compatibility-version.json +91 -75
- package/src/grok/inject.ts +245 -221
- package/src/grok/reset-coupon-ledger.ts +223 -57
- package/src/grok/reset-coupons.ts +13 -6
- package/src/integrations/cursor-detect.ts +1 -1
- package/src/lib/errors.ts +25 -0
- package/src/lib/redact.ts +2 -2
- package/src/oauth/index.ts +1 -1
- package/src/oauth/meta-muse-device.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/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/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/server/inbound-body-admission.ts +24 -7
- package/src/server/index/link-listener.ts +30 -17
- package/src/server/index/serve-options.ts +8 -2
- package/src/server/index.ts +1 -1
- package/src/server/lifecycle.ts +31 -4
- 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/relay-eager.ts +2 -0
- package/src/server/relay.ts +18 -7
- 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 -118
- package/src/server/responses/adapter-continuation.ts +9 -1
- package/src/server/responses/adapter-dispatch.ts +65 -6
- package/src/server/responses/compact.ts +3 -2
- package/src/server/responses/core-opaque-recovery.ts +5 -5
- package/src/server/responses/core.ts +6 -6
- package/src/server/responses/empty-completion-guard.ts +18 -2
- package/src/server/responses/jev-model-invoke.ts +1 -1
- package/src/server/responses/passthrough-dispatch.ts +115 -60
- package/src/server/responses/passthrough-execution.ts +3 -3
- package/src/server/responses/request-transport.ts +125 -5
- package/src/server/responses/run-turn-execution.ts +13 -5
- package/src/server/responses/sidecar-execution.ts +5 -0
- package/src/server/responses/spend-storage-error.ts +33 -0
- package/src/types/request.ts +5 -0
- package/src/usage/expected-prices.ts +1 -1
package/src/grok/inject.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { atomicWriteFile } from "../config";
|
|
5
5
|
import { atomicWriteFileNoFollow } from "../config/atomic-write";
|
|
6
|
+
import { assertConfigWriteDestination, publishConfigWrite, withConfigWriteLock, type ConfigWriteLockOutcome } from "../codex/config-write-lock";
|
|
6
7
|
import { applyEol, dominantEol, isLoopbackHostname, providerBaseHost } from "../codex/inject";
|
|
7
8
|
import {
|
|
8
9
|
grokDefaultReasoningEffort,
|
|
@@ -23,7 +24,18 @@ export interface GrokInjectResult {
|
|
|
23
24
|
ok: boolean;
|
|
24
25
|
changed: boolean;
|
|
25
26
|
message: string;
|
|
26
|
-
skippedReason?: "no-grok-home" | "orphaned-marker" | "non-loopback" | "refresh-only";
|
|
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
|
+
};
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
const BEGIN_MARKER = "# >>> opencodex managed block — do not edit (removed by `ocx stop`) >>>";
|
|
@@ -1155,145 +1167,152 @@ export function injectGrokConfig(
|
|
|
1155
1167
|
const configPath = join(grokHome, "config.toml");
|
|
1156
1168
|
const backupPath = join(grokHome, "config.toml.bak-opencodex");
|
|
1157
1169
|
try {
|
|
1158
|
-
const
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
// Ambiguous fence: refuse before the sweep, or "outside the region" could mean the
|
|
1168
|
-
// entire file.
|
|
1169
|
-
if (originalRegion?.orphaned) return orphanedMarkerResult("injection");
|
|
1170
|
-
const previousManagedModels = managedModelAliases(originalContent, originalRegion);
|
|
1171
|
-
|
|
1172
|
-
// Adopt our own pre-fence entries (#511) BEFORE reserving user aliases, so the stale
|
|
1173
|
-
// duplicate is replaced instead of routed around forever. Runs inside the normalized
|
|
1174
|
-
// window so the user's dominant EOL is still restored below.
|
|
1175
|
-
// Durably marked rows use the full UNFILTERED catalog: explicitly excluded and otherwise
|
|
1176
|
-
// hidden current models must still lose stale generated tables. Ambiguous pre-marker legacy
|
|
1177
|
-
// rows are migrated only when this write emits their replacement. Direct callers that do not
|
|
1178
|
-
// have a separate catalog keep the historical `models` behavior.
|
|
1179
|
-
const catalogModelIds = opts.catalogModelIds ?? new Set(models.map(model => model.id));
|
|
1180
|
-
const emittedModelIds = new Set(models
|
|
1181
|
-
.filter(model => !opts.excluded?.has(model.id))
|
|
1182
|
-
.map(model => model.id));
|
|
1183
|
-
const orphans = findOpencodexOrphans(originalContent, originalRegion)
|
|
1184
|
-
.filter(orphan =>
|
|
1185
|
-
// A provider table carries no alias and no model id: its strict predicate (our key
|
|
1186
|
-
// + loopback + durable marker) is itself the deletion authority, and a leftover
|
|
1187
|
-
// collides with the regenerated provider table (duplicate key).
|
|
1188
|
-
orphan.alias === ""
|
|
1189
|
-
|| (orphan.ownership === "legacy"
|
|
1190
|
-
// A legacy fingerprint is not durable deletion authority. Migrate it only when this
|
|
1191
|
-
// same write will replace the row with a marked managed table.
|
|
1192
|
-
? emittedModelIds.has(orphan.modelId)
|
|
1193
|
-
: catalogModelIds.has(orphan.modelId)
|
|
1194
|
-
|| isDisabledProviderModelId(
|
|
1195
|
-
orphan.modelId,
|
|
1196
|
-
opts.disabledProviderNamespaces,
|
|
1197
|
-
opts.comboPublicModelIds,
|
|
1198
|
-
)));
|
|
1199
|
-
const content = removeOrphanTables(originalContent, orphans);
|
|
1200
|
-
// Removing bytes above the fence MOVES it: recompute rather than adjust arithmetic,
|
|
1201
|
-
// so the splice below cannot cut the file in the wrong place.
|
|
1202
|
-
const region = orphans.length > 0 ? findManagedRegion(content) : originalRegion;
|
|
1203
|
-
|
|
1204
|
-
const buildCandidate = (reservedAliases: ReadonlySet<string>): string => {
|
|
1205
|
-
const block = buildGrokManagedBlock(port, models, opts.hostname, reservedAliases, opts.excluded);
|
|
1206
|
-
let candidate: string;
|
|
1207
|
-
if (region) {
|
|
1208
|
-
candidate = content.slice(0, region.start) + block + content.slice(region.end);
|
|
1209
|
-
} else if (content.length === 0) {
|
|
1210
|
-
candidate = `${block}\n`;
|
|
1211
|
-
} else {
|
|
1212
|
-
// One separator newline preserves the user's original terminator for stripping.
|
|
1213
|
-
candidate = `${content}\n${block}\n`;
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
// Repoint selectors after allocation, so parsing below checks the actual write bytes.
|
|
1217
|
-
const nextManagedModels = managedModelAliases(candidate, findManagedRegion(candidate));
|
|
1218
|
-
const survivors = new Map<string, string>();
|
|
1219
|
-
for (const [alias, modelId] of nextManagedModels) {
|
|
1220
|
-
if (!survivors.has(modelId)) survivors.set(modelId, alias);
|
|
1221
|
-
}
|
|
1222
|
-
const replacements = new Map<string, string | null>();
|
|
1223
|
-
for (const removed of [
|
|
1224
|
-
...orphans.filter(orphan => orphan.alias !== "")
|
|
1225
|
-
.map(orphan => ({ alias: orphan.alias, modelId: orphan.modelId })),
|
|
1226
|
-
...[...previousManagedModels].map(([alias, modelId]) => ({ alias, modelId })),
|
|
1227
|
-
]) {
|
|
1228
|
-
if (nextManagedModels.get(removed.alias) === removed.modelId) continue;
|
|
1229
|
-
const replacement = survivors.get(removed.modelId) ?? null;
|
|
1230
|
-
if (replacement !== removed.alias) replacements.set(removed.alias, replacement);
|
|
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();
|
|
1231
1179
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
//
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
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);
|
|
1248
1219
|
let candidate: string;
|
|
1249
|
-
|
|
1250
|
-
candidate =
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
|
|
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`;
|
|
1254
1227
|
}
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
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);
|
|
1260
1234
|
}
|
|
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);
|
|
1244
|
+
}
|
|
1245
|
+
return rewriteAliasReferences(candidate, replacements);
|
|
1261
1246
|
};
|
|
1262
|
-
nextContent = validCandidate(false) ?? validCandidate(true) ?? "";
|
|
1263
|
-
if (nextContent === "") {
|
|
1264
|
-
throw new Error("Grok config injection refused: neither alias choice produced valid TOML after model-reference rewriting.");
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
1247
|
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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;
|
|
1280
1262
|
try {
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
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
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
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;
|
|
1297
1316
|
} catch (error) {
|
|
1298
1317
|
if (error instanceof GrokRefreshOnlyRefusal) return refreshOnlySkipped();
|
|
1299
1318
|
return errorResult("inject", error);
|
|
@@ -1317,95 +1336,100 @@ export function stripGrokConfig(opts: { grokHome?: string } = {}): GrokInjectRes
|
|
|
1317
1336
|
}
|
|
1318
1337
|
|
|
1319
1338
|
try {
|
|
1320
|
-
const
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
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
|
+
);
|
|
1391
1418
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
stripped
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
? "Removed the opencodex managed block from Grok config."
|
|
1407
|
-
: "Removed stale opencodex-managed model entries from Grok config.",
|
|
1408
|
-
};
|
|
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;
|
|
1409
1433
|
} catch (error) {
|
|
1410
1434
|
return errorResult("strip", error);
|
|
1411
1435
|
}
|