impel-cli 0.17.3 → 0.17.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.17.3",
3
+ "version": "0.17.5",
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,33 +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",
46
- claudeCodeVersion: "2.1.209",
47
- claudeCodeCommit: "0fe048596fd45e79e99353cbe2c3d1b1ac069568",
45
+ version: "1.22209.3",
46
+ claudeCodeVersion: "2.1.215",
47
+ claudeCodeCommit: "316ce99628e89900bf0b1328fed3b8fec0c0c92d",
48
48
  bundleName: "Claude.app",
49
49
  downloads: Object.freeze({
50
50
  arm64: Object.freeze({
51
- url: "https://downloads.claude.ai/releases/darwin/universal/1.20186.9/Claude-69f150a4c9316d5c8cd7b9f130ed583d15c0383e.zip",
52
- sha256: "9364648c74bd525aa293a28103eb37262a5553de21c7ca907766d7fc40623015",
51
+ url: "https://downloads.claude.ai/releases/darwin/universal/1.22209.3/Claude-babe11577dfefe3e209c06bd674628d862f0dbae.zip",
52
+ sha256: "b110e0fdb7b2601d80dfdd7893f7811de382de84c4586113e7873e28f17680fc",
53
53
  }),
54
54
  x64: Object.freeze({
55
- url: "https://downloads.claude.ai/releases/darwin/universal/1.20186.9/Claude-69f150a4c9316d5c8cd7b9f130ed583d15c0383e.zip",
56
- sha256: "9364648c74bd525aa293a28103eb37262a5553de21c7ca907766d7fc40623015",
55
+ url: "https://downloads.claude.ai/releases/darwin/universal/1.22209.3/Claude-babe11577dfefe3e209c06bd674628d862f0dbae.zip",
56
+ sha256: "b110e0fdb7b2601d80dfdd7893f7811de382de84c4586113e7873e28f17680fc",
57
57
  }),
58
58
  }),
59
59
  }),
60
60
  chatgpt: Object.freeze({
61
- version: "26.707.72221",
62
- codexVersion: "0.144.2",
61
+ version: "26.715.31925",
62
+ codexVersion: "0.145.0-alpha.18",
63
63
  bundleName: "ChatGPT.app",
64
64
  downloads: Object.freeze({
65
65
  arm64: Object.freeze({
66
- url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-arm64-26.707.72221.zip",
67
- sha256: "d47a2c0dfb07afec21c415fcec672035a2e90410ecb7c6dc60141518fba524e0",
66
+ url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-arm64-26.715.31925.zip",
67
+ sha256: "a0cded116975c71c723dc8601c07402533b5ff23881d15e52cfb6dc71413e45b",
68
68
  }),
69
69
  x64: Object.freeze({
70
- url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-x64-26.707.72221.zip",
71
- sha256: "cadfdc8e1efa809b021bcf4466ae8c115ffdd280f1a467b518eca8ce8f6717fe",
70
+ url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-x64-26.715.31925.zip",
71
+ sha256: "f18972b893207ff5cbf047599369ce96768c2ace25128a09315058ef0a5cfdcb",
72
72
  }),
73
73
  }),
74
74
  }),
@@ -85,16 +85,22 @@ const ULTRA_REASONING_LEVEL = { effort: "ultra", description: "Maximum reasoning
85
85
  const FAST_SERVICE_TIER = { id: "priority", name: "Fast", description: "1.5x speed, increased usage" };
86
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;
87
87
  const CHATGPT_RESOURCE_ROOT = "linked_vendor_resources_with_patched_asar";
88
- const CLAUDE_PLAN_USAGE_GUARD = "if(!Ze().hasOrgPolicyBackend())return;if((r=fr())";
89
- const IMPEL_CLAUDE_PLAN_USAGE_GUARD = `${"if(!wze())return;".padEnd("if(!Ze().hasOrgPolicyBackend())return;".length, " ")}if((r=fr())`;
90
- const CLAUDE_PLAN_USAGE_REQUEST = 'P.net.fetch(`${ht()}/api/organizations/${o}/usage`,{signal:AbortSignal.timeout(mcr)})';
91
- 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, " ");
92
- const CLAUDE_DRAFT_AGENT_MENTIONS = 'agentMentionsEnabled:"draft"===Be&&s';
93
- 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';
94
92
  const CLAUDE_AGENT_MENTION_FILTER = 'return e.filter(e=>"user"===e.source&&(!s||e.name.toLowerCase().includes(s))).slice(0,10)';
