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 +1 -1
- package/src/agents.js +9 -1
- package/src/skills.js +75 -3
package/package.json
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
|
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;
|