impel-cli 0.18.15 → 0.18.17
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/README.md +55 -0
- package/docs/experimental-managed-cursor.md +205 -0
- package/package.json +2 -1
- package/src/apps.js +320 -43
- package/src/commands/apps.js +43 -8
- package/src/commands/converge.js +18 -3
- package/src/commands/cursorExperimental.js +192 -0
- package/src/commands/experimental.js +8 -2
- package/src/commands/launch.js +11 -2
- package/src/commands/status.js +3 -2
- package/src/commands/update.js +23 -11
- package/src/cursorLocal.js +1554 -0
- package/src/macSetup.js +20 -38
- package/src/provisioning.js +10 -4
- package/src/skills.js +24 -8
- package/src/updates.js +57 -10
- package/src/vendorCliBinaries.js +121 -0
- package/src/vendorCliVersions.js +7 -0
- package/src/windowsApps.js +64 -19
- package/src/windowsSetup.js +7 -4
package/src/apps.js
CHANGED
|
@@ -37,6 +37,17 @@ const APP_DEFINITIONS = {
|
|
|
37
37
|
executableNames: ["ChatGPT", "Codex"],
|
|
38
38
|
},
|
|
39
39
|
};
|
|
40
|
+
const BUNDLE_ARTIFACT_REMOVE_OPTIONS = Object.freeze({
|
|
41
|
+
recursive: true,
|
|
42
|
+
force: true,
|
|
43
|
+
maxRetries: 5,
|
|
44
|
+
retryDelay: 25,
|
|
45
|
+
});
|
|
46
|
+
const BUNDLE_LOCK_WAIT_MS = 120_000;
|
|
47
|
+
const BUNDLE_LOCK_POLL_MS = 100;
|
|
48
|
+
const BUNDLE_LOCK_SLEEP = new Int32Array(new SharedArrayBuffer(4));
|
|
49
|
+
const MACOS_O_EXLOCK = 0x00000020;
|
|
50
|
+
const portableBundleMutationLocks = new Set();
|
|
40
51
|
|
|
41
52
|
// Vendor updates can change minified renderer contracts without notice. Each
|
|
42
53
|
// CLI release therefore installs and accepts only the exact app builds tested
|
|
@@ -81,17 +92,29 @@ export const PINNED_VENDOR_APPS = Object.freeze({
|
|
|
81
92
|
}),
|
|
82
93
|
}),
|
|
83
94
|
chatgpt: Object.freeze({
|
|
84
|
-
version: "26.
|
|
85
|
-
codexVersion: "0.146.0-alpha.
|
|
95
|
+
version: "26.727.40816",
|
|
96
|
+
codexVersion: "0.146.0-alpha.9.2",
|
|
86
97
|
bundleName: "ChatGPT.app",
|
|
98
|
+
windows: Object.freeze({
|
|
99
|
+
storeProductId: "9PLM9XGG6VKS",
|
|
100
|
+
packageName: "OpenAI.Codex",
|
|
101
|
+
// The Microsoft Store manifest identifies this exact reviewed build.
|
|
102
|
+
// Keep one Windows package version so the manifest contract and local
|
|
103
|
+
// AppX validation cannot silently drift apart again.
|
|
104
|
+
packageVersion: "26.727.6591.0",
|
|
105
|
+
codexVersion: "0.146.0-alpha.9.2",
|
|
106
|
+
publisherId: "2p2nqsd0c76g0",
|
|
107
|
+
executable: "app\\ChatGPT.exe",
|
|
108
|
+
updateManifestUrl: "https://persistent.oaistatic.com/codex-app-prod/windows-store-update.json",
|
|
109
|
+
}),
|
|
87
110
|
downloads: Object.freeze({
|
|
88
111
|
arm64: Object.freeze({
|
|
89
|
-
url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-arm64-26.
|
|
90
|
-
sha256: "
|
|
112
|
+
url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-arm64-26.727.40816.zip",
|
|
113
|
+
sha256: "fdbede9b8a28b5bf3bbf1213fa04291724faa6ed2d1d61bb04853bbdfebce219",
|
|
91
114
|
}),
|
|
92
115
|
x64: Object.freeze({
|
|
93
|
-
url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-x64-26.
|
|
94
|
-
sha256: "
|
|
116
|
+
url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-x64-26.727.40816.zip",
|
|
117
|
+
sha256: "12622d916a3993d61fb52a318dcff7af7449eea5070cdd7e094da29f380e3374",
|
|
95
118
|
}),
|
|
96
119
|
}),
|
|
97
120
|
}),
|
|
@@ -107,6 +130,10 @@ const MAX_REASONING_LEVEL = { effort: "max", description: "Maximum reasoning dep
|
|
|
107
130
|
const ULTRA_REASONING_LEVEL = { effort: "ultra", description: "Maximum reasoning with automatic task delegation" };
|
|
108
131
|
const FAST_SERVICE_TIER = { id: "priority", name: "Fast", description: "1.5x speed, increased usage" };
|
|
109
132
|
const FAST_MODE_AUTH_GATE = /([A-Za-z_$][\w$]*)=([A-Za-z_$][\w$]*)&&!([A-Za-z_$][\w$]*)&&([A-Za-z_$][\w$]*)!=null&&\4\?\.requirements\?\.featureRequirements\?\.fast_mode!==!1/gu;
|
|
133
|
+
const CHATGPT_THREAD_START_PROVIDER = /serviceName:([A-Za-z_$][\w$]*)\.serviceName\?\?this\.options\.defaultServiceName,threadSource:\1\.threadSource===void 0\?`user`:\1\.threadSource/gu;
|
|
134
|
+
const CHATGPT_THREAD_FORK_PROVIDER = /let ([A-Za-z_$][\w$]*)=\{\.\.\.([A-Za-z_$][\w$]*),threadSource:\2\.threadSource===void 0\?`user`:\2\.threadSource\}/gu;
|
|
135
|
+
const CHATGPT_THREAD_RESUME_PROVIDER = /modelProvider:null,cwd:([A-Za-z_$][\w$]*)\?\.cwd\?\?null,approvalPolicy:null,sandbox:null,config:\1\?\.config\?\?null,developerInstructions:\1\?\.developerInstructions\?\?void 0/gu;
|
|
136
|
+
const CHATGPT_STRUCTURED_PROVIDER_FALLBACK = /allowProviderModelFallback:!0/gu;
|
|
110
137
|
const CHATGPT_RESOURCE_ROOT = "linked_vendor_resources_with_patched_asar";
|
|
111
138
|
const CLAUDE_PLAN_USAGE_GUARD = "if(!PSe()){cz();return}if(!Ge().hasOrgPolicyBackend())return;if((r=ar())";
|
|
112
139
|
const IMPEL_CLAUDE_PLAN_USAGE_GUARD = `if(!PSe()){cz();return}${"".padEnd("if(!Ge().hasOrgPolicyBackend())return;".length, " ")}if((r=ar())`;
|
|
@@ -236,7 +263,10 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
|
|
|
236
263
|
// and the tenant manifest so unsupported hosts keep Chat and Code usable.
|
|
237
264
|
// 25: remove the unproven Windows Cowork capability gate from beta.0 profiles
|
|
238
265
|
// and restore the reviewed always-enabled managed policy.
|
|
239
|
-
|
|
266
|
+
// 26: remember the managed provider defaults and move legacy GPT-5.5 profiles
|
|
267
|
+
// onto GPT-5.6 Sol so ChatGPT's compact Work picker does not fall back to its
|
|
268
|
+
// unsupported-selection "Reset to default" treatment.
|
|
269
|
+
export const CURRENT_CONFIG_VERSION = 26;
|
|
240
270
|
|
|
241
271
|
// Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
|
|
242
272
|
// helper rebranding, and signing. A vendored bundle is rebuilt only when this
|
|
@@ -245,7 +275,7 @@ export const CURRENT_CONFIG_VERSION = 25;
|
|
|
245
275
|
// — which is what made every `impel update` re-trigger macOS permission
|
|
246
276
|
// prompts. Bump this ONLY when a code change alters the bytes of a built
|
|
247
277
|
// bundle; leave it alone for changes that don't touch bundle contents.
|
|
248
|
-
export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-
|
|
278
|
+
export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-08-01.1";
|
|
249
279
|
|
|
250
280
|
/** Parse the tenant's install manifest, or null when absent/corrupt. */
|
|
251
281
|
export function readTenantManifest(homeDir = os.homedir(), tenantId = null) {
|
|
@@ -323,7 +353,11 @@ export function bundleIsCurrent(status, {
|
|
|
323
353
|
compatibility.vendorSignaturesPreserved === true
|
|
324
354
|
&& compatibility.resourceRoot === CHATGPT_RESOURCE_ROOT
|
|
325
355
|
&& compatibility.patches?.fastModeForGatewayAuth > 0
|
|
326
|
-
&& compatibility.patches?.
|
|
356
|
+
&& compatibility.patches?.threadStartProviderOverride === 1
|
|
357
|
+
&& compatibility.patches?.threadForkProviderOverride === 1
|
|
358
|
+
&& compatibility.patches?.threadResumeProviderOverride === 1
|
|
359
|
+
&& compatibility.patches?.structuredProviderFallbackDisabled === 2
|
|
360
|
+
&& compatibility.patches?.tenantDisplayNamePreload === 2
|
|
327
361
|
)),
|
|
328
362
|
);
|
|
329
363
|
}
|
|
@@ -679,7 +713,7 @@ export function migrateLegacyClaudeAppSessions(userData, homeDir = os.homedir())
|
|
|
679
713
|
return copied;
|
|
680
714
|
}
|
|
681
715
|
|
|
682
|
-
export async function fetchGatewayModels(config, fetchImpl = fetch) {
|
|
716
|
+
export async function fetchGatewayModels(config, fetchImpl = fetch, { allowEmpty = false } = {}) {
|
|
683
717
|
const controller = new AbortController();
|
|
684
718
|
const timeout = setTimeout(() => controller.abort(), 20000);
|
|
685
719
|
try {
|
|
@@ -697,7 +731,9 @@ export async function fetchGatewayModels(config, fetchImpl = fetch) {
|
|
|
697
731
|
}
|
|
698
732
|
if (!Array.isArray(payload?.data)) throw new Error("response has no model data array");
|
|
699
733
|
const models = payload.data.filter(isGatewayModel);
|
|
700
|
-
if (models.length === 0
|
|
734
|
+
if (models.length === 0 && !(allowEmpty && isExplicitNoSeatCatalog(payload))) {
|
|
735
|
+
throw new Error("gateway returned no supported models");
|
|
736
|
+
}
|
|
701
737
|
return { models, source: "gateway", version: payload.version ?? null };
|
|
702
738
|
} finally {
|
|
703
739
|
clearTimeout(timeout);
|
|
@@ -733,6 +769,7 @@ export function installManagedAppFiles({
|
|
|
733
769
|
target,
|
|
734
770
|
vendorPaths[target] || findVendorApp(target, homeDir),
|
|
735
771
|
]));
|
|
772
|
+
const previousManifest = readTenantManifest(homeDir, config.tenantId);
|
|
736
773
|
const vendorCodexModels = targets.includes("chatgpt")
|
|
737
774
|
? readVendorCodexModels(resolvedVendorPaths.chatgpt)
|
|
738
775
|
: new Map();
|
|
@@ -783,6 +820,7 @@ export function installManagedAppFiles({
|
|
|
783
820
|
vendorCodexModels,
|
|
784
821
|
chatgptInvocations,
|
|
785
822
|
generatedAt,
|
|
823
|
+
previousManifest,
|
|
786
824
|
);
|
|
787
825
|
if (RUNTIME_BRAND.features.sessions) ensureCodexSessionHooks(paths.chatgpt.codexHome, config.tenantId, "codex_desktop");
|
|
788
826
|
}
|
|
@@ -794,7 +832,7 @@ export function installManagedAppFiles({
|
|
|
794
832
|
}
|
|
795
833
|
|
|
796
834
|
writeAtomic(path.join(paths.tenantRoot, "manifest.json"), JSON.stringify({
|
|
797
|
-
schemaVersion:
|
|
835
|
+
schemaVersion: 7,
|
|
798
836
|
configVersion: CURRENT_CONFIG_VERSION,
|
|
799
837
|
modelCatalogVersion: 3,
|
|
800
838
|
gatewayUrl: config.gatewayUrl,
|
|
@@ -807,6 +845,10 @@ export function installManagedAppFiles({
|
|
|
807
845
|
models,
|
|
808
846
|
config,
|
|
809
847
|
),
|
|
848
|
+
modelDefaults: {
|
|
849
|
+
claude: models.find((model) => model.provider === "claude" && model.default)?.id ?? null,
|
|
850
|
+
chatgpt: models.find((model) => model.provider === "codex" && model.default)?.id ?? null,
|
|
851
|
+
},
|
|
810
852
|
models: models.map((model) => model.id),
|
|
811
853
|
updatedAt: generatedAt,
|
|
812
854
|
}, null, 2) + "\n", 0o600);
|
|
@@ -960,7 +1002,6 @@ function installPinnedVendorApp(target, homeDir) {
|
|
|
960
1002
|
homeDir, ".config", RUNTIME_BRAND.cli.configNamespace, "vendor", target, pin.version, pin.bundleName,
|
|
961
1003
|
);
|
|
962
1004
|
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
963
|
-
sweepStaleBundleArtifacts(path.dirname(destination), path.basename(destination));
|
|
964
1005
|
replaceDirectory(destination, extractedApp);
|
|
965
1006
|
return destination;
|
|
966
1007
|
} finally {
|
|
@@ -1141,6 +1182,7 @@ function writeChatGPTConfig(
|
|
|
1141
1182
|
vendorCodexModels,
|
|
1142
1183
|
invocations = null,
|
|
1143
1184
|
generatedAt = new Date().toISOString(),
|
|
1185
|
+
previousManifest = null,
|
|
1144
1186
|
) {
|
|
1145
1187
|
const experimental = crossAppModelsEnabled(config);
|
|
1146
1188
|
const orderedModels = experimental
|
|
@@ -1156,8 +1198,28 @@ function writeChatGPTConfig(
|
|
|
1156
1198
|
const currentToml = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
|
|
1157
1199
|
const configuredModel = readTopLevelTomlString(currentToml, "model");
|
|
1158
1200
|
const defaultModelID = orderedModels.find((model) => model.provider === "codex" && model.default)?.id;
|
|
1159
|
-
const
|
|
1160
|
-
|
|
1201
|
+
const defaultModel = codexModels.find((model) => model.slug === defaultModelID);
|
|
1202
|
+
const previousDefaultModelID = previousManifest?.modelDefaults?.chatgpt;
|
|
1203
|
+
const followsPreviousManagedDefault = (
|
|
1204
|
+
typeof previousDefaultModelID === "string"
|
|
1205
|
+
&& configuredModel === previousDefaultModelID
|
|
1206
|
+
&& configuredModel !== defaultModelID
|
|
1207
|
+
);
|
|
1208
|
+
// Profiles written before the default-model field existed cannot distinguish
|
|
1209
|
+
// an old managed default from an explicit user choice. GPT-5.5 is the one
|
|
1210
|
+
// observed legacy default that ChatGPT 26.727 renders as an unsupported
|
|
1211
|
+
// compact-picker selection; migrate it once, while preserving every other
|
|
1212
|
+
// explicit model choice.
|
|
1213
|
+
const hasLegacyCompactPickerDefault = (
|
|
1214
|
+
previousDefaultModelID == null
|
|
1215
|
+
&& Number(previousManifest?.configVersion ?? 0) < CURRENT_CONFIG_VERSION
|
|
1216
|
+
&& configuredModel === "gpt-5.5"
|
|
1217
|
+
&& defaultModelID === "gpt-5.6-sol"
|
|
1218
|
+
);
|
|
1219
|
+
const selectedModel = (followsPreviousManagedDefault || hasLegacyCompactPickerDefault
|
|
1220
|
+
? defaultModel
|
|
1221
|
+
: codexModels.find((model) => model.slug === configuredModel))
|
|
1222
|
+
|| defaultModel
|
|
1161
1223
|
|| codexModels[0];
|
|
1162
1224
|
if (!selectedModel) throw new Error("no Codex models are available for Impel ChatGPT");
|
|
1163
1225
|
const configuredEffort = readTopLevelTomlString(currentToml, "model_reasoning_effort");
|
|
@@ -1517,6 +1579,12 @@ exec "$NODE" "$CLI" token${tenantArgs}
|
|
|
1517
1579
|
}
|
|
1518
1580
|
|
|
1519
1581
|
function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageName) {
|
|
1582
|
+
return withBundleMutationLock(paths.claude.launcher, () => (
|
|
1583
|
+
writeVendoredClaudeBundleLocked(paths, vendorPath, gatewayUrl, safeStorageName)
|
|
1584
|
+
));
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
function writeVendoredClaudeBundleLocked(paths, vendorPath, gatewayUrl, safeStorageName) {
|
|
1520
1588
|
if (!vendorPath) throw new Error("Claude vendor app is unavailable");
|
|
1521
1589
|
if (!isVendableVendorApp("claude", vendorPath)) {
|
|
1522
1590
|
const bundleId = readBundleIdentifier(vendorPath);
|
|
@@ -1530,9 +1598,8 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageNam
|
|
|
1530
1598
|
if (!executableName) throw new Error(`Claude executable is missing from ${vendorPath}`);
|
|
1531
1599
|
|
|
1532
1600
|
const bundle = paths.claude.launcher;
|
|
1533
|
-
const staging = `${bundle}.tmp-${process.pid}`;
|
|
1534
1601
|
sweepStaleBundleArtifacts(path.dirname(bundle), path.basename(bundle));
|
|
1535
|
-
|
|
1602
|
+
const staging = uniqueBundleArtifactPath(bundle, "tmp");
|
|
1536
1603
|
cloneAppBundle(vendorPath, staging);
|
|
1537
1604
|
try {
|
|
1538
1605
|
const plistPath = path.join(staging, "Contents", "Info.plist");
|
|
@@ -1614,9 +1681,9 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageNam
|
|
|
1614
1681
|
}, null, 2) + "\n", 0o644);
|
|
1615
1682
|
|
|
1616
1683
|
signVendoredApp(staging, vendorExecutable, "Claude", signingIdentity);
|
|
1617
|
-
|
|
1684
|
+
replaceDirectoryUnlocked(bundle, staging);
|
|
1618
1685
|
} catch (error) {
|
|
1619
|
-
|
|
1686
|
+
removeBundleArtifact(staging);
|
|
1620
1687
|
throw error;
|
|
1621
1688
|
}
|
|
1622
1689
|
}
|
|
@@ -1630,6 +1697,12 @@ function claudeUsageCompatibilityEnabled(gatewayUrl) {
|
|
|
1630
1697
|
}
|
|
1631
1698
|
|
|
1632
1699
|
function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl, homeDir) {
|
|
1700
|
+
return withBundleMutationLock(paths.chatgpt.launcher, () => (
|
|
1701
|
+
writeVendoredChatGPTBundleLocked(paths, vendorPath, gatewayUrl, homeDir)
|
|
1702
|
+
));
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
function writeVendoredChatGPTBundleLocked(paths, vendorPath, gatewayUrl, homeDir) {
|
|
1633
1706
|
if (!vendorPath) throw new Error("ChatGPT/Codex vendor app is unavailable");
|
|
1634
1707
|
const definition = APP_DEFINITIONS.chatgpt;
|
|
1635
1708
|
const executableName = definition.executableNames.find((name) => (
|
|
@@ -1638,11 +1711,10 @@ function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl, homeDir) {
|
|
|
1638
1711
|
if (!executableName) throw new Error(`ChatGPT/Codex executable is missing from ${vendorPath}`);
|
|
1639
1712
|
|
|
1640
1713
|
const bundle = paths.chatgpt.launcher;
|
|
1641
|
-
|
|
1714
|
+
sweepStaleBundleArtifacts(path.dirname(bundle), path.basename(bundle));
|
|
1715
|
+
const staging = uniqueBundleArtifactPath(bundle, "tmp");
|
|
1642
1716
|
const vendorBundleName = path.basename(vendorPath);
|
|
1643
1717
|
const vendorBundle = path.join(staging, "Contents", "Resources", vendorBundleName);
|
|
1644
|
-
sweepStaleBundleArtifacts(path.dirname(bundle), path.basename(bundle));
|
|
1645
|
-
fs.rmSync(staging, { recursive: true, force: true });
|
|
1646
1718
|
cloneAppBundle(vendorPath, vendorBundle);
|
|
1647
1719
|
try {
|
|
1648
1720
|
const asarPath = path.join(vendorBundle, "Contents", "Resources", "app.asar");
|
|
@@ -1665,6 +1737,8 @@ function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl, homeDir) {
|
|
|
1665
1737
|
linkVendoredChatGPTResources(staging, vendorBundleName);
|
|
1666
1738
|
const runtimeAsarPath = path.join(staging, "Contents", "Resources", "app.asar");
|
|
1667
1739
|
const fastModePatchCount = patchFastModeAuthGate(runtimeAsarPath);
|
|
1740
|
+
const threadProviderPatches = patchChatGPTThreadRouting(runtimeAsarPath);
|
|
1741
|
+
const structuredFallbackPatchCount = patchChatGPTStructuredProviderFallback(runtimeAsarPath);
|
|
1668
1742
|
const asarHash = asarHeaderHash(runtimeAsarPath);
|
|
1669
1743
|
const vendorExecutable = path.join(vendorBundle, "Contents", "MacOS", executableName);
|
|
1670
1744
|
const signingIdentity = resolveVendoredAppSigningIdentity(vendorExecutable);
|
|
@@ -1709,8 +1783,12 @@ function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl, homeDir) {
|
|
|
1709
1783
|
browserData: paths.chatgpt.browserData,
|
|
1710
1784
|
patches: {
|
|
1711
1785
|
fastModeForGatewayAuth: fastModePatchCount,
|
|
1786
|
+
threadStartProviderOverride: threadProviderPatches.start,
|
|
1787
|
+
threadForkProviderOverride: threadProviderPatches.fork,
|
|
1788
|
+
threadResumeProviderOverride: threadProviderPatches.resume,
|
|
1789
|
+
structuredProviderFallbackDisabled: structuredFallbackPatchCount,
|
|
1712
1790
|
desktopAPIForGatewayAuth: 0,
|
|
1713
|
-
tenantDisplayNamePreload:
|
|
1791
|
+
tenantDisplayNamePreload: 2,
|
|
1714
1792
|
},
|
|
1715
1793
|
vendorSignaturesPreserved: true,
|
|
1716
1794
|
resourceRoot: CHATGPT_RESOURCE_ROOT,
|
|
@@ -1718,9 +1796,9 @@ function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl, homeDir) {
|
|
|
1718
1796
|
}, null, 2) + "\n", 0o644);
|
|
1719
1797
|
|
|
1720
1798
|
signVendoredApp(staging, vendorExecutable, "ChatGPT", signingIdentity);
|
|
1721
|
-
|
|
1799
|
+
replaceDirectoryUnlocked(bundle, staging);
|
|
1722
1800
|
} catch (error) {
|
|
1723
|
-
|
|
1801
|
+
removeBundleArtifact(staging);
|
|
1724
1802
|
throw error;
|
|
1725
1803
|
}
|
|
1726
1804
|
}
|
|
@@ -1931,6 +2009,99 @@ function patchFastModeAuthGate(asarPath) {
|
|
|
1931
2009
|
return applyFixedWidthAsarPatches(asarPath, archive, patches, "ChatGPT/Codex Fast-mode eligibility contract");
|
|
1932
2010
|
}
|
|
1933
2011
|
|
|
2012
|
+
/**
|
|
2013
|
+
* Force every desktop app-server thread request onto the tenant's configured
|
|
2014
|
+
* Impel provider. The pinned renderer passes null for internal metadata turns,
|
|
2015
|
+
* which otherwise lets those turns escape to native OpenAI authentication.
|
|
2016
|
+
*/
|
|
2017
|
+
function patchChatGPTThreadRouting(asarPath) {
|
|
2018
|
+
const archive = fs.readFileSync(asarPath);
|
|
2019
|
+
const source = archive.toString("latin1");
|
|
2020
|
+
const provider = JSON.stringify(RUNTIME_BRAND.cli.providerId);
|
|
2021
|
+
const contracts = [
|
|
2022
|
+
{
|
|
2023
|
+
name: "start",
|
|
2024
|
+
pattern: CHATGPT_THREAD_START_PROVIDER,
|
|
2025
|
+
replacement: (match) => (
|
|
2026
|
+
`modelProvider:${provider},serviceName:${match[1]}.serviceName??this.options.defaultServiceName,`
|
|
2027
|
+
+ `threadSource:${match[1]}.threadSource??\`user\``
|
|
2028
|
+
),
|
|
2029
|
+
},
|
|
2030
|
+
{
|
|
2031
|
+
name: "fork",
|
|
2032
|
+
pattern: CHATGPT_THREAD_FORK_PROVIDER,
|
|
2033
|
+
replacement: (match) => (
|
|
2034
|
+
`let ${match[1]}={...${match[2]},modelProvider:${provider},`
|
|
2035
|
+
+ `threadSource:${match[2]}.threadSource??\`user\`}`
|
|
2036
|
+
),
|
|
2037
|
+
},
|
|
2038
|
+
{
|
|
2039
|
+
name: "resume",
|
|
2040
|
+
pattern: CHATGPT_THREAD_RESUME_PROVIDER,
|
|
2041
|
+
replacement: (match) => (
|
|
2042
|
+
`modelProvider:${provider},cwd:${match[1]}?.cwd??null,approvalPolicy:null,sandbox:null,`
|
|
2043
|
+
+ `config:${match[1]}?.config??null,developerInstructions:${match[1]}?.developerInstructions`
|
|
2044
|
+
),
|
|
2045
|
+
},
|
|
2046
|
+
];
|
|
2047
|
+
const counts = {};
|
|
2048
|
+
const patches = [];
|
|
2049
|
+
for (const contract of contracts) {
|
|
2050
|
+
const matches = [...source.matchAll(contract.pattern)];
|
|
2051
|
+
if (matches.length !== 1) {
|
|
2052
|
+
throw new Error(
|
|
2053
|
+
`ChatGPT/Codex thread-${contract.name} provider patch expected 1 match, found ${matches.length}`,
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
const match = matches[0];
|
|
2057
|
+
const replacement = contract.replacement(match);
|
|
2058
|
+
if (replacement.length > match[0].length) {
|
|
2059
|
+
throw new Error(
|
|
2060
|
+
`ChatGPT/Codex thread-${contract.name} provider replacement no longer fits the vendor ASAR contract`,
|
|
2061
|
+
);
|
|
2062
|
+
}
|
|
2063
|
+
patches.push({
|
|
2064
|
+
offset: match.index,
|
|
2065
|
+
original: match[0],
|
|
2066
|
+
replacement: replacement.padEnd(match[0].length, " "),
|
|
2067
|
+
});
|
|
2068
|
+
counts[contract.name] = 1;
|
|
2069
|
+
}
|
|
2070
|
+
applyFixedWidthAsarPatches(
|
|
2071
|
+
asarPath,
|
|
2072
|
+
archive,
|
|
2073
|
+
patches,
|
|
2074
|
+
"ChatGPT/Codex thread-provider routing contract",
|
|
2075
|
+
);
|
|
2076
|
+
return counts;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
/**
|
|
2080
|
+
* Structured title, description, and classification turns opt into falling
|
|
2081
|
+
* back to another provider in the pinned renderer. Keep those turns fail
|
|
2082
|
+
* closed on Impel by changing the two reviewed boolean literals in place.
|
|
2083
|
+
*/
|
|
2084
|
+
function patchChatGPTStructuredProviderFallback(asarPath) {
|
|
2085
|
+
const archive = fs.readFileSync(asarPath);
|
|
2086
|
+
const source = archive.toString("latin1");
|
|
2087
|
+
const matches = [...source.matchAll(CHATGPT_STRUCTURED_PROVIDER_FALLBACK)];
|
|
2088
|
+
if (matches.length !== 2) {
|
|
2089
|
+
throw new Error(
|
|
2090
|
+
`ChatGPT/Codex structured provider-fallback patch expected 2 matches, found ${matches.length}`,
|
|
2091
|
+
);
|
|
2092
|
+
}
|
|
2093
|
+
return applyFixedWidthAsarPatches(
|
|
2094
|
+
asarPath,
|
|
2095
|
+
archive,
|
|
2096
|
+
matches.map((match) => ({
|
|
2097
|
+
offset: match.index,
|
|
2098
|
+
original: match[0],
|
|
2099
|
+
replacement: "allowProviderModelFallback:!1",
|
|
2100
|
+
})),
|
|
2101
|
+
"ChatGPT/Codex structured provider-fallback contract",
|
|
2102
|
+
);
|
|
2103
|
+
}
|
|
2104
|
+
|
|
1934
2105
|
function applyFixedWidthAsarPatches(asarPath, archive, patches, label) {
|
|
1935
2106
|
if (patches.length === 0) {
|
|
1936
2107
|
throw new Error(`this desktop app version has an unsupported ${label}; update impel-cli before installing it`);
|
|
@@ -2213,6 +2384,7 @@ HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
|
2213
2384
|
VENDOR_APP="$HERE/../Resources/"${shellQuote(vendorBundleName)}
|
|
2214
2385
|
export CODEX_HOME=${shellQuote(paths.chatgpt.codexHome)}
|
|
2215
2386
|
export CODEX_AUTHAPI_BASE_URL=${shellQuote(codexAccountBaseUrl)}
|
|
2387
|
+
export CODEX_SPARKLE_ENABLED=false
|
|
2216
2388
|
unset CODEX_ACCESS_TOKEN CODEX_API_KEY OPENAI_API_KEY OPENAI_BASE_URL
|
|
2217
2389
|
secure_codex_home() {
|
|
2218
2390
|
[ ! -L "$CODEX_HOME" ] || exit 1
|
|
@@ -2236,6 +2408,7 @@ secure_codex_home
|
|
|
2236
2408
|
BROWSER_DATA=${shellQuote(paths.chatgpt.browserData)}
|
|
2237
2409
|
mkdir -p "$BROWSER_DATA"
|
|
2238
2410
|
export IMPEL_APP_DISPLAY_NAME=${shellQuote(paths.chatgpt.displayName)}
|
|
2411
|
+
export IMPEL_APP_BUNDLE_ID=${shellQuote(paths.chatgpt.bundleIdentifier)}
|
|
2239
2412
|
PRELOAD="$HERE/../Resources/impel-chatgpt-preload.cjs"
|
|
2240
2413
|
export NODE_OPTIONS="--require=\\\"$PRELOAD\\\""
|
|
2241
2414
|
# Electron resolves resources from the Impel wrapper when the nested app is
|
|
@@ -2250,13 +2423,14 @@ function vendoredChatGPTDisplayNamePreload() {
|
|
|
2250
2423
|
return `"use strict";
|
|
2251
2424
|
|
|
2252
2425
|
const displayName = process.env.IMPEL_APP_DISPLAY_NAME;
|
|
2253
|
-
|
|
2426
|
+
const bundleIdentifier = process.env.IMPEL_APP_BUNDLE_ID;
|
|
2427
|
+
if (process.type === "browser" && displayName && bundleIdentifier) {
|
|
2254
2428
|
const Module = require("node:module");
|
|
2255
2429
|
const path = require("node:path");
|
|
2256
2430
|
const load = Module._load;
|
|
2257
2431
|
let objc;
|
|
2258
2432
|
let appKit;
|
|
2259
|
-
let
|
|
2433
|
+
let launchServicesIdentified = false;
|
|
2260
2434
|
let activationPolicyRefreshed = false;
|
|
2261
2435
|
|
|
2262
2436
|
const loadNativeBridge = () => {
|
|
@@ -2268,20 +2442,31 @@ if (process.type === "browser" && displayName) {
|
|
|
2268
2442
|
return { objc, appKit };
|
|
2269
2443
|
};
|
|
2270
2444
|
|
|
2271
|
-
const
|
|
2272
|
-
if (
|
|
2445
|
+
const setLaunchServicesApplicationIdentity = () => {
|
|
2446
|
+
if (launchServicesIdentified) return;
|
|
2273
2447
|
try {
|
|
2274
2448
|
const native = loadNativeBridge();
|
|
2275
2449
|
new native.objc.NobjcLibrary(
|
|
2276
2450
|
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/ApplicationServices",
|
|
2277
2451
|
);
|
|
2278
2452
|
const title = native.appKit.NSString.stringWithUTF8String$(displayName);
|
|
2453
|
+
const identifier = native.appKit.NSString.stringWithUTF8String$(bundleIdentifier);
|
|
2279
2454
|
const info = native.appKit.NSMutableDictionary.dictionaryWithDictionary$(
|
|
2280
2455
|
native.appKit.NSBundle.mainBundle().infoDictionary(),
|
|
2281
2456
|
);
|
|
2282
2457
|
for (const key of ["CFBundleName", "CFBundleDisplayName"]) {
|
|
2283
2458
|
info.setObject$forKey$(title, native.appKit.NSString.stringWithUTF8String$(key));
|
|
2284
2459
|
}
|
|
2460
|
+
// The signed vendor executable runs from the nested ChatGPT.app, so
|
|
2461
|
+
// LaunchServices initially checks it in as com.openai.codex. When the
|
|
2462
|
+
// native app is also open, Dock groups both processes under ChatGPT even
|
|
2463
|
+
// though LSDisplayName was already tenant-branded. Check the process in
|
|
2464
|
+
// again with the unique outer-wrapper identity to keep each tenant's
|
|
2465
|
+
// Dock item distinct while preserving the vendor executable signature.
|
|
2466
|
+
info.setObject$forKey$(
|
|
2467
|
+
identifier,
|
|
2468
|
+
native.appKit.NSString.stringWithUTF8String$("CFBundleIdentifier"),
|
|
2469
|
+
);
|
|
2285
2470
|
native.objc.callFunction(
|
|
2286
2471
|
"_LSSetApplicationLaunchServicesServerConnectionStatus",
|
|
2287
2472
|
{ returns: "v", args: ["Q", "@"] },
|
|
@@ -2305,7 +2490,7 @@ if (process.type === "browser" && displayName) {
|
|
|
2305
2490
|
title,
|
|
2306
2491
|
null,
|
|
2307
2492
|
);
|
|
2308
|
-
|
|
2493
|
+
launchServicesIdentified = status === 0;
|
|
2309
2494
|
} catch {
|
|
2310
2495
|
// Keep the signed vendor app usable if LaunchServices changes this
|
|
2311
2496
|
// private-but-established process-title API in a future macOS release.
|
|
@@ -2340,7 +2525,7 @@ if (process.type === "browser" && displayName) {
|
|
|
2340
2525
|
const setName = loaded.app.setName.bind(loaded.app);
|
|
2341
2526
|
setName(displayName);
|
|
2342
2527
|
loaded.app.setName = () => setName(displayName);
|
|
2343
|
-
|
|
2528
|
+
setLaunchServicesApplicationIdentity();
|
|
2344
2529
|
setNativeApplicationMenuTitle();
|
|
2345
2530
|
const setApplicationMenu = loaded.Menu.setApplicationMenu.bind(loaded.Menu);
|
|
2346
2531
|
loaded.Menu.setApplicationMenu = (menu) => {
|
|
@@ -2351,11 +2536,11 @@ if (process.type === "browser" && displayName) {
|
|
|
2351
2536
|
loaded.app.setActivationPolicy("accessory");
|
|
2352
2537
|
setTimeout(() => {
|
|
2353
2538
|
loaded.app.setActivationPolicy("regular");
|
|
2354
|
-
|
|
2355
|
-
|
|
2539
|
+
launchServicesIdentified = false;
|
|
2540
|
+
setLaunchServicesApplicationIdentity();
|
|
2356
2541
|
setApplicationMenu(menu);
|
|
2357
2542
|
setNativeApplicationMenuTitle();
|
|
2358
|
-
},
|
|
2543
|
+
}, 100);
|
|
2359
2544
|
}
|
|
2360
2545
|
setTimeout(setNativeApplicationMenuTitle, 0);
|
|
2361
2546
|
return result;
|
|
@@ -2523,30 +2708,122 @@ export function sweepStaleBundleArtifacts(dir, baseName) {
|
|
|
2523
2708
|
throw error;
|
|
2524
2709
|
}
|
|
2525
2710
|
const prefixes = [`${baseName}.tmp-`, `${baseName}.previous-`];
|
|
2526
|
-
const removed =
|
|
2527
|
-
for (const entry of
|
|
2528
|
-
|
|
2711
|
+
const removed = [];
|
|
2712
|
+
for (const entry of entries.filter((candidate) => prefixes.some((prefix) => candidate.startsWith(prefix)))) {
|
|
2713
|
+
if (removeBundleArtifact(path.join(dir, entry))) removed.push(entry);
|
|
2529
2714
|
}
|
|
2530
2715
|
return removed;
|
|
2531
2716
|
}
|
|
2532
2717
|
|
|
2533
|
-
function
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2718
|
+
function uniqueBundleArtifactPath(destination, kind) {
|
|
2719
|
+
return `${destination}.${kind}-${process.pid}-${crypto.randomBytes(6).toString("hex")}`;
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
function removeBundleArtifact(candidate) {
|
|
2723
|
+
try {
|
|
2724
|
+
fs.rmSync(candidate, BUNDLE_ARTIFACT_REMOVE_OPTIONS);
|
|
2725
|
+
return !fs.existsSync(candidate);
|
|
2726
|
+
} catch {
|
|
2727
|
+
return false;
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
export function withBundleMutationLock(destination, run) {
|
|
2732
|
+
const lockPath = path.join(
|
|
2733
|
+
path.dirname(destination),
|
|
2734
|
+
`.${path.basename(destination)}.impel-install.lock`,
|
|
2735
|
+
);
|
|
2736
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
2737
|
+
|
|
2738
|
+
if (process.platform !== "darwin") {
|
|
2739
|
+
if (portableBundleMutationLocks.has(lockPath)) {
|
|
2740
|
+
throw new Error(`another Impel app update is already replacing ${path.basename(destination)}`);
|
|
2741
|
+
}
|
|
2742
|
+
const descriptor = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_RDWR, 0o600);
|
|
2743
|
+
portableBundleMutationLocks.add(lockPath);
|
|
2744
|
+
try {
|
|
2745
|
+
return run();
|
|
2746
|
+
} finally {
|
|
2747
|
+
portableBundleMutationLocks.delete(lockPath);
|
|
2748
|
+
fs.closeSync(descriptor);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
const deadline = Date.now() + BUNDLE_LOCK_WAIT_MS;
|
|
2753
|
+
let descriptor;
|
|
2754
|
+
while (descriptor === undefined) {
|
|
2755
|
+
try {
|
|
2756
|
+
descriptor = fs.openSync(
|
|
2757
|
+
lockPath,
|
|
2758
|
+
fs.constants.O_CREAT | fs.constants.O_RDWR | fs.constants.O_NONBLOCK | MACOS_O_EXLOCK,
|
|
2759
|
+
0o600,
|
|
2760
|
+
);
|
|
2761
|
+
} catch (error) {
|
|
2762
|
+
if (!["EAGAIN", "EWOULDBLOCK"].includes(error?.code)) throw error;
|
|
2763
|
+
if (Date.now() >= deadline) {
|
|
2764
|
+
throw new Error(`timed out waiting for another Impel app update to replace ${path.basename(destination)}`);
|
|
2765
|
+
}
|
|
2766
|
+
Atomics.wait(BUNDLE_LOCK_SLEEP, 0, 0, BUNDLE_LOCK_POLL_MS);
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
try {
|
|
2770
|
+
return run();
|
|
2771
|
+
} finally {
|
|
2772
|
+
fs.closeSync(descriptor);
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
function replaceDirectoryUnlocked(destination, staging, {
|
|
2777
|
+
removeArtifact = removeBundleArtifact,
|
|
2778
|
+
} = {}) {
|
|
2779
|
+
const backup = fs.existsSync(destination)
|
|
2780
|
+
? uniqueBundleArtifactPath(destination, "previous")
|
|
2781
|
+
: null;
|
|
2782
|
+
if (backup) fs.renameSync(destination, backup);
|
|
2537
2783
|
try {
|
|
2538
2784
|
fs.renameSync(staging, destination);
|
|
2539
|
-
fs.rmSync(backup, { recursive: true, force: true });
|
|
2540
2785
|
} catch (error) {
|
|
2541
|
-
if (!fs.existsSync(destination) && fs.existsSync(backup))
|
|
2786
|
+
if (backup && !fs.existsSync(destination) && fs.existsSync(backup)) {
|
|
2787
|
+
fs.renameSync(backup, destination);
|
|
2788
|
+
}
|
|
2542
2789
|
throw error;
|
|
2543
2790
|
}
|
|
2791
|
+
|
|
2792
|
+
// The new bundle is committed once staging has been renamed into place.
|
|
2793
|
+
// Finder, LaunchServices, or a just-closed helper can briefly keep changing
|
|
2794
|
+
// the old tree and make recursive removal end with ENOTEMPTY. Do not turn
|
|
2795
|
+
// that post-commit housekeeping race into a failed tenant update: the next
|
|
2796
|
+
// sweep will retry any backup that remains.
|
|
2797
|
+
if (backup) {
|
|
2798
|
+
try {
|
|
2799
|
+
removeArtifact(backup);
|
|
2800
|
+
} catch {
|
|
2801
|
+
// Cleanup is best-effort after the committed rename.
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
export function replaceDirectory(destination, staging, options = {}) {
|
|
2807
|
+
return withBundleMutationLock(destination, () => (
|
|
2808
|
+
replaceDirectoryUnlocked(destination, staging, options)
|
|
2809
|
+
));
|
|
2544
2810
|
}
|
|
2545
2811
|
|
|
2546
2812
|
function isGatewayModel(model) {
|
|
2547
2813
|
return model && typeof model.id === "string" && (model.provider === "claude" || model.provider === "codex");
|
|
2548
2814
|
}
|
|
2549
2815
|
|
|
2816
|
+
function isExplicitNoSeatCatalog(payload) {
|
|
2817
|
+
const statuses = payload?.provider_status;
|
|
2818
|
+
return Boolean(statuses
|
|
2819
|
+
&& typeof statuses === "object"
|
|
2820
|
+
&& !Array.isArray(statuses)
|
|
2821
|
+
&& ["claude", "codex"].every((provider) => (
|
|
2822
|
+
statuses[provider]?.state === "no_seat"
|
|
2823
|
+
&& statuses[provider]?.routable === false
|
|
2824
|
+
)));
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2550
2827
|
function writeAtomic(target, contents, mode) {
|
|
2551
2828
|
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
2552
2829
|
if (fs.existsSync(target) && fs.readFileSync(target, "utf8") === contents) {
|