impel-cli 0.14.1 → 0.14.2

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.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/package.json +1 -1
  3. package/src/apps.js +116 -2
package/README.md CHANGED
@@ -657,6 +657,10 @@ without contacting the gateway.
657
657
  Start a new task after syncing. Claude Code detects updates to an existing
658
658
  `agents/` directory within a few seconds, but if the sync created that directory
659
659
  for the first time while Claude was already running, restart Claude once.
660
+ In Claude Desktop, selecting an agent on the first turn binds that Code session
661
+ to the agent. Follow-up `@` menus continue to offer that same bound agent and
662
+ insert a normal mention chip. Other agents remain available from a new task,
663
+ because the running Claude session cannot safely switch agents mid-session.
660
664
 
661
665
  Invocation uses the clients' native agent behavior:
662
666
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.14.1",
3
+ "version": "0.14.2",
4
4
  "description": "Configure Claude Code and Codex CLI to talk to Impel's gateway, authenticated by an Impel Personal Access Token",
5
5
  "type": "module",
6
6
  "bin": {
package/src/apps.js CHANGED
@@ -83,6 +83,12 @@ const CLAUDE_PLAN_USAGE_GUARD = "if(!Ze().hasOrgPolicyBackend())return;if((r=fr(
83
83
  const IMPEL_CLAUDE_PLAN_USAGE_GUARD = `${"if(!wze())return;".padEnd("if(!Ze().hasOrgPolicyBackend())return;".length, " ")}if((r=fr())`;
84
84
  const CLAUDE_PLAN_USAGE_REQUEST = 'P.net.fetch(`${ht()}/api/organizations/${o}/usage`,{signal:AbortSignal.timeout(mcr)})';
85
85
  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, " ");
86
+ const CLAUDE_DRAFT_AGENT_MENTIONS = 'agentMentionsEnabled:"draft"===Be&&s';
87
+ const IMPEL_CLAUDE_BOUND_AGENT_MENTIONS = 'agentMentionsEnabled:"draft"===Be?s:Le?.agent??!1';
88
+ const CLAUDE_AGENT_MENTION_FILTER = 'return e.filter(e=>"user"===e.source&&(!s||e.name.toLowerCase().includes(s))).slice(0,10)';
89
+ 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)';
90
+ const CLAUDE_AGENT_MENTION_SELECT = 'onSelect:t=>(e(String(t)),Promise.resolve(null))';
91
+ const IMPEL_CLAUDE_BOUND_AGENT_MENTION_SELECT = 'onSelect:t=>"string"==typeof y?Promise.resolve({chipText:String(t)}):(e(String(t)),Promise.resolve(null))';
86
92
  const LEGACY_CLAUDE_SAFE_STORAGE_NAME = "Claude";
87
93
  const CLAUDE_SAFE_STORAGE_METADATA = "safe-storage.json";
88
94
 
@@ -154,7 +160,7 @@ export const CURRENT_CONFIG_VERSION = 10;
154
160
  // — which is what made every `impel update` re-trigger macOS permission
155
161
  // prompts. Bump this ONLY when a code change alters the bytes of a built
156
162
  // bundle; leave it alone for changes that don't touch bundle contents.
157
- export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-07-16.3";
163
+ export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-07-17.1";
158
164
 
159
165
  /** Parse the tenant's install manifest, or null when absent/corrupt. */
160
166
  export function readTenantManifest(homeDir = os.homedir(), tenantId = null) {
@@ -205,6 +211,12 @@ export function bundleIsCurrent(status, { signingMode = desiredSigningMode() } =
205
211
  && compatibility.browserData === status.browserData
206
212
  ))
207
213
  && managedBundleMatchesCompatibility(status, compatibility)
