impel-cli 0.15.1 → 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.1",
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,6 +75,27 @@ export function resolveMarketplaceName(marketplace) {
74
75
  return typeof name === "string" && name.trim() ? name.trim() : null;
75
76
  }
76
77
 
78
+ /** Resolve a marketplace name from Claude or Codex `marketplace list --json`. */
79
+ export function resolveConfiguredMarketplaceName(output, sourceUrl) {
80
+ let payload;
81
+ try {
82
+ payload = JSON.parse(String(output || ""));
83
+ } catch {
84
+ return null;
85
+ }
86
+ const marketplaces = Array.isArray(payload)
87
+ ? payload
88
+ : payload?.marketplaces;
89
+ if (!Array.isArray(marketplaces)) return null;
90
+ const normalizedSource = String(sourceUrl || "").replace(/\/+$/u, "");
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
+ });
96
+ return resolveMarketplaceName(marketplace);
97
+ }
98
+
77
99
  /**
78
100
  * Resolve the gateway URL for skill serving. Reuses the CLI's configured gateway
79
101
  * (the caller passes `config.gatewayUrl`), then the env/default from config.js,
@@ -258,6 +280,18 @@ function isBenign(result) {
258
280
  return result.ok || BENIGN_OUTPUT.test(`${result.stdout}\n${result.stderr}`);
259
281
  }
260
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
+
261
295
  /** Fetch the marketplace.json and return its registered name, or null on any failure. */
262
296
  export async function fetchMarketplaceName(url, fetchImpl = fetch) {
263
297
  const controller = new AbortController();
@@ -317,13 +351,51 @@ export async function syncSkills({
317
351
 
318
352
  const manifestUrl = marketplaceUrl(gatewayUrl, client);
319
353
  const sourceUrl = marketplaceSourceUrl(gatewayUrl, client);
320
- const marketplaceName = await fetchMarketplaceName(manifestUrl, fetchImpl);
321
- const commands = buildSkillCommands({ client, marketplaceSourceUrl: sourceUrl, marketplaceName });
354
+ let marketplaceName = await fetchMarketplaceName(manifestUrl, fetchImpl);
322
355
 
323
356
  logger.log(`Skills: syncing ${SKILL_PLUGIN_NAME} for ${displayLabel}…`);
324
357
  const failures = [];
358
+ const registerCommand = buildSkillCommands({
359
+ client,
360
+ marketplaceSourceUrl: sourceUrl,
361
+ marketplaceName,
362
+ })[0];
363
+ const registerResult = await runSkillCommandWithRetry(
364
+ run,
365
+ spec.bin,
366
+ registerCommand.args,
367
+ env,
368
+ );
369
+ if (registerResult.missing) {
370
+ failures.push({ phase: registerCommand.phase, reason: `\`${spec.bin}\` disappeared mid-sync` });
371
+ } else if (!isBenign(registerResult)) {
372
+ failures.push({
373
+ phase: registerCommand.phase,
374
+ reason: firstLine(registerResult.stderr) || `exit ${registerResult.status}`,
375
+ });
376
+ }
377
+
378
+ // The public manifest fetch is deliberately best-effort and can time out.
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) {
383
+ const listed = await run(spec.bin, ["plugin", "marketplace", "list", "--json"], env);
384
+ if (listed.ok) marketplaceName = resolveConfiguredMarketplaceName(listed.stdout, sourceUrl);
385
+ }
386
+
387
+ const commands = registerResult.missing ? [] : buildSkillCommands({
388
+ client,
389
+ marketplaceSourceUrl: sourceUrl,
390
+ marketplaceName,
391
+ }).slice(1);
325
392
  for (const command of commands) {
326
- 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
+ );
327
399
  if (result.missing) {
328
400
  failures.push({ phase: command.phase, reason: `\`${spec.bin}\` disappeared mid-sync` });
329
401
  break;