95
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)';
96
94
  const CLAUDE_AGENT_MENTION_SELECT = 'onSelect:t=>(e(String(t)),Promise.resolve(null))';
97
95
  const IMPEL_CLAUDE_BOUND_AGENT_MENTION_SELECT = 'onSelect:t=>"string"==typeof y?Promise.resolve({chipText:String(t)}):(e(String(t)),Promise.resolve(null))';
96
+ // Claude's Code-disabled screens link Back to "/", but the root router honors
97
+ // its separately persisted last Code mode and immediately returns to the same
98
+ // disabled screen. Route directly to the pinned build's canonical Chat page;
99
+ // its normal mode effect then reconciles both renderer and desktop preference.
100
+ const CLAUDE_CODE_DISABLED_BACK_LINK = 'function Fi(){return e.jsxs("a",{href:"/",className:"inline-flex items-center text-sm text-text-400 hover:text-text-300 outline-none"';
101
+ const IMPEL_CLAUDE_CODE_DISABLED_BACK_LINK = 'function Fi(){return e.jsxs("a",{href:"/chat/new",className:"inline-flex items-center text-sm text-text-400 hover:text-text-300 outline-none"';
102
+ const CLAUDE_CODE_NO_SEAT_BACK_LINK = 'b.jsx(D,{variant:"ghost",href:"/",icon:"ArrowLeft",children:b.jsx(A,{defaultMessage:"Back to Claude",id:"e4M0bJ/pZc"})})';
103
+ const IMPEL_CLAUDE_CODE_NO_SEAT_BACK_LINK = 'b.jsx(D,{variant:"ghost",href:"/chat/new",icon:"ArrowLeft",children:b.jsx(A,{defaultMessage:"Back to Claude",id:"e4M0bJ/pZc"})})';
98
104
  const LEGACY_CLAUDE_SAFE_STORAGE_NAME = "Claude";
99
105
  const CLAUDE_SAFE_STORAGE_METADATA = "safe-storage.json";
100
106
 
@@ -176,7 +182,7 @@ export const CURRENT_CONFIG_VERSION = 16;
176
182
  // — which is what made every `impel update` re-trigger macOS permission
177
183
  // prompts. Bump this ONLY when a code change alters the bytes of a built
178
184
  // bundle; leave it alone for changes that don't touch bundle contents.
179
- export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-07-19.1";
185
+ export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-07-20.2";
180
186
 
181
187
  /** Parse the tenant's install manifest, or null when absent/corrupt. */
