impel-cli 0.17.3 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.17.3",
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,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,12 +85,10 @@ 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))';
@@ -176,7 +174,7 @@ export const CURRENT_CONFIG_VERSION = 16;
176
174
  // — which is what made every `impel update` re-trigger macOS permission
177
175
  // prompts. Bump this ONLY when a code change alters the bytes of a built
178
176
  // bundle; leave it alone for changes that don't touch bundle contents.
179
- export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-07-19.1";
177
+ export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-07-20.1";
180
178
 
181
179
  /** Parse the tenant's install manifest, or null when absent/corrupt. */
182
180
  export function readTenantManifest(homeDir = os.homedir(), tenantId = null) {
@@ -282,12 +280,15 @@ function wrapperLaunchesOnTahoe(status) {
282
280
 
283
281
  function managedClaudeRendererMatchesCompatibility(status, compatibility) {
284
282
  try {
285
- const renderer = compatibility.rendererAssets?.agentMentions;
286
- 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;
287
285
  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;
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
+ });
291
292
  } catch {
292
293
  return false;
293
294
  }
@@ -1406,7 +1407,7 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageNam
1406
1407
  const vendorExecutable = path.join(macos, executableName);
1407
1408
  const signingIdentity = resolveVendoredAppSigningIdentity(vendorExecutable);
1408
1409
  writeAtomic(path.join(staging, "Contents", "Resources", "impel-compatibility.json"), JSON.stringify({
1409
- schemaVersion: 4,
1410
+ schemaVersion: 5,
1410
1411
  cliVersion: CLI_VERSION,
1411
1412
  bundleFingerprint: BUNDLE_BUILD_FINGERPRINT,
1412
1413
  signingMode: signingModeForIdentity(signingIdentity),
@@ -1427,10 +1428,7 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageNam
1427
1428
  followupBoundAgentChip: agentMentionPatches.chip,
1428
1429
  },
1429
1430
  rendererAssets: {
1430
- agentMentions: {
1431
- path: agentMentionPatches.path,
1432
- sha256: agentMentionPatches.sha256,
1433
- },
1431
+ agentMentions: agentMentionPatches.assets,
1434
1432
  },
1435
1433
  asarHeaderSha256: newAsarHash,
1436
1434
  }, null, 2) + "\n", 0o644);
@@ -1576,42 +1574,63 @@ function patchClaudeFollowupAgentMentions(bundle) {
1576
1574
 
1577
1575
  const ionDist = path.join(bundle, "Contents", "Resources", "ion-dist");
1578
1576
  const contracts = [
1579
- CLAUDE_DRAFT_AGENT_MENTIONS,
1580
- CLAUDE_AGENT_MENTION_FILTER,
1581
- 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
+ },
1582
1592
  ];
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
- );
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
+ }
1591
1603
  }
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]]) {
1604
+ for (const contract of contracts) {
1605
+ const count = matches.get(contract.key).reduce((total, match) => total + match.count, 0);
1599
1606
  if (count !== 1) {
1600
- 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}`);
1601
1608
  }
1602
1609
  }
1603
1610
 
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);
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);
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
+ });
1627
+ }
1628
+
1609
1629
  return {
1610
- enable,
1611
- filter,
1612
- chip,
1613
- path: path.relative(ionDist, renderer).split(path.sep).join("/"),
1614
- sha256: sha256(Buffer.from(source, "utf8")),
1630
+ enable: 1,
1631
+ filter: 1,
1632
+ chip: 1,
1633
+ assets,
1615
1634
  };
1616
1635
  }
1617
1636
 
@@ -1661,23 +1680,6 @@ function patchFixedWidthAsarString(asarPath, original, replacement, label) {
1661
1680
  function patchClaudePlanUsageRequest(asarPath) {
1662
1681
  const archive = fs.readFileSync(asarPath);
1663
1682
  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
1683
  const credentialAccessor = source.match(
1682
1684
  /function ([A-Za-z_$][\w$]*)\(e\)\{var t;return [\s\S]{0,500}?\[custom-3p\] Credentials loaded from managed config/u,
1683
1685
  )?.[1];
@@ -1693,10 +1695,10 @@ function patchClaudePlanUsageRequest(asarPath) {
1693
1695
  return applyFixedWidthAsarPatches(asarPath, archive, [], "Claude plan-usage gateway request");
1694
1696
  }
1695
1697
 
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;
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;
1697
1699
  const patches = [...source.matchAll(requestPattern)].map((match) => {
1698
1700
  const original = match[0];
1699
- 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}})`;
1700
1702
  if (replacement.length > original.length) {
1701
1703
  throw new Error("Claude plan-usage gateway request replacement no longer fits the vendor ASAR contract");
1702
1704
  }
@@ -1997,7 +1999,11 @@ function vendoredChatGPTWrapperPlist(vendorPath, asarHash, identity, executableN
1997
1999
  <key>CFBundleShortVersionString</key><string>${xmlEscape(bundleVersion(vendorPath) || "1")}</string>
1998
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>
1999
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>
2000
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>
2001
2007
  </dict></plist>
2002
2008
  `;
2003
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);