@agentlayer.tech/wallet 0.1.74 → 0.1.76

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.
@@ -6,6 +6,9 @@ import fs from "node:fs";
6
6
  import os from "node:os";
7
7
  import path from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
+ import { createBootKeyManager } from "./lib/boot-key.mjs";
10
+ import { createHostIntegrationManager, createIntegrationManager } from "./lib/integrations.mjs";
11
+ import { createUpdateTransactionManager } from "./lib/update-transaction.mjs";
9
12
 
10
13
  const cliPath = fileURLToPath(import.meta.url);
11
14
  const packageRoot = path.resolve(path.dirname(cliPath), "..");
@@ -388,20 +391,85 @@ function stagingRootFor(version, env = process.env) {
388
391
  );
389
392
  }
390
393
 
391
- function updateJournalPath(env = process.env) {
392
- return path.join(resolveRuntimeBase(env), "update-journal.json");
394
+ function updateTransactions(env = process.env) {
395
+ return createUpdateTransactionManager({
396
+ runtimeBase: resolveRuntimeBase(env),
397
+ packageVersion,
398
+ env,
399
+ });
393
400
  }
394
401
 
395
- function writeUpdateJournal(state, details = {}, env = process.env) {
396
- writeJsonFile(updateJournalPath(env), {
397
- schema_version: 1,
398
- state,
399
- version: packageVersion,
400
- updated_at: new Date().toISOString(),
401
- ...details,
402
+ function acquireUpdateLock(env = process.env) {
403
+ return updateTransactions(env).acquireLock();
404
+ }
405
+
406
+ function releaseUpdateLock(lock) {
407
+ return createUpdateTransactionManager({
408
+ runtimeBase: path.dirname(lock.path),
409
+ packageVersion,
410
+ }).releaseLock(lock);
411
+ }
412
+
413
+ function holdUpdateLockForTest(env = process.env) {
414
+ updateTransactions(env).holdLockForTest();
415
+ }
416
+
417
+ function readUpdateJournal(env = process.env) {
418
+ return updateTransactions(env).readJournal();
419
+ }
420
+
421
+ function integrations(env = process.env) {
422
+ return createIntegrationManager({
423
+ runtimeBase: resolveRuntimeBase(env),
424
+ packageVersion,
425
+ activeVersion: () => activeVersion(env),
426
+ });
427
+ }
428
+
429
+ function recordManagedIntegration(name, details = {}, env = process.env) {
430
+ return integrations(env).record(name, details);
431
+ }
432
+
433
+ function integrationSyncStatus(env = process.env) {
434
+ return integrations(env).status();
435
+ }
436
+
437
+ function hostIntegrations(env = process.env) {
438
+ return createHostIntegrationManager({
439
+ env,
440
+ packageRoot,
441
+ registry: integrations(env),
442
+ currentRuntimePath: currentRuntimePath(env),
443
+ openclawHome: resolveOpenclawHome(env),
444
+ hermesHome: resolveHermesHome(env),
445
+ codexHome: resolveCodexHome(env),
446
+ codexPluginRoot: resolveCodexPluginInstallRoot(env),
447
+ codexMarketplacePath: resolveCodexMarketplacePath(env),
448
+ claudeMarketplaceDir: resolveClaudeCodeMarketplaceDir(env),
449
+ claudeMarketplaceName: CLAUDE_CODE_MARKETPLACE_NAME,
450
+ expandHome,
451
+ resolveVenvPython,
452
+ commandPath,
453
+ repairRuntimeSymlink,
454
+ repairHermesEnv,
455
+ ensureCodexMarketplaceEntry,
456
+ ensureClaudeCodeMarketplace,
457
+ pinClaudeCacheCopies,
402
458
  });
403
459
  }
404
460
 
461
+ function writeUpdateJournal(state, details = {}, env = process.env) {
462
+ updateTransactions(env).writeJournal(state, details);
463
+ }
464
+
465
+ function recoverInterruptedUpdate(env = process.env) {
466
+ return updateTransactions(env).recover();
467
+ }
468
+
469
+ function updateRecoveryStatus(env = process.env) {
470
+ return updateTransactions(env).status();
471
+ }
472
+
405
473
  function writeReleaseState(runtimeRoot, state, details = {}) {
406
474
  writeJsonFile(path.join(runtimeRoot, ".agent-wallet-release.json"), {
407
475
  schema_version: 1,
@@ -423,14 +491,14 @@ function failStagingRuntime(stagingRoot, error) {
423
491
  return failedRoot;
424
492
  }
425
493
 
426
- function commitStagedRuntime(stagingRoot, releaseRoot) {
494
+ function commitStagedRuntime(stagingRoot, releaseRoot, replacedRoot = null, env = process.env) {
427
495
  if (!stagingRoot) return null;
428
- let replacedRoot = null;
429
496
  if (fs.existsSync(releaseRoot)) {
430
- replacedRoot = uniquePathWithSuffix(
497
+ replacedRoot ||= uniquePathWithSuffix(
431
498
  path.join(path.dirname(releaseRoot), `${path.basename(releaseRoot)}-replaced`),
432
499
  );
433
500
  fs.renameSync(releaseRoot, replacedRoot);
501
+ if (env.AGENT_WALLET_TEST_EXIT_AFTER_RELEASE_RENAME === "1") process.exit(86);
434
502
  }
435
503
  try {
436
504
  fs.renameSync(stagingRoot, releaseRoot);
@@ -944,146 +1012,52 @@ function writeJsonFile(pathname, value) {
944
1012
  fs.writeFileSync(pathname, `${JSON.stringify(value, null, 2)}\n`);
945
1013
  }
946
1014
 
947
- function currentBootKey(env = process.env) {
948
- const currentRoot = resolvedCurrentRuntimeRoot(env);
949
- if (!currentRoot) return "";
950
- return readEnvFile(path.join(currentRoot, "agent-wallet", ".env")).AGENT_WALLET_BOOT_KEY || "";
951
- }
952
-
953
- function readTextIfExists(pathname) {
954
- try {
955
- return fs.readFileSync(pathname, "utf8");
956
- } catch (error) {
957
- if (error?.code === "ENOENT") return "";
958
- throw error;
959
- }
960
- }
961
-
962
- function writeSecretFile(pathname, value) {
1015
+ function writeJsonFileAtomic(pathname, value, mode = 0o600) {
963
1016
  fs.mkdirSync(path.dirname(pathname), { recursive: true });
964
- fs.writeFileSync(pathname, `${String(value || "").trim()}\n`, { mode: 0o600 });
1017
+ const tempPath = `${pathname}.tmp-${process.pid}-${Date.now()}`;
965
1018
  try {
966
- fs.chmodSync(pathname, 0o600);
967
- } catch {
968
- // ignored
1019
+ fs.writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, { mode });
1020
+ fs.renameSync(tempPath, pathname);
1021
+ fs.chmodSync(pathname, mode);
1022
+ } finally {
1023
+ try {
1024
+ fs.rmSync(tempPath, { force: true });
1025
+ } catch {
1026
+ // ignored
1027
+ }
969
1028
  }
970
1029
  }
971
1030
 
972
- function resolveBootKeyFromFile(env = process.env) {
973
- const keyFile = String(env.AGENT_WALLET_BOOT_KEY_FILE || "").trim();
974
- if (!keyFile) return "";
975
- return readTextIfExists(path.resolve(expandHome(keyFile))).trim();
976
- }
977
-
978
- function defaultBootKeyFile(env = process.env) {
979
- return path.join(resolveRuntimeBase(env), "boot-key");
1031
+ function bootKeys(env = process.env) {
1032
+ return createBootKeyManager({
1033
+ runtimeBase: resolveRuntimeBase(env),
1034
+ openclawHome: resolveOpenclawHome(env),
1035
+ currentRuntimeRoot: () => resolvedCurrentRuntimeRoot(env),
1036
+ resolveVenvPython,
1037
+ expandHome,
1038
+ bridgeTimeoutMs: positiveIntEnv(
1039
+ "AGENT_WALLET_KEYSTORE_BRIDGE_TIMEOUT_MS",
1040
+ KEYSTORE_BRIDGE_TIMEOUT_MS,
1041
+ env,
1042
+ ),
1043
+ env,
1044
+ });
980
1045
  }
981
1046
 
982
1047
  function ensureBootKeyFile(env = process.env) {
983
- const configuredFile = String(env.AGENT_WALLET_BOOT_KEY_FILE || "").trim();
984
- const keyFile = configuredFile ? path.resolve(expandHome(configuredFile)) : defaultBootKeyFile(env);
985
- const existing = readTextIfExists(keyFile).trim();
986
- if (existing) {
987
- return { path: keyFile, status: "existing" };
988
- }
989
- const bootKey = String(env.AGENT_WALLET_BOOT_KEY || "").trim() || resolveBootKeyFromFile(env) || currentBootKey(env);
990
- if (!bootKey) {
991
- return { path: keyFile, status: "missing" };
992
- }
993
- writeSecretFile(keyFile, bootKey);
994
- return { path: keyFile, status: "created" };
995
- }
996
-
997
- // Read the boot key from the OS keystore via the current runtime's Python
998
- // (best-effort, "" on any failure). Lets a re-install after the runtime migration
999
- // — which moves the key into the keystore and deletes every plaintext copy — still
1000
- // resolve the existing boot key instead of refusing to touch sealed secrets.
1001
- function readBootKeyFromKeystore(env = process.env) {
1002
- const runtimeRoot = resolvedCurrentRuntimeRoot(env);
1003
- if (!runtimeRoot) return "";
1004
- const py = resolveVenvPython(runtimeRoot);
1005
- if (!py) return "";
1006
- try {
1007
- const res = spawnSync(
1008
- py,
1009
- ["-c", "from agent_wallet.config import read_boot_key_from_keystore as r; print(r())"],
1010
- {
1011
- cwd: path.join(runtimeRoot, "agent-wallet"),
1012
- encoding: "utf8",
1013
- timeout: positiveIntEnv("AGENT_WALLET_KEYSTORE_BRIDGE_TIMEOUT_MS", KEYSTORE_BRIDGE_TIMEOUT_MS, env),
1014
- env: { ...env, OPENCLAW_HOME: resolveOpenclawHome(env) },
1015
- },
1016
- );
1017
- if ((res.status ?? 1) !== 0) return "";
1018
- return String(res.stdout || "").trim();
1019
- } catch {
1020
- return "";
1021
- }
1048
+ return bootKeys(env).ensureFile();
1022
1049
  }
1023
1050
 
1024
- // New runtimes own boot-key precedence and verify candidates against
1025
- // sealed_keys.json. An import failure means the active runtime predates this
1026
- // bridge, so callers may use the legacy JS fallback below.
1027
1051
  function readBootKeyFromRuntimeResolver(env = process.env) {
1028
- const runtimeRoot = resolvedCurrentRuntimeRoot(env);
1029
- if (!runtimeRoot) return { supported: false, key: "" };
1030
- const py = resolveVenvPython(runtimeRoot);
1031
- if (!py) return { supported: false, key: "" };
1032
- try {
1033
- const res = spawnSync(
1034
- py,
1035
- ["-c", "from agent_wallet.config import resolve_boot_key_for_installer as r; print(r())"],
1036
- {
1037
- cwd: path.join(runtimeRoot, "agent-wallet"),
1038
- encoding: "utf8",
1039
- timeout: positiveIntEnv("AGENT_WALLET_KEYSTORE_BRIDGE_TIMEOUT_MS", KEYSTORE_BRIDGE_TIMEOUT_MS, env),
1040
- env: { ...env, OPENCLAW_HOME: resolveOpenclawHome(env) },
1041
- },
1042
- );
1043
- if ((res.status ?? 1) !== 0) return { supported: false, key: "" };
1044
- return { supported: true, key: String(res.stdout || "").trim() };
1045
- } catch {
1046
- return { supported: false, key: "" };
1047
- }
1052
+ return bootKeys(env).resolveFromRuntime();
1048
1053
  }
1049
1054
 
1050
1055
  function resolveLegacyInstallerBootKey(env = process.env) {
1051
- for (const [source, key] of [
1052
- ["legacy_keystore", readBootKeyFromKeystore(env)],
1053
- ["current_runtime_env", currentBootKey(env)],
1054
- ["configured_file", resolveBootKeyFromFile(env)],
1055
- ["default_file", readTextIfExists(defaultBootKeyFile(env)).trim()],
1056
- ]) {
1057
- if (key) return { key, source };
1058
- }
1059
- return { key: "", source: "none" };
1056
+ return bootKeys(env).resolveLegacy();
1060
1057
  }
1061
1058
 
1062
- // Provision the boot key into the OS keystore via the freshly installed runtime's
1063
- // Python. Returns true only when the import stored AND verified the key, so the
1064
- // caller may safely drop the plaintext .env copy. Best-effort: false on any failure
1065
- // (e.g. no usable keystore), in which case the caller keeps the legacy .env write.
1066
1059
  function provisionBootKeyToKeystore(releaseRoot, env, bootKey) {
1067
- const key = String(bootKey || "").trim();
1068
- if (!key) return false;
1069
- const py = resolveVenvPython(releaseRoot);
1070
- if (!py) return false;
1071
- try {
1072
- const res = spawnSync(
1073
- py,
1074
- ["-m", "agent_wallet.openclaw_cli", "boot-key-import", "--key-stdin"],
1075
- {
1076
- cwd: path.join(releaseRoot, "agent-wallet"),
1077
- input: key,
1078
- encoding: "utf8",
1079
- timeout: positiveIntEnv("AGENT_WALLET_KEYSTORE_BRIDGE_TIMEOUT_MS", KEYSTORE_BRIDGE_TIMEOUT_MS, env),
1080
- env: { ...env, OPENCLAW_HOME: resolveOpenclawHome(env) },
1081
- },
1082
- );
1083
- return (res.status ?? 1) === 0;
1084
- } catch {
1085
- return false;
1086
- }
1060
+ return bootKeys(env).provision(releaseRoot, bootKey);
1087
1061
  }
1088
1062
 
1089
1063
  function resolveVenvPython(releaseRoot) {
@@ -1161,6 +1135,10 @@ function verifyRuntime(releaseRoot, env = process.env) {
1161
1135
  };
1162
1136
  }
1163
1137
 
1138
+ function verifyBootKeyWithRuntime(releaseRoot, env = process.env) {
1139
+ return bootKeys(env).verifyWithRuntime(releaseRoot);
1140
+ }
1141
+
1164
1142
  function resolveEditorServerChecks(env = process.env) {
1165
1143
  const checks = [];
1166
1144
  // Claude Code's launcher (run_mcp.sh) falls back to the runtime codex
@@ -1301,10 +1279,30 @@ function runDoctor(args = []) {
1301
1279
  fix: rsync.in_sync === false ? "npm run release:local" : "",
1302
1280
  });
1303
1281
 
1282
+ const integrationSync = integrationSyncStatus(env);
1283
+ checks.push({
1284
+ name: "framework_integrations_in_sync",
1285
+ ok: true,
1286
+ in_sync: integrationSync.in_sync,
1287
+ integrations: integrationSync.integrations,
1288
+ error: "",
1289
+ fix: integrationSync.in_sync ? "" : "wallet update --yes",
1290
+ });
1291
+
1292
+ const recoveryStatus = updateRecoveryStatus(env);
1293
+ checks.push({
1294
+ name: "update_recovery_state",
1295
+ ok: true,
1296
+ ...recoveryStatus,
1297
+ error: "",
1298
+ fix: recoveryStatus.needs_recovery ? "wallet install --yes" : "",
1299
+ });
1300
+
1304
1301
  const ok = checks.every((c) => c.ok);
1305
1302
  console.log(
1306
1303
  JSON.stringify(
1307
1304
  {
1305
+ schema_version: 1,
1308
1306
  ok,
1309
1307
  package_name: packageJson.name,
1310
1308
  package_version: packageVersion,
@@ -1324,6 +1322,7 @@ function runDoctor(args = []) {
1324
1322
 
1325
1323
  function runStatus(args = []) {
1326
1324
  const payload = {
1325
+ schema_version: 1,
1327
1326
  ok: true,
1328
1327
  package_name: packageJson.name,
1329
1328
  package_version: packageVersion,
@@ -1336,6 +1335,8 @@ function runStatus(args = []) {
1336
1335
  failed_releases: listFailedReleases(),
1337
1336
  update_available: computeUpdateAvailability(),
1338
1337
  runtime_in_sync: computeRuntimeInSync(),
1338
+ framework_integrations: integrationSyncStatus(),
1339
+ update_recovery: updateRecoveryStatus(),
1339
1340
  };
1340
1341
  if (hasFlag(args, "--verbose")) {
1341
1342
  payload.verbose = true;
@@ -1353,19 +1354,18 @@ function buildInstallerEnv(args) {
1353
1354
  const sealedKeysExist = fs.existsSync(sealedKeysPath);
1354
1355
  const dryRun = hasFlag(args, "--dry-run");
1355
1356
  let bootKeySource = env.AGENT_WALLET_BOOT_KEY ? "environment" : "none";
1356
- if (!env.AGENT_WALLET_BOOT_KEY) {
1357
- const runtimeResolution = readBootKeyFromRuntimeResolver(env);
1358
- const fallback = runtimeResolution.supported
1359
- ? {
1360
- key: runtimeResolution.key,
1361
- source: runtimeResolution.key ? "runtime_verified" : "runtime_rejected",
1362
- }
1363
- : resolveLegacyInstallerBootKey(env);
1364
- const existingBootKey = fallback.key;
1365
- bootKeySource = fallback.source;
1366
- if (existingBootKey) {
1367
- env.AGENT_WALLET_BOOT_KEY = existingBootKey;
1357
+ const runtimeResolution = readBootKeyFromRuntimeResolver(env);
1358
+ if (runtimeResolution.supported) {
1359
+ bootKeySource = runtimeResolution.key ? "runtime_verified" : "runtime_rejected";
1360
+ if (runtimeResolution.key) {
1361
+ env.AGENT_WALLET_BOOT_KEY = runtimeResolution.key;
1362
+ } else {
1363
+ delete env.AGENT_WALLET_BOOT_KEY;
1368
1364
  }
1365
+ } else if (!env.AGENT_WALLET_BOOT_KEY) {
1366
+ const fallback = resolveLegacyInstallerBootKey(env);
1367
+ bootKeySource = fallback.source;
1368
+ if (fallback.key) env.AGENT_WALLET_BOOT_KEY = fallback.key;
1369
1369
  }
1370
1370
 
1371
1371
  const shouldGenerateSecrets =
@@ -1395,7 +1395,7 @@ function buildInstallerEnv(args) {
1395
1395
  return { env, generated, bootKeySource };
1396
1396
  }
1397
1397
 
1398
- function runInstall(args, { commandName = "install" } = {}) {
1398
+ function runInstallUnlocked(args, { commandName = "install" } = {}) {
1399
1399
  if (!fs.existsSync(setupPath)) {
1400
1400
  console.error(`Missing bundled setup.sh at ${setupPath}`);
1401
1401
  return 1;
@@ -1409,6 +1409,13 @@ function runInstall(args, { commandName = "install" } = {}) {
1409
1409
  const previousPath = previousRuntimePath();
1410
1410
  const installerArgs = withoutCliOnlyArgs(args);
1411
1411
  const dryRun = hasFlag(args, "--dry-run");
1412
+ const recovery = dryRun
1413
+ ? { attempted: false, ok: true, reason: "dry run" }
1414
+ : recoverInterruptedUpdate(process.env);
1415
+ if (!recovery.ok) {
1416
+ console.error(`Could not recover interrupted update: ${recovery.reason || recovery.error}`);
1417
+ return 1;
1418
+ }
1412
1419
  const stagingRoot = !explicitRuntimeRoot && !dryRun ? stagingRootFor(packageVersion) : null;
1413
1420
  const installRoot = stagingRoot || releaseRoot;
1414
1421
 
@@ -1429,7 +1436,17 @@ function runInstall(args, { commandName = "install" } = {}) {
1429
1436
  const { env, generated, bootKeySource } = installerEnv;
1430
1437
  if (stagingRoot) {
1431
1438
  env.OPENCLAW_INSTALL_FINAL_ROOT = releaseRoot;
1432
- writeUpdateJournal("preparing", { staging_root: stagingRoot, release_root: releaseRoot }, env);
1439
+ writeUpdateJournal(
1440
+ "preparing",
1441
+ {
1442
+ transaction_id: crypto.randomUUID(),
1443
+ staging_root: stagingRoot,
1444
+ release_root: releaseRoot,
1445
+ current_target_before: readLinkOrNull(currentPath),
1446
+ previous_target_before: readLinkOrNull(previousPath),
1447
+ },
1448
+ env,
1449
+ );
1433
1450
  }
1434
1451
  const result = spawnSync("sh", [setupPath, ...installerArgs], {
1435
1452
  cwd: packageRoot,
@@ -1523,6 +1540,34 @@ function runInstall(args, { commandName = "install" } = {}) {
1523
1540
  return 1;
1524
1541
  }
1525
1542
 
1543
+ const bootKeyVerification = verifyBootKeyWithRuntime(installRoot, env);
1544
+ if (!bootKeyVerification.ok) {
1545
+ const failedRoot = failStagingRuntime(stagingRoot, bootKeyVerification.error);
1546
+ writeUpdateJournal(
1547
+ "failed",
1548
+ { failed_runtime: failedRoot, error: bootKeyVerification.error },
1549
+ env,
1550
+ );
1551
+ console.error(
1552
+ JSON.stringify(
1553
+ {
1554
+ ok: false,
1555
+ command: commandName,
1556
+ version: packageVersion,
1557
+ category: "boot_key_rejected",
1558
+ error: bootKeyVerification.error,
1559
+ switched_current: false,
1560
+ failed_runtime: failedRoot,
1561
+ current_runtime_target: readLinkOrNull(currentPath),
1562
+ fix: "Remove stale AGENT_WALLET_BOOT_KEY overrides and retry the update.",
1563
+ },
1564
+ null,
1565
+ 2,
1566
+ ),
1567
+ );
1568
+ return 1;
1569
+ }
1570
+
1526
1571
  if (env.AGENT_WALLET_BOOT_KEY) {
1527
1572
  const envPath = path.join(installRoot, "agent-wallet", ".env");
1528
1573
  // Prefer the OS keystore: provision the boot key there and keep the plaintext
@@ -1540,11 +1585,32 @@ function runInstall(args, { commandName = "install" } = {}) {
1540
1585
  writeReleaseState(installRoot, "verified", {
1541
1586
  verification_skipped: Boolean(verification.skipped),
1542
1587
  });
1543
- writeUpdateJournal("verified", { staging_root: stagingRoot, release_root: releaseRoot }, env);
1588
+ writeUpdateJournal(
1589
+ "verified",
1590
+ { ...readUpdateJournal(env), staging_root: stagingRoot, release_root: releaseRoot },
1591
+ env,
1592
+ );
1544
1593
 
1545
1594
  let replacedRoot = null;
1546
1595
  try {
1547
- replacedRoot = commitStagedRuntime(stagingRoot, releaseRoot);
1596
+ replacedRoot = fs.existsSync(releaseRoot)
1597
+ ? uniquePathWithSuffix(
1598
+ path.join(path.dirname(releaseRoot), `${path.basename(releaseRoot)}-replaced`),
1599
+ )
1600
+ : null;
1601
+ writeUpdateJournal(
1602
+ "committing",
1603
+ {
1604
+ ...readUpdateJournal(env),
1605
+ staging_root: stagingRoot,
1606
+ release_root: releaseRoot,
1607
+ replaced_root: replacedRoot,
1608
+ current_target_before: readLinkOrNull(currentPath),
1609
+ previous_target_before: readLinkOrNull(previousPath),
1610
+ },
1611
+ env,
1612
+ );
1613
+ replacedRoot = commitStagedRuntime(stagingRoot, releaseRoot, replacedRoot, env);
1548
1614
  } catch (error) {
1549
1615
  const failedRoot = failStagingRuntime(stagingRoot, error.message);
1550
1616
  writeUpdateJournal("failed", { failed_runtime: failedRoot, error: error.message }, env);
@@ -1560,9 +1626,29 @@ function runInstall(args, { commandName = "install" } = {}) {
1560
1626
  switchSymlink(previousPath, previousTarget);
1561
1627
  }
1562
1628
  switchSymlink(currentPath, releaseRoot);
1563
- writeUpdateJournal("committed", { release_root: releaseRoot, previous_runtime: previousTarget }, env);
1629
+ writeUpdateJournal(
1630
+ "committed",
1631
+ { ...readUpdateJournal(env), release_root: releaseRoot, previous_runtime: previousTarget },
1632
+ env,
1633
+ );
1634
+
1635
+ recordManagedIntegration(
1636
+ "openclaw",
1637
+ {
1638
+ config_path: path.resolve(
1639
+ expandHome(parseFlagValue(args, "--config-path") || path.join(resolveOpenclawHome(env), "openclaw.json")),
1640
+ ),
1641
+ extension_path: path.join(currentPath, ".openclaw", "extensions", "agent-wallet"),
1642
+ package_root: path.join(currentPath, "agent-wallet"),
1643
+ },
1644
+ env,
1645
+ );
1564
1646
 
1565
1647
  const integrationRefresh = repairInstalledEditorIntegrations(env);
1648
+ const globalCliRefresh = safelyRefreshIntegration(
1649
+ "global-cli",
1650
+ () => refreshGlobalCliIfNeeded(env),
1651
+ );
1566
1652
 
1567
1653
  const pythonInfo = activePythonRuntimeInfo(env);
1568
1654
  const nodeInfo = activeNodeRuntimeInfo(env)
@@ -1585,7 +1671,9 @@ function runInstall(args, { commandName = "install" } = {}) {
1585
1671
  boot_key_source: bootKeySource,
1586
1672
  staged: Boolean(stagingRoot),
1587
1673
  release_state: "verified",
1674
+ recovery,
1588
1675
  integration_refresh: integrationRefresh,
1676
+ global_cli_refresh: globalCliRefresh,
1589
1677
  },
1590
1678
  null,
1591
1679
  2,
@@ -1594,6 +1682,34 @@ function runInstall(args, { commandName = "install" } = {}) {
1594
1682
  return 0;
1595
1683
  }
1596
1684
 
1685
+ function runInstall(args, options = {}) {
1686
+ if (hasFlag(args, "--dry-run")) return runInstallUnlocked(args, options);
1687
+ const lock = acquireUpdateLock(process.env);
1688
+ if (!lock.ok) {
1689
+ console.error(
1690
+ JSON.stringify(
1691
+ {
1692
+ ok: false,
1693
+ category: "update_locked",
1694
+ error: "Another wallet install or update is already running.",
1695
+ lock_path: lock.path,
1696
+ lock_owner: lock.owner,
1697
+ fix: "Wait for the active update to finish, then retry.",
1698
+ },
1699
+ null,
1700
+ 2,
1701
+ ),
1702
+ );
1703
+ return 1;
1704
+ }
1705
+ try {
1706
+ holdUpdateLockForTest(process.env);
1707
+ return runInstallUnlocked(args, options);
1708
+ } finally {
1709
+ releaseUpdateLock(lock);
1710
+ }
1711
+ }
1712
+
1597
1713
  function resolveUpdatePackageSpec(env = process.env) {
1598
1714
  const explicit = String(env[UPDATE_PACKAGE_SPEC_ENV] || "").trim();
1599
1715
  if (explicit) return explicit;
@@ -2001,6 +2117,14 @@ function runHermesInstall(args) {
2001
2117
  }
2002
2118
  }
2003
2119
 
2120
+ recordManagedIntegration("hermes", {
2121
+ hermes_home: hermesHome,
2122
+ plugin_target: pluginTarget,
2123
+ env_path: hermesEnvPath,
2124
+ restart_required: true,
2125
+ ...(enable.skipped ? {} : { registration_ok: enable.ok }),
2126
+ });
2127
+
2004
2128
  console.log(
2005
2129
  JSON.stringify(
2006
2130
  {
@@ -2086,6 +2210,15 @@ function runCodexInstall(args) {
2086
2210
  }
2087
2211
  }
2088
2212
 
2213
+ recordManagedIntegration("codex", {
2214
+ codex_home: codexHome,
2215
+ plugin_target: pluginTarget,
2216
+ marketplace_path: marketplace.marketplace_path,
2217
+ marketplace_name: marketplace.marketplace_name,
2218
+ restart_required: true,
2219
+ ...(add.skipped ? {} : { registration_ok: add.ok }),
2220
+ });
2221
+
2089
2222
  console.log(
2090
2223
  JSON.stringify(
2091
2224
  {
@@ -2281,6 +2414,16 @@ function runClaudeCodeInstall(args) {
2281
2414
  }
2282
2415
  }
2283
2416
 
2417
+ recordManagedIntegration("claude-code", {
2418
+ marketplace_dir: marketplaceDir,
2419
+ plugin_target: pluginLink,
2420
+ cache_root: path.resolve(
2421
+ expandHome(process.env.AGENT_WALLET_CLAUDE_CODE_CACHE_ROOT || "~/.claude/plugins/cache"),
2422
+ ),
2423
+ restart_required: true,
2424
+ ...(enable.skipped ? {} : { registration_ok: enable.ok }),
2425
+ });
2426
+
2284
2427
  const ok = enable.skipped || enable.ok;
2285
2428
  const pinnedCache = pinClaudeCacheCopies();
2286
2429
  const pluginDirFlagFull = `claude --plugin-dir ${pluginSource}`;
@@ -2328,7 +2471,7 @@ function pathEntryExists(pathname) {
2328
2471
  }
2329
2472
  }
2330
2473
 
2331
- function repairRuntimeSymlink(name, linkPath, desiredTarget, env = process.env) {
2474
+ function repairRuntimeSymlink(name, linkPath, desiredTarget, env = process.env, { allowExternal = false } = {}) {
2332
2475
  let rawTarget;
2333
2476
  try {
2334
2477
  const stat = fs.lstatSync(linkPath);
@@ -2348,7 +2491,7 @@ function repairRuntimeSymlink(name, linkPath, desiredTarget, env = process.env)
2348
2491
  if (absoluteTarget === path.resolve(desiredTarget) || absoluteTarget.startsWith(`${logicalCurrent}${path.sep}`)) {
2349
2492
  return { name, attempted: true, ok: true, repaired: false, reason: "already current" };
2350
2493
  }
2351
- if (!runtimeReleasePath(absoluteTarget, env)) {
2494
+ if (!allowExternal && !runtimeReleasePath(absoluteTarget, env)) {
2352
2495
  return { name, attempted: false, ok: true, repaired: false, reason: "external target preserved" };
2353
2496
  }
2354
2497
  if (!fs.existsSync(desiredTarget)) {
@@ -2358,8 +2501,7 @@ function repairRuntimeSymlink(name, linkPath, desiredTarget, env = process.env)
2358
2501
  return { name, attempted: true, ok: true, repaired: true, target: desiredTarget };
2359
2502
  }
2360
2503
 
2361
- function repairHermesEnv(env = process.env) {
2362
- const envPath = path.join(resolveHermesHome(env), ".env");
2504
+ function repairHermesEnv(envPath, env = process.env) {
2363
2505
  const existing = readEnvFile(envPath);
2364
2506
  const repaired = [];
2365
2507
  const currentPackage = path.join(currentRuntimePath(env), "agent-wallet");
@@ -2379,45 +2521,63 @@ function repairHermesEnv(env = process.env) {
2379
2521
  return repaired;
2380
2522
  }
2381
2523
 
2382
- function repairInstalledEditorIntegrations(env = process.env) {
2383
- const results = [];
2384
- const currentRoot = currentRuntimePath(env);
2385
- const hermesTarget = path.join(resolveHermesHome(env), "plugins", "agent_wallet");
2386
- if (pathEntryExists(hermesTarget) || fs.existsSync(path.join(resolveHermesHome(env), ".env"))) {
2387
- const result = repairRuntimeSymlink(
2388
- "hermes",
2389
- hermesTarget,
2390
- path.join(currentRoot, "hermes", "plugins", "agent_wallet"),
2391
- env,
2392
- );
2393
- result.env_repaired = repairHermesEnv(env);
2394
- results.push(result);
2524
+ function globalNpmPackageInfo(env = process.env) {
2525
+ const npmBin = commandPath("npm");
2526
+ if (!npmBin) return null;
2527
+ const rootResult = spawnSync(npmBin, ["root", "--global"], { encoding: "utf8", env });
2528
+ if (rootResult.status !== 0) return null;
2529
+ const packageRoot = path.join(rootResult.stdout.trim(), ...packageJson.name.split("/"));
2530
+ try {
2531
+ const manifest = readJsonFile(path.join(packageRoot, "package.json"));
2532
+ if (manifest?.name !== packageJson.name) return null;
2533
+ return { npm_bin: npmBin, package_root: packageRoot, version: manifest.version || null };
2534
+ } catch {
2535
+ return null;
2395
2536
  }
2537
+ }
2396
2538
 
2397
- const codexTarget = path.join(resolveCodexPluginInstallRoot(env), "agent-wallet");
2398
- if (pathEntryExists(codexTarget)) {
2399
- results.push(
2400
- repairRuntimeSymlink(
2401
- "codex",
2402
- codexTarget,
2403
- path.join(currentRoot, "codex", "plugins", "agent-wallet"),
2404
- env,
2405
- ),
2406
- );
2539
+ function refreshGlobalCliIfNeeded(env = process.env) {
2540
+ const installed = globalNpmPackageInfo(env);
2541
+ if (!installed) {
2542
+ return { attempted: false, ok: true, reason: "global package is not installed" };
2407
2543
  }
2408
-
2409
- const claudeTarget = path.join(resolveClaudeCodeMarketplaceDir(env), "plugins", "agent-wallet");
2410
- if (pathEntryExists(claudeTarget)) {
2411
- const result = repairRuntimeSymlink(
2412
- "claude-code",
2413
- claudeTarget,
2414
- path.join(currentRoot, "claude-code", "plugins", "agent-wallet"),
2415
- env,
2416
- );
2417
- result.cache_pins = pinClaudeCacheCopies(env);
2418
- results.push(result);
2544
+ if (installed.version === packageVersion) {
2545
+ return { attempted: false, ok: true, reason: "already current", version: packageVersion };
2419
2546
  }
2420
- return results;
2547
+ const fromNpmCache = path.resolve(packageRoot).split(path.sep).includes("_npx");
2548
+ const forced = env.AGENT_WALLET_FORCE_GLOBAL_CLI_REFRESH === "1";
2549
+ if (!fromNpmCache && !forced) {
2550
+ return {
2551
+ attempted: false,
2552
+ ok: false,
2553
+ reason: "installer is not running from npm exec",
2554
+ installed_version: installed.version,
2555
+ target_version: packageVersion,
2556
+ fix: `npm install --global ${packageJson.name}@${packageVersion}`,
2557
+ };
2558
+ }
2559
+ const packageSpec = `${packageJson.name}@${packageVersion}`;
2560
+ const result = spawnSync(
2561
+ installed.npm_bin,
2562
+ ["install", "--global", "--no-audit", "--no-fund", packageSpec],
2563
+ { encoding: "utf8", env },
2564
+ );
2565
+ return {
2566
+ attempted: true,
2567
+ ok: result.status === 0,
2568
+ previous_version: installed.version,
2569
+ target_version: packageVersion,
2570
+ error: result.status === 0 ? "" : (result.stderr || result.stdout || "").trim(),
2571
+ fix: result.status === 0 ? "" : `npm install --global ${packageSpec}`,
2572
+ };
2573
+ }
2574
+
2575
+ function safelyRefreshIntegration(name, callback) {
2576
+ return integrations().safelyRefresh(name, callback);
2577
+ }
2578
+
2579
+ function repairInstalledEditorIntegrations(env = process.env) {
2580
+ return hostIntegrations(env).refreshAll();
2421
2581
  }
2422
2582
 
2423
2583
  const args = process.argv.slice(2);