impel-cli 0.17.2 → 0.17.4

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 CHANGED
@@ -311,3 +311,18 @@ run a real package manager, or modify native personal profiles.
311
311
 
312
312
  CI runs on Ubuntu, macOS, and Windows with supported Node versions. See
313
313
  [RELEASING.md](RELEASING.md) for the npm release process.
314
+
315
+ The macOS config-contract job downloads the exact ChatGPT archive declared in
316
+ `PINNED_VENDOR_APPS`, verifies its checksum, signature, app version, and
317
+ embedded Codex version, then validates a deterministic generated profile with
318
+ Codex's strict config loader. A ChatGPT pin update must also update the embedded
319
+ Codex version and the reviewed config fixture when its contract changes.
320
+
321
+ The pinned Claude contract job likewise force-downloads the exact desktop app,
322
+ verifies the embedded Claude Code release manifest, builds the managed bundle
323
+ with ad-hoc signing, and checks deterministic generated desktop and Claude Code
324
+ settings against a reviewed fixture. The manifest-selected Claude Code runtime
325
+ runs `doctor` in bare mode against an isolated profile; CI intercepts the
326
+ `security` command so the check cannot read, reset, unlock, or modify a
327
+ Keychain. A Claude pin update must also update its `claudeCodeVersion` and
328
+ `claudeCodeCommit` pins and reviewed contract fixture.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.17.2",
3
+ "version": "0.17.4",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
package/src/apps.js CHANGED
@@ -42,30 +42,33 @@ const APP_DEFINITIONS = {
42
42
  // with its compatibility patches. Updating a pin requires a new CLI release.
43
43
  export const PINNED_VENDOR_APPS = Object.freeze({
44
44
  claude: Object.freeze({
45
- version: "1.20186.9",
45
+ version: "1.22209.3",
46
+ claudeCodeVersion: "2.1.215",
47
+ claudeCodeCommit: "316ce99628e89900bf0b1328fed3b8fec0c0c92d",
46
48
  bundleName: "Claude.app",
47
49
  downloads: Object.freeze({
48
50
  arm64: Object.freeze({
49
- url: "https://downloads.claude.ai/releases/darwin/universal/1.20186.9/Claude-69f150a4c9316d5c8cd7b9f130ed583d15c0383e.zip",
50
- sha256: "9364648c74bd525aa293a28103eb37262a5553de21c7ca907766d7fc40623015",
51
+ url: "https://downloads.claude.ai/releases/darwin/universal/1.22209.3/Claude-babe11577dfefe3e209c06bd674628d862f0dbae.zip",
52
+ sha256: "b110e0fdb7b2601d80dfdd7893f7811de382de84c4586113e7873e28f17680fc",
51
53
  }),
52
54
  x64: Object.freeze({
53
- url: "https://downloads.claude.ai/releases/darwin/universal/1.20186.9/Claude-69f150a4c9316d5c8cd7b9f130ed583d15c0383e.zip",
54
- sha256: "9364648c74bd525aa293a28103eb37262a5553de21c7ca907766d7fc40623015",
55
+ url: "https://downloads.claude.ai/releases/darwin/universal/1.22209.3/Claude-babe11577dfefe3e209c06bd674628d862f0dbae.zip",
56
+ sha256: "b110e0fdb7b2601d80dfdd7893f7811de382de84c4586113e7873e28f17680fc",
55
57
  }),
56
58
  }),
57
59
  }),
58
60
  chatgpt: Object.freeze({
59
- version: "26.707.72221",
61
+ version: "26.715.31925",
62
+ codexVersion: "0.145.0-alpha.18",
60
63
  bundleName: "ChatGPT.app",
61
64
  downloads: Object.freeze({
62
65
  arm64: Object.freeze({
63
- url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-arm64-26.707.72221.zip",
64
- sha256: "d47a2c0dfb07afec21c415fcec672035a2e90410ecb7c6dc60141518fba524e0",
66
+ url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-arm64-26.715.31925.zip",
67
+ sha256: "a0cded116975c71c723dc8601c07402533b5ff23881d15e52cfb6dc71413e45b",
65
68
  }),
66
69
  x64: Object.freeze({
67
- url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-x64-26.707.72221.zip",
68
- sha256: "cadfdc8e1efa809b021bcf4466ae8c115ffdd280f1a467b518eca8ce8f6717fe",
70
+ url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-x64-26.715.31925.zip",
71
+ sha256: "f18972b893207ff5cbf047599369ce96768c2ace25128a09315058ef0a5cfdcb",
69
72
  }),
70
73
  }),
71
74
  }),
@@ -82,12 +85,10 @@ const ULTRA_REASONING_LEVEL = { effort: "ultra", description: "Maximum reasoning
82
85
  const FAST_SERVICE_TIER = { id: "priority", name: "Fast", description: "1.5x speed, increased usage" };
83
86
  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;
84
87
  const CHATGPT_RESOURCE_ROOT = "linked_vendor_resources_with_patched_asar";
85
- const CLAUDE_PLAN_USAGE_GUARD = "if(!Ze().hasOrgPolicyBackend())return;if((r=fr())";
86
- const IMPEL_CLAUDE_PLAN_USAGE_GUARD = `${"if(!wze())return;".padEnd("if(!Ze().hasOrgPolicyBackend())return;".length, " ")}if((r=fr())`;
87
- const CLAUDE_PLAN_USAGE_REQUEST = 'P.net.fetch(`${ht()}/api/organizations/${o}/usage`,{signal:AbortSignal.timeout(mcr)})';
88
- const IMPEL_CLAUDE_PLAN_USAGE_REQUEST = 'P.net.fetch("https://gateway.useimpel.com/ui/a/u",{headers:{key:dR(Oe()).apiKey}})'.padEnd(CLAUDE_PLAN_USAGE_REQUEST.length, " ");
89
- const CLAUDE_DRAFT_AGENT_MENTIONS = 'agentMentionsEnabled:"draft"===Be&&s';
90
- const IMPEL_CLAUDE_BOUND_AGENT_MENTIONS = 'agentMentionsEnabled:"draft"===Be?s:Le?.agent??!1';
88
+ const CLAUDE_PLAN_USAGE_GUARD = "if(!Vve()){f8();return}if(!et().hasOrgPolicyBackend())return;if((r=yr())";
89
+ const IMPEL_CLAUDE_PLAN_USAGE_GUARD = `if(!Vve()){f8();return}${"".padEnd("if(!et().hasOrgPolicyBackend())return;".length, " ")}if((r=yr())`;
90
+ const CLAUDE_DRAFT_AGENT_MENTIONS = 'agentMentionsEnabled:"draft"===He&&s';
91
+ const IMPEL_CLAUDE_BOUND_AGENT_MENTIONS = 'agentMentionsEnabled:"draft"===He?s:Le?.agent??!1';
91
92
  const CLAUDE_AGENT_MENTION_FILTER = 'return e.filter(e=>"user"===e.source&&(!s||e.name.toLowerCase().includes(s))).slice(0,10)';
92
93
  const IMPEL_CLAUDE_BOUND_AGENT_MENTION_FILTER = 'return e.filter(e=>"user"===e.source&&(!s||e.name.toLowerCase().includes(s))&&("string"!=typeof y||e.name===y)).slice(0,10)';
93
94
  const CLAUDE_AGENT_MENTION_SELECT = 'onSelect:t=>(e(String(t)),Promise.resolve(null))';
@@ -173,7 +174,7 @@ export const CURRENT_CONFIG_VERSION = 16;
173
174
  // — which is what made every `impel update` re-trigger macOS permission
174
175
  // prompts. Bump this ONLY when a code change alters the bytes of a built
175
176
  // bundle; leave it alone for changes that don't touch bundle contents.
176
- export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-07-19.1";
177
+ export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-07-20.1";
177
178
 
178
179
  /** Parse the tenant's install manifest, or null when absent/corrupt. */
179
180
  export function readTenantManifest(homeDir = os.homedir(), tenantId = null) {
@@ -279,12 +280,15 @@ function wrapperLaunchesOnTahoe(status) {
279
280
 
280
281
  function managedClaudeRendererMatchesCompatibility(status, compatibility) {
281
282
  try {
282
- const renderer = compatibility.rendererAssets?.agentMentions;
283
- if (!renderer?.path || !renderer.sha256 || path.isAbsolute(renderer.path)) return false;
283
+ const renderers = compatibility.rendererAssets?.agentMentions;
284
+ if (!Array.isArray(renderers) || renderers.length === 0) return false;
284
285
  const root = path.resolve(status.launcher, "Contents", "Resources", "ion-dist");
285
- const target = path.resolve(root, renderer.path);
286
- if (!target.startsWith(`${root}${path.sep}`)) return false;
287
- return sha256(fs.readFileSync(target)) === renderer.sha256;
286
+ return renderers.every((renderer) => {
287
+ if (!renderer?.path || !renderer.sha256 || path.isAbsolute(renderer.path)) return false;
288
+ const target = path.resolve(root, renderer.path);
289
+ if (!target.startsWith(`${root}${path.sep}`)) return false;
290
+ return sha256(fs.readFileSync(target)) === renderer.sha256;
291
+ });
288
292
  } catch {
289
293
  return false;
290
294
  }
@@ -589,6 +593,7 @@ export function installManagedAppFiles({
589
593
  writeTokenHelperFile = true,
590
594
  claudeUserData = null,
591
595
  chatgptInvocations = null,
596
+ generatedAt = new Date().toISOString(),
592
597
  }) {
593
598
  // true = rebuild every target's bundle; false = configs only; an array
594
599
  // limits the expensive rebuild to just the stale targets.
@@ -621,7 +626,14 @@ export function installManagedAppFiles({
621
626
  ensureClaudeSessionHooks(paths.claude.userData, config.tenantId, "claude_desktop");
622
627
  }
623
628
  else {
624
- writeChatGPTConfig(paths, config, models, vendorCodexModels, chatgptInvocations);
629
+ writeChatGPTConfig(
630
+ paths,
631
+ config,
632
+ models,
633
+ vendorCodexModels,
634
+ chatgptInvocations,
635
+ generatedAt,
636
+ );
625
637
  ensureCodexSessionHooks(paths.chatgpt.codexHome, config.tenantId, "codex_desktop");
626
638
  }
627
639
  // Non-bundle targets are the background-refresh / fast-open path: configs,
@@ -666,7 +678,7 @@ export function installManagedAppFiles({
666
678
  config,
667
679
  ),
668
680
  models: models.map((model) => model.id),
669
- updatedAt: new Date().toISOString(),
681
+ updatedAt: generatedAt,
670
682
  }, null, 2) + "\n", 0o600);
671
683
  return installed;
672
684
  }
@@ -815,10 +827,13 @@ function installPinnedVendorApp(target, homeDir) {
815
827
  }
816
828
  }
817
829
 
818
- export function ensureVendorApp(target, { homeDir = os.homedir() } = {}) {
830
+ export function ensureVendorApp(target, {
831
+ homeDir = os.homedir(),
832
+ forceDownload = false,
833
+ } = {}) {
819
834
  const definition = APP_DEFINITIONS[target];
820
835
  const pin = PINNED_VENDOR_APPS[target];
821
- const current = findVendorApp(target, homeDir);
836
+ const current = forceDownload ? null : findVendorApp(target, homeDir);
822
837
  if (current) return { path: current, action: "existing", note: `verified ${pin.version}` };
823
838
 
824
839
  // Name-collision transparency: e.g. OpenAI's native ChatGPT chat app
@@ -974,7 +989,14 @@ function writeClaudeDefaultApp(paths) {
974
989
  writeAtomic(configPath, JSON.stringify(next, null, 2) + "\n", 0o600);
975
990
  }
976
991
 
977
- function writeChatGPTConfig(paths, config, models, vendorCodexModels, invocations = null) {
992
+ function writeChatGPTConfig(
993
+ paths,
994
+ config,
995
+ models,
996
+ vendorCodexModels,
997
+ invocations = null,
998
+ generatedAt = new Date().toISOString(),
999
+ ) {
978
1000
  const experimental = crossAppModelsEnabled(config);
979
1001
  const orderedModels = experimental
980
1002
  ? [...models.filter((model) => model.provider === "codex"), ...models.filter((model) => model.provider === "claude")]
@@ -1008,7 +1030,7 @@ function writeChatGPTConfig(paths, config, models, vendorCodexModels, invocation
1008
1030
  ? configuredTier
1009
1031
  : (configuredTier === "fast" && supportedTiers.includes("priority") ? "priority" : null);
1010
1032
  const catalog = {
1011
- fetched_at: new Date().toISOString(),
1033
+ fetched_at: generatedAt,
1012
1034
  client_version: "impel-managed-2",
1013
1035
  models: codexModels,
1014
1036
  };
@@ -1209,18 +1231,27 @@ function codexCatalogEntry(model, index, vendorModel) {
1209
1231
  function readVendorCodexModels(vendorPath) {
1210
1232
  const binary = vendorPath && path.join(vendorPath, "Contents", "Resources", "codex");
1211
1233
  if (!binary || !fs.existsSync(binary)) return new Map();
1212
- const env = { ...process.env };
1213
- delete env.CODEX_HOME;
1214
- for (const args of [["debug", "models"], ["debug", "models", "--bundled"]]) {
1215
- const result = spawnSync(binary, args, { encoding: "utf8", env, maxBuffer: 20 * 1024 * 1024 });
1216
- if (result.status !== 0) continue;
1234
+ const codexHome = fs.mkdtempSync(path.join(os.tmpdir(), "impel-vendor-codex-models-"));
1235
+ try {
1236
+ // Vendor enrichment must come only from the exact bundled release. Reading
1237
+ // the operator's native CODEX_HOME here would make generated app configs
1238
+ // depend on unrelated local state and could trigger an online catalog read.
1239
+ const env = { ...process.env, CODEX_HOME: codexHome };
1240
+ const result = spawnSync(binary, ["debug", "models", "--bundled"], {
1241
+ encoding: "utf8",
1242
+ env,
1243
+ maxBuffer: 20 * 1024 * 1024,
1244
+ });
1245
+ if (result.status !== 0) return new Map();
1217
1246
  try {
1218
1247
  const payload = JSON.parse(result.stdout);
1219
1248
  const models = Array.isArray(payload) ? payload : payload.models;
1220
1249
  if (Array.isArray(models)) return new Map(models.map((model) => [model.slug, model]));
1221
1250
  } catch {
1222
- // Fall through to the bundled catalog or the Impel-generated schema.
1251
+ // Fall through to the Impel-generated schema.
1223
1252
  }
1253
+ } finally {
1254
+ fs.rmSync(codexHome, { recursive: true, force: true });
1224
1255
  }
1225
1256
  return new Map();
1226
1257
  }
@@ -1376,7 +1407,7 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageNam
1376
1407
  const vendorExecutable = path.join(macos, executableName);
1377
1408
  const signingIdentity = resolveVendoredAppSigningIdentity(vendorExecutable);
1378
1409
  writeAtomic(path.join(staging, "Contents", "Resources", "impel-compatibility.json"), JSON.stringify({
1379
- schemaVersion: 4,
1410
+ schemaVersion: 5,
1380
1411
  cliVersion: CLI_VERSION,
1381
1412
  bundleFingerprint: BUNDLE_BUILD_FINGERPRINT,
1382
1413
  signingMode: signingModeForIdentity(signingIdentity),
@@ -1397,10 +1428,7 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageNam
1397
1428
  followupBoundAgentChip: agentMentionPatches.chip,
1398
1429
  },
1399
1430
  rendererAssets: {
1400
- agentMentions: {
1401
- path: agentMentionPatches.path,
1402
- sha256: agentMentionPatches.sha256,
1403
- },
1431
+ agentMentions: agentMentionPatches.assets,
1404
1432
  },
1405
1433
  asarHeaderSha256: newAsarHash,
1406
1434
  }, null, 2) + "\n", 0o644);
@@ -1546,42 +1574,63 @@ function patchClaudeFollowupAgentMentions(bundle) {
1546
1574
 
1547
1575
  const ionDist = path.join(bundle, "Contents", "Resources", "ion-dist");
1548
1576
  const contracts = [
1549
- CLAUDE_DRAFT_AGENT_MENTIONS,
1550
- CLAUDE_AGENT_MENTION_FILTER,
1551
- CLAUDE_AGENT_MENTION_SELECT,
1577
+ {
1578
+ key: "enable",
1579
+ original: CLAUDE_DRAFT_AGENT_MENTIONS,
1580
+ replacement: IMPEL_CLAUDE_BOUND_AGENT_MENTIONS,
1581
+ },
1582
+ {
1583
+ key: "filter",
1584
+ original: CLAUDE_AGENT_MENTION_FILTER,
1585
+ replacement: IMPEL_CLAUDE_BOUND_AGENT_MENTION_FILTER,
1586
+ },
1587
+ {
1588
+ key: "chip",
1589
+ original: CLAUDE_AGENT_MENTION_SELECT,
1590
+ replacement: IMPEL_CLAUDE_BOUND_AGENT_MENTION_SELECT,
1591
+ },
1552
1592
  ];
1553
- const candidates = listJavaScriptFiles(ionDist).filter((candidate) => {
1554
- const source = fs.readFileSync(candidate, "utf8");
1555
- return contracts.every((contract) => source.includes(contract));
1556
- });
1557
- if (candidates.length !== 1) {
1558
- throw new Error(
1559
- `Claude follow-up agent-mention renderer expected 1 matching asset, found ${candidates.length}; update impel-cli before installing this desktop app version`,
1560
- );
1593
+ const sources = new Map(listJavaScriptFiles(ionDist).map((candidate) => [
1594
+ candidate,
1595
+ fs.readFileSync(candidate, "utf8"),
1596
+ ]));
1597
+ const matches = new Map(contracts.map(({ key }) => [key, []]));
1598
+ for (const [candidate, source] of sources) {
1599
+ for (const contract of contracts) {
1600
+ const count = exactTextMatchCount(source, contract.original);
1601
+ if (count > 0) matches.get(contract.key).push({ candidate, count });
1602
+ }
1561
1603
  }
1562
-
1563
- const renderer = candidates[0];
1564
- let source = fs.readFileSync(renderer, "utf8");
1565
- const enable = exactTextMatchCount(source, CLAUDE_DRAFT_AGENT_MENTIONS);
1566
- const filter = exactTextMatchCount(source, CLAUDE_AGENT_MENTION_FILTER);
1567
- const chip = exactTextMatchCount(source, CLAUDE_AGENT_MENTION_SELECT);
1568
- for (const [label, count] of [["enable", enable], ["filter", filter], ["chip", chip]]) {
1604
+ for (const contract of contracts) {
1605
+ const count = matches.get(contract.key).reduce((total, match) => total + match.count, 0);
1569
1606
  if (count !== 1) {
1570
- throw new Error(`Claude follow-up agent-mention ${label} patch expected 1 match, found ${count}`);
1607
+ throw new Error(`Claude follow-up agent-mention ${contract.key} patch expected 1 match, found ${count}`);
1608
+ }
1609
+ }
1610
+
1611
+ const assets = [];
1612
+ for (const candidate of [...new Set(
1613
+ [...matches.values()].flatMap((entries) => entries.map(({ candidate: file }) => file)),
1614
+ )].sort()) {
1615
+ const source = fs.readFileSync(candidate, "utf8");
1616
+ const applicable = contracts.filter((contract) => source.includes(contract.original));
1617
+ let patched = source;
1618
+ for (const contract of applicable) {
1619
+ patched = patched.replace(contract.original, contract.replacement);
1571
1620
  }
1621
+ writeAtomic(candidate, patched, fs.statSync(candidate).mode & 0o777);
1622
+ assets.push({
1623
+ path: path.relative(ionDist, candidate).split(path.sep).join("/"),
1624
+ sha256: sha256(Buffer.from(patched, "utf8")),
1625
+ patches: applicable.map(({ key }) => key),
1626
+ });
1572
1627
  }
1573
1628
 
1574
- source = source
1575
- .replace(CLAUDE_DRAFT_AGENT_MENTIONS, IMPEL_CLAUDE_BOUND_AGENT_MENTIONS)
1576
- .replace(CLAUDE_AGENT_MENTION_FILTER, IMPEL_CLAUDE_BOUND_AGENT_MENTION_FILTER)
1577
- .replace(CLAUDE_AGENT_MENTION_SELECT, IMPEL_CLAUDE_BOUND_AGENT_MENTION_SELECT);
1578
- writeAtomic(renderer, source, fs.statSync(renderer).mode & 0o777);
1579
1629
  return {
1580
- enable,
1581
- filter,
1582
- chip,
1583
- path: path.relative(ionDist, renderer).split(path.sep).join("/"),
1584
- sha256: sha256(Buffer.from(source, "utf8")),
1630
+ enable: 1,
1631
+ filter: 1,
1632
+ chip: 1,
1633
+ assets,
1585
1634
  };
1586
1635
  }
1587
1636
 
@@ -1631,23 +1680,6 @@ function patchFixedWidthAsarString(asarPath, original, replacement, label) {
1631
1680
  function patchClaudePlanUsageRequest(asarPath) {
1632
1681
  const archive = fs.readFileSync(asarPath);
1633
1682
  const source = archive.toString("latin1");
1634
- const legacyPatches = [];
1635
- for (
1636
- let offset = source.indexOf(CLAUDE_PLAN_USAGE_REQUEST);
1637
- offset !== -1;
1638
- offset = source.indexOf(CLAUDE_PLAN_USAGE_REQUEST, offset + CLAUDE_PLAN_USAGE_REQUEST.length)
1639
- ) {
1640
- legacyPatches.push({
1641
- offset,
1642
- original: CLAUDE_PLAN_USAGE_REQUEST,
1643
- replacement: IMPEL_CLAUDE_PLAN_USAGE_REQUEST,
1644
- });
1645
- }
1646
- if (legacyPatches.length > 0) {
1647
- return applyFixedWidthAsarPatches(
1648
- asarPath, archive, legacyPatches, "Claude plan-usage gateway request",
1649
- );
1650
- }
1651
1683
  const credentialAccessor = source.match(
1652
1684
  /function ([A-Za-z_$][\w$]*)\(e\)\{var t;return [\s\S]{0,500}?\[custom-3p\] Credentials loaded from managed config/u,
1653
1685
  )?.[1];
@@ -1663,10 +1695,10 @@ function patchClaudePlanUsageRequest(asarPath) {
1663
1695
  return applyFixedWidthAsarPatches(asarPath, archive, [], "Claude plan-usage gateway request");
1664
1696
  }
1665
1697
 
1666
- const requestPattern = /([A-Za-z_$][\w$]*)\.net\.fetch\(`\$\{[A-Za-z_$][\w$]*\(\)\}\/api\/organizations\/\$\{[A-Za-z_$][\w$]*\}\/usage`,\{signal:AbortSignal\.timeout\([A-Za-z_$][\w$]*\)\}\)/gu;
1698
+ const requestPattern = /const ([A-Za-z_$][\w$]*)=`\$\{[A-Za-z_$][\w$]*\(\)\}\/api\/organizations\/\$\{[A-Za-z_$][\w$]*\}\/usage\$\{[A-Za-z_$][\w$]*\?"\?skip_spend=1":""\}`,([A-Za-z_$][\w$]*)=await ([A-Za-z_$][\w$]*)\.net\.fetch\(\1,\{signal:AbortSignal\.timeout\([A-Za-z_$][\w$]*\)\}\)/gu;
1667
1699
  const patches = [...source.matchAll(requestPattern)].map((match) => {
1668
1700
  const original = match[0];
1669
- const replacement = `${match[1]}.net.fetch("https://gateway.useimpel.com/ui/a/u",{headers:{key:${credentialAccessor}(${configAccessor}()).apiKey}})`;
1701
+ const replacement = `const ${match[1]}="https://gateway.useimpel.com/ui/a/u",${match[2]}=await ${match[3]}.net.fetch(${match[1]},{headers:{key:${credentialAccessor}(${configAccessor}()).apiKey}})`;
1670
1702
  if (replacement.length > original.length) {
1671
1703
  throw new Error("Claude plan-usage gateway request replacement no longer fits the vendor ASAR contract");
1672
1704
  }
@@ -1967,7 +1999,11 @@ function vendoredChatGPTWrapperPlist(vendorPath, asarHash, identity, executableN
1967
1999
  <key>CFBundleShortVersionString</key><string>${xmlEscape(bundleVersion(vendorPath) || "1")}</string>
1968
2000
  <key>ElectronAsarIntegrity</key><dict><key>Resources/app.asar</key><dict><key>algorithm</key><string>SHA256</string><key>hash</key><string>${xmlEscape(asarHash)}</string></dict></dict>
1969
2001
  <key>LSMinimumSystemVersion</key><string>12.0</string>
2002
+ <key>NSCalendarsFullAccessUsageDescription</key><string>Allow access so that ChatGPT can read and update your calendar events when you ask.</string>
2003
+ <key>NSCalendarsUsageDescription</key><string>Allow access so that ChatGPT can read and update your calendar events when you ask.</string>
1970
2004
  <key>NSHighResolutionCapable</key><true/>
2005
+ <key>NSRemindersFullAccessUsageDescription</key><string>Allow access so that ChatGPT can read and update your reminders when you ask.</string>
2006
+ <key>NSRemindersUsageDescription</key><string>Allow access so that ChatGPT can read and update your reminders when you ask.</string>
1971
2007
  </dict></plist>
1972
2008
  `;
1973
2009
  }
@@ -1,4 +1,5 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
+ import crypto from "node:crypto";
2
3
  import fs from "node:fs";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
@@ -251,14 +252,47 @@ export function findManagedWindowsChatGPTApp(appsRoot, dependencies = {}) {
251
252
  return null;
252
253
  }
253
254
 
255
+ function sha256File(file) {
256
+ const hash = crypto.createHash("sha256");
257
+ const buffer = Buffer.allocUnsafe(1024 * 1024);
258
+ const descriptor = fs.openSync(file, "r");
259
+ try {
260
+ for (let offset = 0; ; offset += buffer.length) {
261
+ const bytesRead = fs.readSync(descriptor, buffer, 0, buffer.length, offset);
262
+ if (bytesRead === 0) break;
263
+ hash.update(buffer.subarray(0, bytesRead));
264
+ }
265
+ return hash.digest("hex");
266
+ } finally {
267
+ fs.closeSync(descriptor);
268
+ }
269
+ }
270
+
271
+ function windowsChatGPTSourceIdentity(binary) {
272
+ const sourceRoot = path.dirname(binary);
273
+ const files = {
274
+ executable: binary,
275
+ appAsar: path.join(sourceRoot, "resources", "app.asar"),
276
+ codex: path.join(sourceRoot, "resources", "codex.exe"),
277
+ };
278
+ return {
279
+ executableName: path.basename(binary),
280
+ sha256: Object.fromEntries(
281
+ Object.entries(files).map(([name, file]) => [name, sha256File(file)]),
282
+ ),
283
+ };
284
+ }
285
+
254
286
  export function windowsChatGPTStageIsCurrent(binary, appsRoot) {
255
287
  if (!binary || !findManagedWindowsChatGPTApp(appsRoot)) return false;
256
288
  try {
257
289
  const record = JSON.parse(
258
290
  fs.readFileSync(path.join(windowsChatGPTCacheRoot(appsRoot), "source.json"), "utf8"),
259
291
  );
260
- return path.win32.normalize(record.sourceBinary || "").toLowerCase()
261
- === path.win32.normalize(binary).toLowerCase();
292
+ return record.schemaVersion === 2
293
+ && path.win32.normalize(record.sourceBinary || "").toLowerCase()
294
+ === path.win32.normalize(binary).toLowerCase()
295
+ && JSON.stringify(record.sourceIdentity) === JSON.stringify(windowsChatGPTSourceIdentity(binary));
262
296
  } catch {
263
297
  return false;
264
298
  }
@@ -288,6 +322,7 @@ export function stageWindowsChatGPTApp(binary, appsRoot, dependencies = {}) {
288
322
  if (!io.isFile(binary) || !io.isFile(sourceAsar) || !io.isFile(sourceCodex)) {
289
323
  throw new Error(`unsupported ChatGPT/Codex Windows app layout at ${sourceRoot}`);
290
324
  }
325
+ const sourceIdentity = windowsChatGPTSourceIdentity(binary);
291
326
 
292
327
  const cacheRoot = windowsChatGPTCacheRoot(appsRoot);
293
328
  const destination = windowsManagedChatGPTRoot(appsRoot);
@@ -309,8 +344,9 @@ export function stageWindowsChatGPTApp(binary, appsRoot, dependencies = {}) {
309
344
  try {
310
345
  io.rename(staging, destination);
311
346
  fs.writeFileSync(path.join(cacheRoot, "source.json"), `${JSON.stringify({
312
- schemaVersion: 1,
347
+ schemaVersion: 2,
313
348
  sourceBinary: path.win32.normalize(binary),
349
+ sourceIdentity,
314
350
  stagedAt: new Date().toISOString(),
315
351
  }, null, 2)}\n`, { mode: 0o600 });
316
352
  io.remove(backup);