impel-cli 0.15.2 → 0.15.3

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.15.2",
3
+ "version": "0.15.3",
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/agents.js CHANGED
@@ -29,6 +29,7 @@ const NATIVE_AGENT_TOOL_NAMES = [
29
29
  const SAFE_AGENT_ID_RE = /^[A-Za-z0-9_.:-]{1,160}$/u;
30
30
  const SAFE_SCOPE_PARAM_RE = /^[A-Za-z0-9_.:-]{1,160}$/u;
31
31
  const MAX_CATALOG_ITEMS = 500;
32
+ const RETRYABLE_AGENT_CATALOG_ERROR = /request timed out|fetch failed|ECONNRESET|ETIMEDOUT|EAI_AGAIN|network error/iu;
32
33
  const RESERVED_AGENT_NAMES = Object.freeze({
33
34
  claude: new Set(["explore", "general-purpose", "plan"]),
34
35
  codex: new Set(["default", "explorer", "worker"]),
@@ -540,7 +541,14 @@ export async function syncAgentProfiles({
540
541
  .map((profile) => ({ ...profile, skipped: true, reason: "fresh" }));
541
542
  if (pending.length === 0) return skipped;
542
543
 
543
- const catalog = await fetchCatalog({ gatewayUrl, credential, tenantId: normalizedTenant });
544
+ let catalog;
545
+ try {
546
+ catalog = await fetchCatalog({ gatewayUrl, credential, tenantId: normalizedTenant });
547
+ } catch (error) {
548
+ if (!RETRYABLE_AGENT_CATALOG_ERROR.test(String(error?.message || error))) throw error;
549
+ logger.log("Agents: catalog request failed transiently; retrying once…");
550
+ catalog = await fetchCatalog({ gatewayUrl, credential, tenantId: normalizedTenant });
551
+ }
544
552
  const results = [];
545
553
  for (const profile of pending) {
546
554
  const result = syncAgentProfile({
package/src/skills.js CHANGED
@@ -34,6 +34,7 @@ export const SKILLS_FALLBACK_GATEWAY_URL = "https://gateway.useimpel.ai";
34
34
  // and Claude both exit non-zero when a marketplace/plugin is already present, so
35
35
  // these must not be reported as real failures.
36
36
  const BENIGN_OUTPUT = /already (exist|install|add|present|configur)|up[ -]?to[ -]?date|no changes|nothing to (do|update)/i;
37
+ const TRANSIENT_SKILL_OUTPUT = /timed? out|timeout of \d+ms exceeded|ECONNRESET|ETIMEDOUT|EAI_AGAIN|network error|failed to download/i;
37
38
 
38
39
  const CLIENT_SPECS = {
39
40
  claude: {
@@ -74,20 +75,24 @@ export function resolveMarketplaceName(marketplace) {
74
75
  return typeof name === "string" && name.trim() ? name.trim() : null;
75
76
  }
76
77
 
77
- /** Resolve a marketplace name from Claude's local `marketplace list --json`. */
78
+ /** Resolve a marketplace name from Claude or Codex `marketplace list --json`. */
78
79
  export function resolveConfiguredMarketplaceName(output, sourceUrl) {
79
- let marketplaces;
80
+ let payload;
80
81
  try {
81
- marketplaces = JSON.parse(String(output || ""));
82
+ payload = JSON.parse(String(output || ""));
82
83
  } catch {
83
84
  return null;
84
85
  }
86
+ const marketplaces = Array.isArray(payload)
87
+ ? payload
88
+ : payload?.marketplaces;
85
89
  if (!Array.isArray(marketplaces)) return null;
86
90
  const normalizedSource = String(sourceUrl || "").replace(/\/+$/u, "");
87
- const marketplace = marketplaces.find((candidate) => (
88
- typeof candidate?.url === "string"
89
- && candidate.url.replace(/\/+$/u, "") === normalizedSource
90
- ));
91
+ const marketplace = marketplaces.find((candidate) => {
92
+ const source = candidate?.url || candidate?.marketplaceSource?.source;
93
+ return typeof source === "string"
94
+ && source.replace(/\/+$/u, "") === normalizedSource;
95
+ });
91
96
  return resolveMarketplaceName(marketplace);
92
97
  }
93
98
 
@@ -275,6 +280,18 @@ function isBenign(result) {
275
280
  return result.ok || BENIGN_OUTPUT.test(`${result.stdout}\n${result.stderr}`);
276
281
  }
277
282
 
283
+ async function runSkillCommandWithRetry(run, bin, args, env) {
284
+ const first = await run(bin, args, env);
285
+ if (
286
+ first.ok
287
+ || first.missing
288
+ || !TRANSIENT_SKILL_OUTPUT.test(`${first.stdout}\n${first.stderr}`)
289
+ ) {
290
+ return first;
291
+ }
292
+ return run(bin, args, env);
293
+ }
294
+
278
295
  /** Fetch the marketplace.json and return its registered name, or null on any failure. */
279
296
  export async function fetchMarketplaceName(url, fetchImpl = fetch) {
280
297
  const controller = new AbortController();
@@ -343,7 +360,12 @@ export async function syncSkills({
343
360
  marketplaceSourceUrl: sourceUrl,
344
361
  marketplaceName,
345
362
  })[0];
346
- const registerResult = await run(spec.bin, registerCommand.args, env);
363
+ const registerResult = await runSkillCommandWithRetry(
364
+ run,
365
+ spec.bin,
366
+ registerCommand.args,
367
+ env,
368
+ );
347
369
  if (registerResult.missing) {
348
370
  failures.push({ phase: registerCommand.phase, reason: `\`${spec.bin}\` disappeared mid-sync` });
349
371
  } else if (!isBenign(registerResult)) {
@@ -354,9 +376,10 @@ export async function syncSkills({
354
376
  }
355
377
 
356
378
  // The public manifest fetch is deliberately best-effort and can time out.
357
- // Once Claude has registered the marketplace, its local JSON index gives us
358
- // the same dynamic name so refreshes can still use PLUGIN@MARKETPLACE.
359
- if (!marketplaceName && client === "claude" && !registerResult.missing) {
379
+ // Once either client has registered the marketplace, its local JSON index
380
+ // gives us the same dynamic name so refreshes can still use
381
+ // PLUGIN@MARKETPLACE.
382
+ if (!marketplaceName && !registerResult.missing) {
360
383
  const listed = await run(spec.bin, ["plugin", "marketplace", "list", "--json"], env);
361
384
  if (listed.ok) marketplaceName = resolveConfiguredMarketplaceName(listed.stdout, sourceUrl);
362
385
  }
@@ -367,7 +390,12 @@ export async function syncSkills({
367
390
  marketplaceName,
368
391
  }).slice(1);
369
392
  for (const command of commands) {
370
- const result = await run(spec.bin, command.args, env);
393
+ const result = await runSkillCommandWithRetry(
394
+ run,
395
+ spec.bin,
396
+ command.args,
397
+ env,
398
+ );
371
399
  if (result.missing) {
372
400
  failures.push({ phase: command.phase, reason: `\`${spec.bin}\` disappeared mid-sync` });
373
401
  break;