214
+ && (status.target !== "claude" || (
215
+ compatibility.patches?.followupBoundAgentEnable === 1
216
+ && compatibility.patches?.followupBoundAgentFilter === 1
217
+ && compatibility.patches?.followupBoundAgentChip === 1
218
+ && managedClaudeRendererMatchesCompatibility(status, compatibility)
219
+ ))
208
220
  && (status.target !== "chatgpt" || (
209
221
  compatibility.vendorSignaturesPreserved === true
210
222
  && compatibility.resourceRoot === CHATGPT_RESOURCE_ROOT
@@ -214,6 +226,19 @@ export function bundleIsCurrent(status, { signingMode = desiredSigningMode() } =
214
226
  );
215
227
  }
216
228
 
229
+ function managedClaudeRendererMatchesCompatibility(status, compatibility) {
230
+ try {
231
+ const renderer = compatibility.rendererAssets?.agentMentions;
232
+ if (!renderer?.path || !renderer.sha256 || path.isAbsolute(renderer.path)) return false;
233
+ const root = path.resolve(status.launcher, "Contents", "Resources", "ion-dist");
234
+ const target = path.resolve(root, renderer.path);
235
+ if (!target.startsWith(`${root}${path.sep}`)) return false;
236
+ return sha256(fs.readFileSync(target)) === renderer.sha256;
237
+ } catch {
238
+ return false;
239
+ }
240
+ }
241
+
217
242
  function managedBundleMatchesCompatibility(status, compatibility) {
218
243
  try {
219
244
  const managedVendorBundle = status.target === "claude"
@@ -1153,6 +1178,7 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageNam
1153
1178
  );
1154
1179
  }
1155
1180
 
1181
+ const agentMentionPatches = patchClaudeFollowupAgentMentions(staging);
1156
1182
  const oldAsarHash = asarHeaderHash(asarPath);
1157
1183
  let planUsageGuardPatchCount = 0;
1158
1184
  let planUsageRequestPatchCount = 0;
@@ -1193,7 +1219,7 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageNam
1193
1219
 
1194
1220
  const macos = path.join(staging, "Contents", "MacOS");
1195
1221
  writeAtomic(path.join(staging, "Contents", "Resources", "impel-compatibility.json"), JSON.stringify({
1196
- schemaVersion: 3,
1222
+ schemaVersion: 4,
1197
1223
  cliVersion: CLI_VERSION,
1198
1224
  bundleFingerprint: BUNDLE_BUILD_FINGERPRINT,
1199
1225
  signingMode: desiredSigningMode(),
@@ -1209,6 +1235,15 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageNam
1209
1235
  aggregatePlanUsageGuard: planUsageGuardPatchCount,
1210
1236
  aggregatePlanUsageRequest: planUsageRequestPatchCount,
1211
1237
  safeStorageName: safeStorageNamePatchCount,
1238
+ followupBoundAgentEnable: agentMentionPatches.enable,
1239
+ followupBoundAgentFilter: agentMentionPatches.filter,
1240
+ followupBoundAgentChip: agentMentionPatches.chip,
1241
+ },
1242
+ rendererAssets: {
1243
+ agentMentions: {
1244
+ path: agentMentionPatches.path,
1245
+ sha256: agentMentionPatches.sha256,
1246
+ },
1212
1247
  },
1213
1248
  asarHeaderSha256: newAsarHash,
1214
1249
  }, null, 2) + "\n", 0o644);
@@ -1330,6 +1365,85 @@ function linkVendoredChatGPTResources(bundle, vendorBundleName) {
1330
1365
  }
1331
1366
  }
1332
1367
 
