impel-cli 0.15.2 → 0.16.0
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/README.md +13 -9
- package/package.json +1 -1
- package/src/agents.js +9 -1
- package/src/commands/setup.js +70 -25
- package/src/commands/update.js +27 -4
- package/src/installRecovery/checkpoint.js +2 -3
- package/src/installRecovery/engine.js +430 -0
- package/src/installRecovery/inference.js +167 -0
- package/src/installRecovery/tools.js +581 -0
- package/src/skills.js +40 -12
- package/src/installRecovery/actions.js +0 -440
- package/src/installRecovery/client.js +0 -84
- package/src/installRecovery/loop.js +0 -258
package/README.md
CHANGED
|
@@ -115,15 +115,19 @@ checksum-verifying native installers, so Claude Code does not inherit its newer
|
|
|
115
115
|
Node.js requirement from the deprecated npm package. On Linux, it prepares the
|
|
116
116
|
isolated command-line workflow.
|
|
117
117
|
|
|
118
|
-
If setup or the CLI self-update fails, Impel first runs
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
118
|
+
If setup or the CLI self-update fails, Impel first runs deterministic local
|
|
119
|
+
repairs for known failure fingerprints — offline, with no upload. If the
|
|
120
|
+
failure persists, an interactive terminal offers assisted recovery (use
|
|
121
|
+
`impel setup --repair` or `impel update --repair` to opt in explicitly): a
|
|
122
|
+
bounded local loop where a gateway-hosted model selects typed diagnostic and
|
|
123
|
+
repair tools and the CLI validates, approves, and executes them on this
|
|
124
|
+
machine. Before the first upload, the CLI prints the exact sanitized payload.
|
|
125
|
+
Free-form model commands are impossible by construction; repairs limited to
|
|
126
|
+
Impel-owned files need one approval per session, while vendor installers and
|
|
127
|
+
PATH edits always confirm individually. Recovery only reports success after
|
|
128
|
+
the failed steps re-pass their local health checks, and it stops after 12
|
|
129
|
+
model turns or 10 minutes. Use `--no-recovery` or
|
|
130
|
+
`IMPEL_DISABLE_INSTALL_RECOVERY=1` to keep recovery off.
|
|
127
131
|
|
|
128
132
|
### If something does not work
|
|
129
133
|
|
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/commands/setup.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { parseFlags } from "../args.js";
|
|
7
7
|
import {
|
|
8
|
+
crossAppModelsEnabled,
|
|
8
9
|
loadConfig,
|
|
9
10
|
saveConfig,
|
|
10
11
|
normalizeGatewayUrl,
|
|
@@ -14,10 +15,11 @@ import {
|
|
|
14
15
|
redactSecretText,
|
|
15
16
|
} from "../config.js";
|
|
16
17
|
import { promptSecret, promptText } from "../prompt.js";
|
|
18
|
+
import { ensureImpelClaudeProfile, ensureImpelCodexProfile } from "../cliProfiles.js";
|
|
17
19
|
import { fetchTenants, normalizeTenantId, tenantCredential } from "../tenants.js";
|
|
18
20
|
import { cmdApps } from "./apps.js";
|
|
19
21
|
import { prepareWindowsClis, windowsCliInstallCommands } from "../windowsSetup.js";
|
|
20
|
-
import { runInstallRecovery } from "../installRecovery/
|
|
22
|
+
import { runInstallRecovery } from "../installRecovery/engine.js";
|
|
21
23
|
|
|
22
24
|
const HELP = `impel setup - guided end-to-end gateway setup
|
|
23
25
|
|
|
@@ -410,14 +412,63 @@ export async function cmdSetup(argv, overrides = {}) {
|
|
|
410
412
|
])
|
|
411
413
|
),
|
|
412
414
|
};
|
|
415
|
+
|
|
416
|
+
// Recovery ends as "fixed" only when every goal derived from the failed
|
|
417
|
+
// steps passes a fresh local check. App installs have no cheap external
|
|
418
|
+
// probe, so that goal tracks the most recent reviewed install result.
|
|
419
|
+
const appInstallState = { ok: false };
|
|
420
|
+
const installVendorApp = async (target) => {
|
|
421
|
+
const installed = await io.installApps(["install", target]);
|
|
422
|
+
appInstallState.ok = installed !== false;
|
|
423
|
+
return installed;
|
|
424
|
+
};
|
|
425
|
+
const goals = [];
|
|
426
|
+
if (io.platform === "win32" && setupFailures.some((failure) => /vendor_cli|windows_clis/iu.test(failure.step))) {
|
|
427
|
+
goals.push({
|
|
428
|
+
id: "windows-clis",
|
|
429
|
+
description: "Claude Code and Codex are installed and pass their version checks",
|
|
430
|
+
run: async () => {
|
|
431
|
+
const checked = await io.prepareWindowsClis({
|
|
432
|
+
gatewayUrl,
|
|
433
|
+
tenantId: tenant.id,
|
|
434
|
+
skipInstall: true,
|
|
435
|
+
});
|
|
436
|
+
return checked.missingAfter.length === 0
|
|
437
|
+
? true
|
|
438
|
+
: { ok: false, detail: `still missing: ${checked.missingAfter.join(", ")}` };
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
if (setupFailures.some((failure) => /vendor_app/iu.test(failure.step))) {
|
|
443
|
+
goals.push({
|
|
444
|
+
id: "desktop-apps",
|
|
445
|
+
description: "The Impel desktop app profiles installed cleanly",
|
|
446
|
+
run: () => appInstallState.ok === true,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
if (setupFailures.some((failure) => failure.step === "verify.gateway")) {
|
|
450
|
+
goals.push({
|
|
451
|
+
id: "gateway",
|
|
452
|
+
description: "The gateway accepts the stored credential",
|
|
453
|
+
run: async () => {
|
|
454
|
+
const checked = await io.probe(gatewayUrl, pat, tenant.id);
|
|
455
|
+
return checked.reachable && !checked.rejected
|
|
456
|
+
? true
|
|
457
|
+
: { ok: false, detail: checked.error || `HTTP ${checked.status}` };
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
413
462
|
try {
|
|
414
463
|
const recovery = await io.recoverInstall(
|
|
415
464
|
{
|
|
416
465
|
failure: aggregateFailure,
|
|
417
466
|
config,
|
|
467
|
+
goals,
|
|
418
468
|
explicit: Boolean(flags.repair),
|
|
419
469
|
noRecovery: Boolean(flags["no-recovery"]),
|
|
420
470
|
actionContext: {
|
|
471
|
+
tenantId: tenant.id,
|
|
421
472
|
probeGateway: () => io.probe(gatewayUrl, pat, tenant.id),
|
|
422
473
|
installVendorClis: (tool) =>
|
|
423
474
|
io.prepareWindowsClis({
|
|
@@ -427,15 +478,21 @@ export async function cmdSetup(argv, overrides = {}) {
|
|
|
427
478
|
installTools: [tool],
|
|
428
479
|
}),
|
|
429
480
|
repairProfiles: async () => {
|
|
430
|
-
if (io.platform
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
481
|
+
if (io.platform === "win32") {
|
|
482
|
+
const result = await io.prepareWindowsClis({
|
|
483
|
+
gatewayUrl,
|
|
484
|
+
tenantId: tenant.id,
|
|
485
|
+
skipInstall: true,
|
|
486
|
+
});
|
|
487
|
+
return result.missingAfter.length === 0;
|
|
488
|
+
}
|
|
489
|
+
ensureImpelClaudeProfile(gatewayUrl, tenant.id, {
|
|
490
|
+
crossAppModels: crossAppModelsEnabled(config),
|
|
435
491
|
});
|
|
436
|
-
|
|
492
|
+
ensureImpelCodexProfile(gatewayUrl, tenant.id);
|
|
493
|
+
return true;
|
|
437
494
|
},
|
|
438
|
-
installVendorApp
|
|
495
|
+
installVendorApp,
|
|
439
496
|
retryStep: async (step) => {
|
|
440
497
|
if (/vendor_cli|windows_clis/iu.test(step || "")) {
|
|
441
498
|
const result = await io.prepareWindowsClis({
|
|
@@ -446,7 +503,7 @@ export async function cmdSetup(argv, overrides = {}) {
|
|
|
446
503
|
return result.missingAfter.length === 0;
|
|
447
504
|
}
|
|
448
505
|
if (/vendor_app/iu.test(step || "")) {
|
|
449
|
-
return (await
|
|
506
|
+
return (await installVendorApp("all")) !== false;
|
|
450
507
|
}
|
|
451
508
|
if (/gateway/iu.test(step || "")) {
|
|
452
509
|
const result = await io.probe(gatewayUrl, pat, tenant.id);
|
|
@@ -462,23 +519,11 @@ export async function cmdSetup(argv, overrides = {}) {
|
|
|
462
519
|
...(overrides.recoveryOverrides || {}),
|
|
463
520
|
}
|
|
464
521
|
);
|
|
522
|
+
// "fixed" is only reported after every goal passed a fresh local check
|
|
523
|
+
// inside the engine, so no separate re-verification is needed here.
|
|
465
524
|
if (recovery.fixed) {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
gatewayUrl,
|
|
469
|
-
tenantId: tenant.id,
|
|
470
|
-
skipInstall: true,
|
|
471
|
-
});
|
|
472
|
-
platformSetupFailed = checked.missingAfter.length > 0;
|
|
473
|
-
} else {
|
|
474
|
-
platformSetupFailed = false;
|
|
475
|
-
}
|
|
476
|
-
if (setupFailures.some((failure) => failure.step === "verify.gateway")) {
|
|
477
|
-
const checked = await io.probe(gatewayUrl, pat, tenant.id);
|
|
478
|
-
verificationFailed = !checked.reachable || checked.rejected;
|
|
479
|
-
} else {
|
|
480
|
-
verificationFailed = false;
|
|
481
|
-
}
|
|
525
|
+
platformSetupFailed = false;
|
|
526
|
+
verificationFailed = false;
|
|
482
527
|
}
|
|
483
528
|
} catch (error) {
|
|
484
529
|
console.warn(`Install recovery could not start (${redactSecretText(error?.message || error)}).`);
|
package/src/commands/update.js
CHANGED
|
@@ -16,7 +16,7 @@ import { nativeCommandInvocation } from "../nativeProcess.js";
|
|
|
16
16
|
import { windowsClaudeUserData } from "../windowsApps.js";
|
|
17
17
|
import { withProgress } from "../progress.js";
|
|
18
18
|
import { installedAppTenantIds } from "./apps.js";
|
|
19
|
-
import { runInstallRecovery } from "../installRecovery/
|
|
19
|
+
import { runInstallRecovery } from "../installRecovery/engine.js";
|
|
20
20
|
import {
|
|
21
21
|
fetchRemoteVersion,
|
|
22
22
|
installedVersion,
|
|
@@ -220,7 +220,10 @@ export async function cmdUpdate(argv, overrides = {}) {
|
|
|
220
220
|
}
|
|
221
221
|
const config = io.loadConfig();
|
|
222
222
|
let recovered = false;
|
|
223
|
-
if (config?.pat && config?.tenantId
|
|
223
|
+
if (config?.pat && config?.tenantId) {
|
|
224
|
+
// The goal tracks the most recent reviewed retry of the npm install:
|
|
225
|
+
// recovery may only end "fixed" once that retry actually succeeded.
|
|
226
|
+
const updateState = { ok: false };
|
|
224
227
|
try {
|
|
225
228
|
const recovery = await io.recoverInstall(
|
|
226
229
|
{
|
|
@@ -232,11 +235,31 @@ export async function cmdUpdate(argv, overrides = {}) {
|
|
|
232
235
|
message: "The npm-verified global impel-cli update failed.",
|
|
233
236
|
},
|
|
234
237
|
config,
|
|
238
|
+
goals: [
|
|
239
|
+
{
|
|
240
|
+
id: "cli-update",
|
|
241
|
+
description: "The global impel-cli npm update completed successfully",
|
|
242
|
+
run: () => updateState.ok === true,
|
|
243
|
+
},
|
|
244
|
+
],
|
|
235
245
|
explicit: Boolean(flags.repair),
|
|
236
246
|
noRecovery: Boolean(flags["no-recovery"]),
|
|
237
247
|
actionContext: {
|
|
238
|
-
|
|
239
|
-
|
|
248
|
+
tenantId: config.tenantId,
|
|
249
|
+
probeGateway: async () => {
|
|
250
|
+
try {
|
|
251
|
+
const response = await fetch(`${config.gatewayUrl}/health`, {
|
|
252
|
+
signal: AbortSignal.timeout(5000),
|
|
253
|
+
});
|
|
254
|
+
return { reachable: true, status: response.status, rejected: false };
|
|
255
|
+
} catch (error) {
|
|
256
|
+
return { reachable: false, error: redactSecretText(error?.message || error) };
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
retryStep: async () => {
|
|
260
|
+
updateState.ok = io.selfUpdate(updateInstallSpec()) === true;
|
|
261
|
+
return updateState.ok;
|
|
262
|
+
},
|
|
240
263
|
},
|
|
241
264
|
},
|
|
242
265
|
overrides.recoveryOverrides || {}
|
|
@@ -11,10 +11,9 @@ export const INSTALL_RECOVERY_CHECKPOINT_PATH = path.join(
|
|
|
11
11
|
function validCheckpoint(value) {
|
|
12
12
|
return Boolean(
|
|
13
13
|
value &&
|
|
14
|
-
value.protocolVersion ===
|
|
15
|
-
typeof value.sessionId === "string" &&
|
|
16
|
-
typeof value.recoveryToken === "string" &&
|
|
14
|
+
value.protocolVersion === 2 &&
|
|
17
15
|
typeof value.fingerprint === "string" &&
|
|
16
|
+
typeof value.status === "string" &&
|
|
18
17
|
typeof value.expiresAt === "number"
|
|
19
18
|
);
|
|
20
19
|
}
|