182
188
  export function readTenantManifest(homeDir = os.homedir(), tenantId = null) {
@@ -247,6 +253,8 @@ export function bundleIsCurrent(status, {
247
253
  compatibility.patches?.followupBoundAgentEnable === 1
248
254
  && compatibility.patches?.followupBoundAgentFilter === 1
249
255
  && compatibility.patches?.followupBoundAgentChip === 1
256
+ && compatibility.patches?.codeDisabledBack === 1
257
+ && compatibility.patches?.codeNoSeatBack === 1
250
258
  && managedClaudeRendererMatchesCompatibility(status, compatibility)
251
259
  ))
252
260
  && (status.target !== "chatgpt" || (
@@ -282,12 +290,19 @@ function wrapperLaunchesOnTahoe(status) {
282
290
 
283
291
  function managedClaudeRendererMatchesCompatibility(status, compatibility) {
284
292
  try {
285
- const renderer = compatibility.rendererAssets?.agentMentions;
286
- if (!renderer?.path || !renderer.sha256 || path.isAbsolute(renderer.path)) return false;
293
+ const groups = [
294
+ compatibility.rendererAssets?.agentMentions,
295
+ compatibility.rendererAssets?.codeDisabledBack,
296
+ ];
297
+ if (groups.some((renderers) => !Array.isArray(renderers) || renderers.length === 0)) return false;
298
+ const renderers = groups.flat();
287
299
  const root = path.resolve(status.launcher, "Contents", "Resources", "ion-dist");
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;
300
+ return renderers.every((renderer) => {
301
+ if (!renderer?.path || !renderer.sha256 || path.isAbsolute(renderer.path)) return false;
302
+ const target = path.resolve(root, renderer.path);
303
+ if (!target.startsWith(`${root}${path.sep}`)) return false;
304
+ return sha256(fs.readFileSync(target)) === renderer.sha256;
305
+ });
291
306
  } catch {
292
307
  return false;
293
308
  }
@@ -1364,6 +1379,7 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageNam
1364
1379
  }
1365
1380
 
1366
1381
  const agentMentionPatches = patchClaudeFollowupAgentMentions(staging);
1382
+ const codeDisabledBackPatches = patchClaudeCodeDisabledBackRouting(staging);
1367
1383
  const oldAsarHash = asarHeaderHash(asarPath);
1368
1384
  let planUsageGuardPatchCount = 0;
1369
1385
  let planUsageRequestPatchCount = 0;
@@ -1406,7 +1422,7 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageNam
1406
1422
  const vendorExecutable = path.join(macos, executableName);
1407
1423
  const signingIdentity = resolveVendoredAppSigningIdentity(vendorExecutable);
1408
1424
  writeAtomic(path.join(staging, "Contents", "Resources", "impel-compatibility.json"), JSON.stringify({
1409
- schemaVersion: 4,
1425
+ schemaVersion: 6,
1410
1426
  cliVersion: CLI_VERSION,
1411
1427
  bundleFingerprint: BUNDLE_BUILD_FINGERPRINT,
1412
1428
  signingMode: signingModeForIdentity(signingIdentity),
@@ -1425,12 +1441,12 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageNam
1425
1441
  followupBoundAgentEnable: agentMentionPatches.enable,
1426
1442
  followupBoundAgentFilter: agentMentionPatches.filter,
1427
1443
  followupBoundAgentChip: agentMentionPatches.chip,
1444
+ codeDisabledBack: codeDisabledBackPatches.disabled,
1445
+ codeNoSeatBack: codeDisabledBackPatches.noSeat,
1428
1446
  },
1429
1447
  rendererAssets: {
1430
- agentMentions: {
1431
- path: agentMentionPatches.path,
1432
- sha256: agentMentionPatches.sha256,
1433
- },
1448
+ agentMentions: agentMentionPatches.assets,
1449
+ codeDisabledBack: codeDisabledBackPatches.assets,
1434
1450
  },
1435
1451
  asarHeaderSha256: newAsarHash,
1436
1452
  }, null, 2) + "\n", 0o644);
@@ -1567,52 +1583,109 @@ function linkVendoredChatGPTResources(bundle, vendorBundleName) {
1567
1583
  }
1568
1584
 
1569
1585
  function patchClaudeFollowupAgentMentions(bundle) {
1586
+ const assets = patchClaudeRendererContracts(bundle, {
1587
+ supportLabel: "follow-up agent mentions",
1588
+ patchLabel: "follow-up agent-mention",
1589
+ contracts: [
1590
+ {
1591
+ key: "enable",
1592
+ original: CLAUDE_DRAFT_AGENT_MENTIONS,
1593
+ replacement: IMPEL_CLAUDE_BOUND_AGENT_MENTIONS,
1594
+ },
1595
+ {
1596
+ key: "filter",
1597
+ original: CLAUDE_AGENT_MENTION_FILTER,
1598
+ replacement: IMPEL_CLAUDE_BOUND_AGENT_MENTION_FILTER,
1599
+ },
1600
+ {
1601
+ key: "chip",
1602
+ original: CLAUDE_AGENT_MENTION_SELECT,
1603
+ replacement: IMPEL_CLAUDE_BOUND_AGENT_MENTION_SELECT,
1604
+ },
1605
+ ],
1606
+ });
1607
+ return {
1608
+ enable: 1,
1609
+ filter: 1,
1610
+ chip: 1,
1611
+ assets,
1612
+ };
1613
+ }
1614
+
1615
+ function patchClaudeCodeDisabledBackRouting(bundle) {
1616
+ const assets = patchClaudeRendererContracts(bundle, {
1617
+ supportLabel: "Code-disabled Back routing",
1618
+ patchLabel: "Code-disabled Back routing",
1619
+ contracts: [
1620
+ {
1621
+ key: "disabled",
1622
+ original: CLAUDE_CODE_DISABLED_BACK_LINK,
1623
+ replacement: IMPEL_CLAUDE_CODE_DISABLED_BACK_LINK,
1624
+ },
1625
+ {
1626
+ key: "noSeat",
1627
+ original: CLAUDE_CODE_NO_SEAT_BACK_LINK,
1628
+ replacement: IMPEL_CLAUDE_CODE_NO_SEAT_BACK_LINK,
1629
+ },
1630
+ ],
1631
+ });
1632
+ return {
1633
+ disabled: 1,
1634
+ noSeat: 1,
1635
+ assets,
1636
+ };
1637
+ }
1638
+
1639
+ function patchClaudeRendererContracts(bundle, {
1640
+ supportLabel,
1641
+ patchLabel,
1642
+ contracts,
1643
+ }) {
1570
1644
  const version = bundleVersion(bundle);
1571
1645
  if (version !== PINNED_VENDOR_APPS.claude.version) {
1572
1646
  throw new Error(
1573
- `Claude follow-up agent mentions support is pinned to ${PINNED_VENDOR_APPS.claude.version}; found ${version || "unknown"}`,
1647
+ `Claude ${supportLabel} support is pinned to ${PINNED_VENDOR_APPS.claude.version}; found ${version || "unknown"}`,
1574
1648
  );
1575
1649
  }
1576
1650
 
1577
1651
  const ionDist = path.join(bundle, "Contents", "Resources", "ion-dist");
1578
- const contracts = [
1579
- CLAUDE_DRAFT_AGENT_MENTIONS,
1580
- CLAUDE_AGENT_MENTION_FILTER,
1581
- CLAUDE_AGENT_MENTION_SELECT,
1582
- ];
1583
- const candidates = listJavaScriptFiles(ionDist).filter((candidate) => {
1584
- const source = fs.readFileSync(candidate, "utf8");
1585
- return contracts.every((contract) => source.includes(contract));
1586
- });
1587
- if (candidates.length !== 1) {
1588
- throw new Error(
1589
- `Claude follow-up agent-mention renderer expected 1 matching asset, found ${candidates.length}; update impel-cli before installing this desktop app version`,
1590
- );
1652
+ const sources = new Map(listJavaScriptFiles(ionDist).map((candidate) => [
1653
+ candidate,
1654
+ fs.readFileSync(candidate, "utf8"),
1655
+ ]));
1656
+ const matches = new Map(contracts.map(({ key }) => [key, []]));
1657
+ for (const [candidate, source] of sources) {
1658
+ for (const contract of contracts) {
1659
+ const count = exactTextMatchCount(source, contract.original);
1660
+ if (count > 0) matches.get(contract.key).push({ candidate, count });
1661
+ }
1591
1662
  }
1592
-
1593
- const renderer = candidates[0];
1594
- let source = fs.readFileSync(renderer, "utf8");
1595
- const enable = exactTextMatchCount(source, CLAUDE_DRAFT_AGENT_MENTIONS);
1596
- const filter = exactTextMatchCount(source, CLAUDE_AGENT_MENTION_FILTER);
1597
- const chip = exactTextMatchCount(source, CLAUDE_AGENT_MENTION_SELECT);
1598
- for (const [label, count] of [["enable", enable], ["filter", filter], ["chip", chip]]) {
1663
+ for (const contract of contracts) {
1664
+ const count = matches.get(contract.key).reduce((total, match) => total + match.count, 0);
1599
1665
  if (count !== 1) {
1600
- throw new Error(`Claude follow-up agent-mention ${label} patch expected 1 match, found ${count}`);
1666
+ throw new Error(`Claude ${patchLabel} ${contract.key} patch expected 1 match, found ${count}`);
1601
1667
  }
1602
1668
  }
1603
1669
 
1604
- source = source
1605
- .replace(CLAUDE_DRAFT_AGENT_MENTIONS, IMPEL_CLAUDE_BOUND_AGENT_MENTIONS)
1606
- .replace(CLAUDE_AGENT_MENTION_FILTER, IMPEL_CLAUDE_BOUND_AGENT_MENTION_FILTER)
1607
- .replace(CLAUDE_AGENT_MENTION_SELECT, IMPEL_CLAUDE_BOUND_AGENT_MENTION_SELECT);
1608
- writeAtomic(renderer, source, fs.statSync(renderer).mode & 0o777);
1609
- return {
1610
- enable,
1611
- filter,
1612
- chip,
1613
- path: path.relative(ionDist, renderer).split(path.sep).join("/"),
1614
- sha256: sha256(Buffer.from(source, "utf8")),
1615
- };
1670
+ const assets = [];
1671
+ for (const candidate of [...new Set(
1672
+ [...matches.values()].flatMap((entries) => entries.map(({ candidate: file }) => file)),
1673
+ )].sort()) {
1674
+ const source = fs.readFileSync(candidate, "utf8");
1675
+ const applicable = contracts.filter((contract) => source.includes(contract.original));
1676
+ let patched = source;
1677
+ for (const contract of applicable) {
1678
+ patched = patched.replace(contract.original, contract.replacement);
1679
+ }
1680
+ writeAtomic(candidate, patched, fs.statSync(candidate).mode & 0o777);
1681
+ assets.push({
1682
+ path: path.relative(ionDist, candidate).split(path.sep).join("/"),
1683
+ sha256: sha256(Buffer.from(patched, "utf8")),
1684
+ patches: applicable.map(({ key }) => key),
1685
+ });
1686
+ }
1687
+
1688
+ return assets;
1616
1689
  }
1617
1690
 
1618
1691
  function listJavaScriptFiles(root) {
@@ -1661,23 +1734,6 @@ function patchFixedWidthAsarString(asarPath, original, replacement, label) {
1661
1734
  function patchClaudePlanUsageRequest(asarPath) {
1662
1735
  const archive = fs.readFileSync(asarPath);
1663
1736
  const source = archive.toString("latin1");
1664
- const legacyPatches = [];
1665
- for (
1666
- let offset = source.indexOf(CLAUDE_PLAN_USAGE_REQUEST);
1667
- offset !== -1;
1668
- offset = source.indexOf(CLAUDE_PLAN_USAGE_REQUEST, offset + CLAUDE_PLAN_USAGE_REQUEST.length)
1669
- ) {
1670
- legacyPatches.push({
1671
- offset,
1672
- original: CLAUDE_PLAN_USAGE_REQUEST,
1673
- replacement: IMPEL_CLAUDE_PLAN_USAGE_REQUEST,
1674
- });
1675
- }
1676
- if (legacyPatches.length > 0) {
1677
- return applyFixedWidthAsarPatches(
1678
- asarPath, archive, legacyPatches, "Claude plan-usage gateway request",
1679
- );
1680
- }
1681
1737
  const credentialAccessor = source.match(
1682
1738
  /function ([A-Za-z_$][\w$]*)\(e\)\{var t;return [\s\S]{0,500}?\[custom-3p\] Credentials loaded from managed config/u,
1683
1739
  )?.[1];
@@ -1693,10 +1749,10 @@ function patchClaudePlanUsageRequest(asarPath) {
1693
1749
  return applyFixedWidthAsarPatches(asarPath, archive, [], "Claude plan-usage gateway request");
1694
1750
  }
1695
1751
 
1696
- 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;
1752
+ 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;
1697
1753
  const patches = [...source.matchAll(requestPattern)].map((match) => {
1698
1754
  const original = match[0];
1699
- const replacement = `${match[1]}.net.fetch("https://gateway.useimpel.com/ui/a/u",{headers:{key:${credentialAccessor}(${configAccessor}()).apiKey}})`;
1755
+ 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}})`;
1700
1756
  if (replacement.length > original.length) {
1701
1757
  throw new Error("Claude plan-usage gateway request replacement no longer fits the vendor ASAR contract");
1702
1758
  }
@@ -1997,7 +2053,11 @@ function vendoredChatGPTWrapperPlist(vendorPath, asarHash, identity, executableN
1997
2053
  <key>CFBundleShortVersionString</key><string>${xmlEscape(bundleVersion(vendorPath) || "1")}</string>
1998
2054
  <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>
1999
2055
  <key>LSMinimumSystemVersion</key><string>12.0</string>
2056
+ <key>NSCalendarsFullAccessUsageDescription</key><string>Allow access so that ChatGPT can read and update your calendar events when you ask.</string>
2057
+ <key>NSCalendarsUsageDescription</key><string>Allow access so that ChatGPT can read and update your calendar events when you ask.</string>
2000
2058
  <key>NSHighResolutionCapable</key><true/>
2059
+ <key>NSRemindersFullAccessUsageDescription</key><string>Allow access so that ChatGPT can read and update your reminders when you ask.</string>
2060
+ <key>NSRemindersUsageDescription</key><string>Allow access so that ChatGPT can read and update your reminders when you ask.</string>
2001
2061
  </dict></plist>
2002
2062
  `;
2003
2063
  }
@@ -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);