1368
+ function patchClaudeFollowupAgentMentions(bundle) {
1369
+ const version = bundleVersion(bundle);
1370
+ if (version !== PINNED_VENDOR_APPS.claude.version) {
1371
+ throw new Error(
1372
+ `Claude follow-up agent mentions support is pinned to ${PINNED_VENDOR_APPS.claude.version}; found ${version || "unknown"}`,
1373
+ );
1374
+ }
1375
+
1376
+ const ionDist = path.join(bundle, "Contents", "Resources", "ion-dist");
1377
+ const contracts = [
1378
+ CLAUDE_DRAFT_AGENT_MENTIONS,
1379
+ CLAUDE_AGENT_MENTION_FILTER,
1380
+ CLAUDE_AGENT_MENTION_SELECT,
1381
+ ];
1382
+ const candidates = listJavaScriptFiles(ionDist).filter((candidate) => {
1383
+ const source = fs.readFileSync(candidate, "utf8");
1384
+ return contracts.every((contract) => source.includes(contract));
1385
+ });
1386
+ if (candidates.length !== 1) {
1387
+ throw new Error(
1388
+ `Claude follow-up agent-mention renderer expected 1 matching asset, found ${candidates.length}; update impel-cli before installing this desktop app version`,
1389
+ );
1390
+ }
1391
+
1392
+ const renderer = candidates[0];
1393
+ let source = fs.readFileSync(renderer, "utf8");
1394
+ const enable = exactTextMatchCount(source, CLAUDE_DRAFT_AGENT_MENTIONS);
1395
+ const filter = exactTextMatchCount(source, CLAUDE_AGENT_MENTION_FILTER);
1396
+ const chip = exactTextMatchCount(source, CLAUDE_AGENT_MENTION_SELECT);
1397
+ for (const [label, count] of [["enable", enable], ["filter", filter], ["chip", chip]]) {
1398
+ if (count !== 1) {
1399
+ throw new Error(`Claude follow-up agent-mention ${label} patch expected 1 match, found ${count}`);
1400
+ }
1401
+ }
1402
+
1403
+ source = source
1404
+ .replace(CLAUDE_DRAFT_AGENT_MENTIONS, IMPEL_CLAUDE_BOUND_AGENT_MENTIONS)
1405
+ .replace(CLAUDE_AGENT_MENTION_FILTER, IMPEL_CLAUDE_BOUND_AGENT_MENTION_FILTER)
1406
+ .replace(CLAUDE_AGENT_MENTION_SELECT, IMPEL_CLAUDE_BOUND_AGENT_MENTION_SELECT);
1407
+ writeAtomic(renderer, source, fs.statSync(renderer).mode & 0o777);
1408
+ return {
1409
+ enable,
1410
+ filter,
1411
+ chip,
1412
+ path: path.relative(ionDist, renderer).split(path.sep).join("/"),
1413
+ sha256: sha256(Buffer.from(source, "utf8")),
1414
+ };
1415
+ }
1416
+
1417
+ function listJavaScriptFiles(root) {
1418
+ const files = [];
1419
+ const visit = (directory) => {
1420
+ let entries;
1421
+ try {
1422
+ entries = fs.readdirSync(directory, { withFileTypes: true });
1423
+ } catch (error) {
1424
+ if (error?.code === "ENOENT") return;
1425
+ throw error;
1426
+ }
1427
+ for (const entry of entries) {
1428
+ const target = path.join(directory, entry.name);
1429
+ if (entry.isDirectory()) visit(target);
1430
+ else if (entry.isFile() && entry.name.endsWith(".js")) files.push(target);
1431
+ }
1432
+ };
1433
+ visit(root);
1434
+ return files.sort();
1435
+ }
1436
+
1437
+ function exactTextMatchCount(source, expected) {
1438
+ let count = 0;
1439
+ for (
1440
+ let offset = source.indexOf(expected);
1441
+ offset !== -1;
1442
+ offset = source.indexOf(expected, offset + expected.length)
1443
+ ) count += 1;
1444
+ return count;
1445
+ }
1446
+
1333
1447
  function patchFixedWidthAsarString(asarPath, original, replacement, label) {
1334
1448
  if (Buffer.byteLength(original, "latin1") !== Buffer.byteLength(replacement, "latin1")) {
1335
1449
  throw new Error(`${label} replacement must preserve the vendor ASAR byte length`);