impel-cli 0.17.6 → 0.17.7

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.6",
3
+ "version": "0.17.7",
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
@@ -41,19 +41,26 @@ const APP_DEFINITIONS = {
41
41
  // CLI release therefore installs and accepts only the exact app builds tested
42
42
  // with its compatibility patches. Updating a pin requires a new CLI release.
43
43
  export const PINNED_VENDOR_APPS = Object.freeze({
44
+ // 1.22209.3 (the previous pin) routes every managed 3P gateway profile into
45
+ // the Claude Code marketing page at app://localhost/code/disabled: the
46
+ // desktop Code surface never becomes available, the hardcoded-off
47
+ // claude_code_web gate traps /code/*, and the page's "Back to Claude" link
48
+ // targets "/" so the persisted Code mode loops straight back. 1.20186.9 is
49
+ // the last build verified working end-to-end for gateway profiles. Do not
50
+ // re-upgrade without a verified fix for the Code-surface routing contract.
44
51
  claude: Object.freeze({
45
- version: "1.22209.3",
46
- claudeCodeVersion: "2.1.215",
47
- claudeCodeCommit: "316ce99628e89900bf0b1328fed3b8fec0c0c92d",
52
+ version: "1.20186.9",
53
+ claudeCodeVersion: "2.1.209",
54
+ claudeCodeCommit: "0fe048596fd45e79e99353cbe2c3d1b1ac069568",
48
55
  bundleName: "Claude.app",
49
56
  downloads: Object.freeze({
50
57
  arm64: Object.freeze({
51
- url: "https://downloads.claude.ai/releases/darwin/universal/1.22209.3/Claude-babe11577dfefe3e209c06bd674628d862f0dbae.zip",
52
- sha256: "b110e0fdb7b2601d80dfdd7893f7811de382de84c4586113e7873e28f17680fc",
58
+ url: "https://downloads.claude.ai/releases/darwin/universal/1.20186.9/Claude-69f150a4c9316d5c8cd7b9f130ed583d15c0383e.zip",
59
+ sha256: "9364648c74bd525aa293a28103eb37262a5553de21c7ca907766d7fc40623015",
53
60
  }),
54
61
  x64: Object.freeze({
55
- url: "https://downloads.claude.ai/releases/darwin/universal/1.22209.3/Claude-babe11577dfefe3e209c06bd674628d862f0dbae.zip",
56
- sha256: "b110e0fdb7b2601d80dfdd7893f7811de382de84c4586113e7873e28f17680fc",
62
+ url: "https://downloads.claude.ai/releases/darwin/universal/1.20186.9/Claude-69f150a4c9316d5c8cd7b9f130ed583d15c0383e.zip",
63
+ sha256: "9364648c74bd525aa293a28103eb37262a5553de21c7ca907766d7fc40623015",
57
64
  }),
58
65
  }),
59
66
  }),
@@ -85,10 +92,12 @@ const ULTRA_REASONING_LEVEL = { effort: "ultra", description: "Maximum reasoning
85
92
  const FAST_SERVICE_TIER = { id: "priority", name: "Fast", description: "1.5x speed, increased usage" };
86
93
  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
94
  const CHATGPT_RESOURCE_ROOT = "linked_vendor_resources_with_patched_asar";
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';
95
+ const CLAUDE_PLAN_USAGE_GUARD = "if(!Ze().hasOrgPolicyBackend())return;if((r=fr())";
96
+ const IMPEL_CLAUDE_PLAN_USAGE_GUARD = `${"if(!wze())return;".padEnd("if(!Ze().hasOrgPolicyBackend())return;".length, " ")}if((r=fr())`;
97
+ const CLAUDE_PLAN_USAGE_REQUEST = 'P.net.fetch(`${ht()}/api/organizations/${o}/usage`,{signal:AbortSignal.timeout(mcr)})';
98
+ 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, " ");
99
+ const CLAUDE_DRAFT_AGENT_MENTIONS = 'agentMentionsEnabled:"draft"===Be&&s';
100
+ const IMPEL_CLAUDE_BOUND_AGENT_MENTIONS = 'agentMentionsEnabled:"draft"===Be?s:Le?.agent??!1';
92
101
  const CLAUDE_AGENT_MENTION_FILTER = 'return e.filter(e=>"user"===e.source&&(!s||e.name.toLowerCase().includes(s))).slice(0,10)';
93
102
  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)';
94
103
  const CLAUDE_AGENT_MENTION_SELECT = 'onSelect:t=>(e(String(t)),Promise.resolve(null))';
@@ -174,7 +183,7 @@ export const CURRENT_CONFIG_VERSION = 17;
174
183
  // — which is what made every `impel update` re-trigger macOS permission
175
184
  // prompts. Bump this ONLY when a code change alters the bytes of a built
176
185
  // bundle; leave it alone for changes that don't touch bundle contents.
177
- export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-07-20.3";
186
+ export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-07-21.1";
178
187
 
179
188
  /** Parse the tenant's install manifest, or null when absent/corrupt. */
180
189
  export function readTenantManifest(homeDir = os.homedir(), tenantId = null) {
@@ -245,6 +254,7 @@ export function bundleIsCurrent(status, {
245
254
  compatibility.patches?.followupBoundAgentEnable === 1
246
255
  && compatibility.patches?.followupBoundAgentFilter === 1
247
256
  && compatibility.patches?.followupBoundAgentChip === 1
257
+ && claudePatchSetMatchesCompatibility(compatibility)
248
258
  && managedClaudeRendererMatchesCompatibility(status, compatibility)
249
259
  ))
250
260
  && (status.target !== "chatgpt" || (
@@ -278,6 +288,36 @@ function wrapperLaunchesOnTahoe(status) {
278
288
  }
279
289
  }
280
290
 
291
+ // The exact patch and renderer-asset groups this CLI build writes. A bundle
292
+ // whose compatibility metadata records a DIFFERENT set — even a superset — was
293
+ // built by different patching logic (e.g. a dev working tree that reused the
294
+ // released fingerprint), so it must be reported stale and rebuilt rather than
295
+ // trusted to behave like a reviewed release.
296
+ const CLAUDE_COMPATIBILITY_PATCH_KEYS = Object.freeze([
297
+ "aggregatePlanUsageGuard",
298
+ "aggregatePlanUsageRequest",
299
+ "safeStorageName",
300
+ "followupBoundAgentEnable",
301
+ "followupBoundAgentFilter",
302
+ "followupBoundAgentChip",
303
+ ]);
304
+ const CLAUDE_COMPATIBILITY_RENDERER_GROUPS = Object.freeze(["agentMentions"]);
305
+
306
+ function claudePatchSetMatchesCompatibility(compatibility) {
307
+ try {
308
+ const patchKeys = Object.keys(compatibility.patches ?? {}).sort();
309
+ const rendererGroups = Object.keys(compatibility.rendererAssets ?? {}).sort();
310
+ return (
311
+ patchKeys.length === CLAUDE_COMPATIBILITY_PATCH_KEYS.length
312
+ && [...CLAUDE_COMPATIBILITY_PATCH_KEYS].sort().every((key, index) => patchKeys[index] === key)
313
+ && rendererGroups.length === CLAUDE_COMPATIBILITY_RENDERER_GROUPS.length
314
+ && [...CLAUDE_COMPATIBILITY_RENDERER_GROUPS].sort().every((key, index) => rendererGroups[index] === key)
315
+ );
316
+ } catch {
317
+ return false;
318
+ }
319
+ }
320
+
281
321
  function managedClaudeRendererMatchesCompatibility(status, compatibility) {
282
322
  try {
283
323
  const renderers = compatibility.rendererAssets?.agentMentions;
@@ -618,27 +658,13 @@ export function installManagedAppFiles({
618
658
 
619
659
  const installed = [];
620
660
  for (const target of targets) {
621
- if (target === "claude") {
622
- migrateLegacyClaudeAppSessions(paths.claude.userData, homeDir);
623
- writeClaudeNative3PSelection(paths);
624
- writeClaudeCodeSettings(paths);
625
- writeClaudeConfig(paths, config, models);
626
- ensureClaudeSessionHooks(paths.claude.userData, config.tenantId, "claude_desktop");
627
- }
628
- else {
629
- writeChatGPTConfig(
630
- paths,
631
- config,
632
- models,
633
- vendorCodexModels,
634
- chatgptInvocations,
635
- generatedAt,
636
- );
637
- ensureCodexSessionHooks(paths.chatgpt.codexHome, config.tenantId, "codex_desktop");
638
- }
639
661
  // Non-bundle targets are the background-refresh / fast-open path: configs,
640
662
  // token helper, catalog, and manifest only. Bundle swaps require the app
641
663
  // to be closed (see quitBlockingApps), so those paths never attempt one.
664
+ // A stale bundle is rebuilt BEFORE its profile config is rewritten: the
665
+ // generated config is only reviewed against the pinned vendor build, so a
666
+ // failed rebuild must leave the old bundle paired with its old config
667
+ // rather than stranding it with a contract written for a different app.
642
668
  if (bundleTargets.has(target)) {
643
669
  if (resolvedVendorPaths[target] && isVendableVendorApp(target, resolvedVendorPaths[target])) {
644
670
  assertPinnedVendorAppVersion(target, resolvedVendorPaths[target]);
@@ -656,6 +682,24 @@ export function installManagedAppFiles({
656
682
  fs.rmSync(appPaths(homeDir)[target].launcher, { recursive: true, force: true });
657
683
  }
658
684
  }
685
+ if (target === "claude") {
686
+ migrateLegacyClaudeAppSessions(paths.claude.userData, homeDir);
687
+ writeClaudeNative3PSelection(paths);
688
+ writeClaudeCodeSettings(paths);
689
+ writeClaudeConfig(paths, config, models);
690
+ ensureClaudeSessionHooks(paths.claude.userData, config.tenantId, "claude_desktop");
691
+ }
692
+ else {
693
+ writeChatGPTConfig(
694
+ paths,
695
+ config,
696
+ models,
697
+ vendorCodexModels,
698
+ chatgptInvocations,
699
+ generatedAt,
700
+ );
701
+ ensureCodexSessionHooks(paths.chatgpt.codexHome, config.tenantId, "codex_desktop");
702
+ }
659
703
  installed.push({
660
704
  target,
661
705
  launcher: paths[target].launcher,
@@ -1672,6 +1716,23 @@ function patchFixedWidthAsarString(asarPath, original, replacement, label) {
1672
1716
  function patchClaudePlanUsageRequest(asarPath) {
1673
1717
  const archive = fs.readFileSync(asarPath);
1674
1718
  const source = archive.toString("latin1");
1719
+ const legacyPatches = [];
1720
+ for (
1721
+ let offset = source.indexOf(CLAUDE_PLAN_USAGE_REQUEST);
1722
+ offset !== -1;
1723
+ offset = source.indexOf(CLAUDE_PLAN_USAGE_REQUEST, offset + CLAUDE_PLAN_USAGE_REQUEST.length)
1724
+ ) {
1725
+ legacyPatches.push({
1726
+ offset,
1727
+ original: CLAUDE_PLAN_USAGE_REQUEST,
1728
+ replacement: IMPEL_CLAUDE_PLAN_USAGE_REQUEST,
1729
+ });
1730
+ }
1731
+ if (legacyPatches.length > 0) {
1732
+ return applyFixedWidthAsarPatches(
1733
+ asarPath, archive, legacyPatches, "Claude plan-usage gateway request",
1734
+ );
1735
+ }
1675
1736
  const credentialAccessor = source.match(
1676
1737
  /function ([A-Za-z_$][\w$]*)\(e\)\{var t;return [\s\S]{0,500}?\[custom-3p\] Credentials loaded from managed config/u,
1677
1738
  )?.[1];
@@ -1687,10 +1748,10 @@ function patchClaudePlanUsageRequest(asarPath) {
1687
1748
  return applyFixedWidthAsarPatches(asarPath, archive, [], "Claude plan-usage gateway request");
1688
1749
  }
1689
1750
 
1690
- 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;
1751
+ 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;
1691
1752
  const patches = [...source.matchAll(requestPattern)].map((match) => {
1692
1753
  const original = match[0];
1693
- 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}})`;
1754
+ const replacement = `${match[1]}.net.fetch("https://gateway.useimpel.com/ui/a/u",{headers:{key:${credentialAccessor}(${configAccessor}()).apiKey}})`;
1694
1755
  if (replacement.length > original.length) {
1695
1756
  throw new Error("Claude plan-usage gateway request replacement no longer fits the vendor ASAR contract");
1696
1757
  }
@@ -772,7 +772,7 @@ export async function cmdApps(argv, overrides = {}) {
772
772
  return refreshApps(targets, {
773
773
  staleOnly: Boolean(flags["stale-only"]),
774
774
  tenantId: flags.tenant || null,
775
- });
775
+ }, overrides);
776
776
  }
777
777
  if (action === "uninstall") {
778
778
  const config = loadConfig();
@@ -1163,8 +1163,20 @@ function claudeConfigIsCurrent(configPath, config, gatewayUrl) {
1163
1163
  // run (config writes only, never a bundle swap, never quits anything) and
1164
1164
  // silent on any failure: it is invoked detached from the apps' token helper,
1165
1165
  // so there is no user to talk to and nothing may leak to stdout.
1166
- async function refreshApps(targets, { staleOnly = false, tenantId = null } = {}) {
1167
- const stored = loadConfig();
1166
+ async function refreshApps(targets, { staleOnly = false, tenantId = null } = {}, overrides = {}) {
1167
+ const io = {
1168
+ loadStoredConfig: loadConfig,
1169
+ selectedConfig: selectedAppConfig,
1170
+ status: appStatus,
1171
+ bundleCurrent: bundleIsCurrent,
1172
+ fetchModels: fetchGatewayModels,
1173
+ installFiles: installManagedAppFiles,
1174
+ syncSkills: syncSkillsSafe,
1175
+ syncAgents: syncAgentProfilesSafe,
1176
+ secureCodexHome: secureManagedCodexHome,
1177
+ ...overrides,
1178
+ };
1179
+ const stored = io.loadStoredConfig();
1168
1180
  if (!stored?.pat) return;
1169
1181
  const requestedTenantId = tenantId
1170
1182
  ? normalizeTenantId(tenantId)
@@ -1181,17 +1193,25 @@ async function refreshApps(targets, { staleOnly = false, tenantId = null } = {})
1181
1193
 
1182
1194
  let config;
1183
1195
  try {
1184
- config = await selectedAppConfig(targets, tenantId ? requestedTenantId : null);
1196
+ config = await io.selectedConfig(targets, tenantId ? requestedTenantId : null);
1185
1197
  } catch {
1186
1198
  return;
1187
1199
  }
1188
- const statuses = appStatus(targets, os.homedir(), config.tenantId, config.tenantName);
1189
- const installedTargets = statuses.filter((status) => status.launcherInstalled);
1200
+ // Refresh only targets whose installed bundle was built by THIS CLI's
1201
+ // bundle logic. The generated config contract is reviewed against the
1202
+ // pinned vendor build, so rewriting configs (and advancing the manifest)
1203
+ // under a stale bundle would strand an old app with a config written for a
1204
+ // different app version. Stale bundles wait for the next foreground
1205
+ // install/open/update, which swaps bundle and config together.
1206
+ const statuses = io.status(targets, os.homedir(), config.tenantId, config.tenantName);
1207
+ const installedTargets = statuses.filter((status) => (
1208
+ status.launcherInstalled && io.bundleCurrent(status)
1209
+ ));
1190
1210
  if (installedTargets.length === 0) return;
1191
1211
 
1192
1212
  let catalog;
1193
1213
  try {
1194
- catalog = await fetchGatewayModels(config);
1214
+ catalog = await io.fetchModels(config);
1195
1215
  } catch {
1196
1216
  return;
1197
1217
  }
@@ -1204,7 +1224,7 @@ async function refreshApps(targets, { staleOnly = false, tenantId = null } = {})
1204
1224
  supportedTargetSet.has(status.target)
1205
1225
  ));
1206
1226
  if (supportedStatuses.length === 0) return;
1207
- installManagedAppFiles({
1227
+ io.installFiles({
1208
1228
  config,
1209
1229
  targets: supportedStatuses.map((status) => status.target),
1210
1230
  models: catalog.models,
@@ -1217,10 +1237,10 @@ async function refreshApps(targets, { staleOnly = false, tenantId = null } = {})
1217
1237
  const gatewayUrl = resolveSkillsGateway(config.gatewayUrl);
1218
1238
  for (const status of supportedStatuses) {
1219
1239
  const { client, env, label } = appSkillTarget(status.target, tenantPaths);
1220
- await syncSkillsSafe({ client, gatewayUrl, env, label });
1221
- if (status.target === "chatgpt") secureManagedCodexHome(tenantPaths.chatgpt.codexHome);
1240
+ await io.syncSkills({ client, gatewayUrl, env, label });
1241
+ if (status.target === "chatgpt") io.secureCodexHome(tenantPaths.chatgpt.codexHome);
1222
1242
  }
1223
- await syncAgentProfilesSafe({
1243
+ await io.syncAgents({
1224
1244
  profiles: supportedStatuses.map((status) => appAgentProfile(status.target, tenantPaths)),
1225
1245
  gatewayUrl: config.gatewayUrl,
1226
1246
  credential: config.pat,