@agentlayer.tech/wallet 0.1.89 → 0.1.91

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.
Files changed (33) hide show
  1. package/.openclaw/extensions/agent-wallet/dist/index.js +5 -5
  2. package/.openclaw/extensions/agent-wallet/index.ts +5 -5
  3. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +1 -1
  4. package/.openclaw/extensions/agent-wallet/package.json +1 -1
  5. package/CHANGELOG.md +14 -0
  6. package/README.md +46 -23
  7. package/VERSION +1 -1
  8. package/agent-wallet/README.md +26 -1
  9. package/agent-wallet/UPGRADE_COMPATIBILITY.md +2 -1
  10. package/agent-wallet/agent_wallet/__init__.py +1 -1
  11. package/agent-wallet/agent_wallet/autonomous_permissions.py +43 -1
  12. package/agent-wallet/agent_wallet/openclaw_adapter.py +604 -375
  13. package/agent-wallet/agent_wallet/providers/x402.py +84 -0
  14. package/agent-wallet/openclaw.plugin.json +1 -1
  15. package/agent-wallet/pyproject.toml +1 -1
  16. package/agent-wallet/scripts/install_agent_wallet.py +197 -11
  17. package/agent-wallet/scripts/install_openclaw_local_config.py +4 -1
  18. package/agent-wallet/scripts/install_openclaw_sealed_keys.py +6 -2
  19. package/bin/lib/host-detection.mjs +236 -0
  20. package/bin/lib/integrations.mjs +69 -5
  21. package/bin/openclaw-agent-wallet.mjs +398 -28
  22. package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
  23. package/claude-code/plugins/agent-wallet/README.md +3 -1
  24. package/claude-code/plugins/agent-wallet/commands/agentlayer-autonomous-approve.md +7 -8
  25. package/claude-code/plugins/agent-wallet/commands/agentlayer-autonomous-revoke.md +2 -2
  26. package/claude-code/plugins/agent-wallet/commands/wallet-base.md +38 -15
  27. package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
  28. package/codex/plugins/agent-wallet/README.md +11 -6
  29. package/codex/plugins/agent-wallet/skills/wallet-base/SKILL.md +56 -0
  30. package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
  31. package/package.json +8 -3
  32. package/wdk-btc-wallet/package.json +1 -1
  33. package/wdk-evm-wallet/package.json +1 -1
@@ -81,6 +81,24 @@ export function createIntegrationManager({ runtimeBase, packageVersion, activeVe
81
81
  return registry.integrations[name];
82
82
  }
83
83
 
84
+ function recoverCorruptRegistry() {
85
+ const registry = readRegistry();
86
+ if (!registry.registry_error) {
87
+ return { recovered: false, backup: null };
88
+ }
89
+ const corruptBackup = quarantineCorruptRegistry(registry);
90
+ const recovered = emptyRegistry();
91
+ if (corruptBackup) {
92
+ recovered.recovered_corrupt_registry = path.basename(corruptBackup);
93
+ }
94
+ recovered.updated_at = new Date().toISOString();
95
+ writeJsonAtomic(registryPath, recovered);
96
+ return {
97
+ recovered: true,
98
+ backup: corruptBackup,
99
+ };
100
+ }
101
+
84
102
  function managed(name) {
85
103
  const entry = readRegistry().integrations[name];
86
104
  return entry?.managed === true ? entry : null;
@@ -140,7 +158,16 @@ export function createIntegrationManager({ runtimeBase, packageVersion, activeVe
140
158
  .filter(Boolean);
141
159
  }
142
160
 
143
- return { registryPath, readRegistry, record, managed, status, safelyRefresh, refreshAll };
161
+ return {
162
+ registryPath,
163
+ readRegistry,
164
+ record,
165
+ recoverCorruptRegistry,
166
+ managed,
167
+ status,
168
+ safelyRefresh,
169
+ refreshAll,
170
+ };
144
171
  }
145
172
 
146
173
  export function createHostIntegrationManager({
@@ -190,6 +217,37 @@ export function createHostIntegrationManager({
190
217
  }
191
218
  }
192
219
 
220
+ function adoptOpenclaw() {
221
+ const configPath = path.join(openclawHome, "openclaw.json");
222
+ let config;
223
+ try {
224
+ config = readJson(configPath);
225
+ } catch {
226
+ return null;
227
+ }
228
+ const walletEntry = config?.plugins?.entries?.["agent-wallet"];
229
+ const packageRoot = walletEntry?.config?.packageRoot;
230
+ const loadPaths = Array.isArray(config?.plugins?.load?.paths)
231
+ ? config.plugins.load.paths
232
+ : [];
233
+ const ownedPackage =
234
+ typeof packageRoot === "string" &&
235
+ packageRoot.trim() &&
236
+ runtimeOwnedTarget(packageRoot);
237
+ const ownedExtension = loadPaths.some(
238
+ (item) =>
239
+ String(item || "").replaceAll("\\", "/").endsWith("/.openclaw/extensions/agent-wallet") &&
240
+ runtimeOwnedTarget(String(item)),
241
+ );
242
+ if (!walletEntry || (!ownedPackage && !ownedExtension)) return null;
243
+ return registry.record("openclaw", {
244
+ config_path: configPath,
245
+ extension_path: path.join(currentRuntimePath, ".openclaw", "extensions", "agent-wallet"),
246
+ package_root: path.join(currentRuntimePath, "agent-wallet"),
247
+ adopted_legacy_install: true,
248
+ });
249
+ }
250
+
193
251
  function adoptHermes() {
194
252
  const pluginTarget = path.join(hermesHome, "plugins", "agent_wallet");
195
253
  const target = symlinkTarget(pluginTarget);
@@ -268,7 +326,7 @@ export function createHostIntegrationManager({
268
326
  }
269
327
 
270
328
  function refreshOpenclaw() {
271
- const entry = registry.managed("openclaw");
329
+ const entry = registry.managed("openclaw") || adoptOpenclaw();
272
330
  if (!entry) {
273
331
  return { name: "openclaw", attempted: false, ok: true, repaired: false, reason: "not managed" };
274
332
  }
@@ -411,13 +469,19 @@ export function createHostIntegrationManager({
411
469
  };
412
470
  }
413
471
 
414
- function refreshAll() {
415
- return registry.refreshAll({
472
+ function refreshAll(names = null) {
473
+ const selected = names ? new Set(names) : null;
474
+ const refreshers = {
416
475
  openclaw: refreshOpenclaw,
417
476
  hermes: refreshHermes,
418
477
  codex: refreshCodex,
419
478
  "claude-code": refreshClaudeCode,
420
- });
479
+ };
480
+ return registry.refreshAll(
481
+ Object.fromEntries(
482
+ Object.entries(refreshers).filter(([name]) => !selected || selected.has(name)),
483
+ ),
484
+ );
421
485
  }
422
486
 
423
487
  return { refreshAll };
@@ -5,8 +5,14 @@ import crypto from "node:crypto";
5
5
  import fs from "node:fs";
6
6
  import os from "node:os";
7
7
  import path from "node:path";
8
+ import readline from "node:readline/promises";
8
9
  import { fileURLToPath } from "node:url";
9
10
  import { createBootKeyManager } from "./lib/boot-key.mjs";
11
+ import {
12
+ buildInstallPlan,
13
+ detectHosts,
14
+ stripUniversalInstallerArgs,
15
+ } from "./lib/host-detection.mjs";
10
16
  import { createHostIntegrationManager, createIntegrationManager } from "./lib/integrations.mjs";
11
17
  import { createUpdateTransactionManager } from "./lib/update-transaction.mjs";
12
18
 
@@ -33,6 +39,7 @@ function printHelp() {
33
39
 
34
40
  Usage:
35
41
  openclaw-agent-wallet install [options]
42
+ openclaw-agent-wallet detect [--json]
36
43
  openclaw-agent-wallet hermes install [options]
37
44
  openclaw-agent-wallet codex install [options]
38
45
  openclaw-agent-wallet claude-code install [options]
@@ -44,12 +51,20 @@ Usage:
44
51
 
45
52
  Common install options:
46
53
  --yes Generate local runtime secrets when missing.
54
+ --hosts <list> Hosts to install: detected, all, none, or comma-separated names.
55
+ --exclude <list> Detected/managed hosts to leave untouched.
56
+ --runtime-only Install/update the shared runtime without adding host plugins.
57
+ --no-prompt Accept the automatic host selection in interactive terminals.
47
58
  --no-auto-secrets Do not generate runtime secrets automatically.
48
59
  --backend <backend> solana_local, wdk_btc_local, wdk_evm_local, or none.
49
60
  --network <network> devnet, mainnet, base, ethereum, bitcoin, etc.
61
+ --invite <code> Bind a welcome invite to the local Base address.
50
62
 
51
63
  Examples:
52
64
  npx @agentlayer.tech/wallet install --yes
65
+ npx @agentlayer.tech/wallet install --yes --hosts codex,claude-code
66
+ npx @agentlayer.tech/wallet detect --json
67
+ npx @agentlayer.tech/wallet install --yes --invite alw_...
53
68
  npx @agentlayer.tech/wallet hermes install --yes
54
69
  npx @agentlayer.tech/wallet codex install --yes
55
70
  npx @agentlayer.tech/wallet claude-code install --yes
@@ -322,6 +337,32 @@ function runWithCliTelemetry(fn, { startEvent, successEvent, failedEvent, comman
322
337
  return code;
323
338
  }
324
339
 
340
+ async function runWithCliTelemetryAsync(
341
+ fn,
342
+ { startEvent, successEvent, failedEvent, commandName, host = "", args = [] },
343
+ ) {
344
+ recordCliTelemetry(startEvent, { commandName, host, ok: true, args, flush: false });
345
+ let code;
346
+ try {
347
+ code = await fn();
348
+ } catch (error) {
349
+ recordCliTelemetry(failedEvent, {
350
+ commandName,
351
+ host,
352
+ ok: false,
353
+ args,
354
+ });
355
+ throw error;
356
+ }
357
+ recordCliTelemetry(code === 0 ? successEvent : failedEvent, {
358
+ commandName,
359
+ host,
360
+ ok: code === 0,
361
+ args,
362
+ });
363
+ return code;
364
+ }
365
+
325
366
  // Shared with the Python runtime (agent_wallet/update_check.py): the cache lives
326
367
  // under OPENCLAW_HOME/agent-wallet-runtime regardless of OPENCLAW_INSTALL_ROOT.
327
368
  function updateCheckCachePath(env = process.env) {
@@ -458,6 +499,134 @@ function hostIntegrations(env = process.env) {
458
499
  });
459
500
  }
460
501
 
502
+ function managedHostNames(env = process.env) {
503
+ const registry = integrations(env).readRegistry();
504
+ return Object.entries(registry.integrations || {})
505
+ .filter(([, entry]) => entry?.managed === true)
506
+ .map(([name]) => name);
507
+ }
508
+
509
+ function runtimeInstalled(env = process.env) {
510
+ try {
511
+ const stat = fs.lstatSync(currentRuntimePath(env));
512
+ return stat.isSymbolicLink() || stat.isDirectory();
513
+ } catch (error) {
514
+ if (error?.code === "ENOENT") return false;
515
+ throw error;
516
+ }
517
+ }
518
+
519
+ function universalInstallPlan(args, env = process.env) {
520
+ const detections = detectHosts({
521
+ env,
522
+ commandPath,
523
+ });
524
+ const managedHosts = managedHostNames(env);
525
+ const plan = buildInstallPlan({
526
+ args,
527
+ detections: detections.map((entry) => ({
528
+ ...entry,
529
+ managed: managedHosts.includes(entry.name),
530
+ })),
531
+ managedHosts,
532
+ runtimeInstalled: runtimeInstalled(env),
533
+ });
534
+ const universalSelectionExplicit =
535
+ hasFlag(args, "--hosts") ||
536
+ hasFlag(args, "--exclude") ||
537
+ hasFlag(args, "--runtime-only") ||
538
+ hasFlag(args, "--managed-only");
539
+ if (
540
+ !plan.runtime_installed_before &&
541
+ !universalSelectionExplicit &&
542
+ hasFlag(args, "--config-path")
543
+ ) {
544
+ plan.selected_hosts = ["openclaw"];
545
+ plan.selection_reason = "explicit_openclaw_config";
546
+ }
547
+ return plan;
548
+ }
549
+
550
+ function runDetect(args = [], env = process.env) {
551
+ let plan;
552
+ try {
553
+ plan = universalInstallPlan(args, env);
554
+ } catch (error) {
555
+ console.error(error.message);
556
+ return 2;
557
+ }
558
+ console.log(
559
+ JSON.stringify(
560
+ {
561
+ schema_version: 1,
562
+ ok: true,
563
+ runtime_base: resolveRuntimeBase(env),
564
+ runtime_installed: plan.runtime_installed_before,
565
+ detected_hosts: plan.detected_hosts,
566
+ managed_hosts: plan.managed_hosts,
567
+ default_selected_hosts: plan.selected_hosts,
568
+ selection_reason: plan.selection_reason,
569
+ hosts: plan.detections,
570
+ },
571
+ null,
572
+ 2,
573
+ ),
574
+ );
575
+ return 0;
576
+ }
577
+
578
+ async function promptForInstallPlan(plan, args) {
579
+ const shouldPrompt =
580
+ !plan.runtime_installed_before &&
581
+ plan.selection_reason === "fresh_install_detected" &&
582
+ !hasFlag(args, "--yes") &&
583
+ !hasFlag(args, "--no-prompt") &&
584
+ process.stdin.isTTY &&
585
+ process.stdout.isTTY;
586
+ if (!shouldPrompt) return plan;
587
+
588
+ const excluded = new Set(plan.excluded_hosts);
589
+ const selected = new Set(plan.selected_hosts);
590
+ const terminal = readline.createInterface({
591
+ input: process.stdin,
592
+ output: process.stdout,
593
+ });
594
+ try {
595
+ console.log("AgentLayer detected these agent frameworks:");
596
+ for (const host of plan.detections.filter((entry) => entry.detected && !excluded.has(entry.name))) {
597
+ const answer = (
598
+ await terminal.question(
599
+ ` Install AgentLayer for ${host.display_name}? ${selected.has(host.name) ? "[Y/n]" : "[y/N]"} `,
600
+ )
601
+ ).trim().toLowerCase();
602
+ if (answer) {
603
+ if (["y", "yes"].includes(answer)) selected.add(host.name);
604
+ if (["n", "no"].includes(answer)) selected.delete(host.name);
605
+ }
606
+ }
607
+ } finally {
608
+ terminal.close();
609
+ }
610
+ return {
611
+ ...plan,
612
+ selection_reason: "interactive",
613
+ selected_hosts: plan.detections
614
+ .map((entry) => entry.name)
615
+ .filter((name) => selected.has(name)),
616
+ };
617
+ }
618
+
619
+ async function runUniversalInstall(args, options = {}) {
620
+ try {
621
+ const initialPlan = universalInstallPlan(args);
622
+ const installPlan = await promptForInstallPlan(initialPlan, args);
623
+ return runInstall(args, { ...options, installPlan });
624
+ } catch (error) {
625
+ console.error(error.message);
626
+ return 2;
627
+ }
628
+ }
629
+
461
630
  function writeUpdateJournal(state, details = {}, env = process.env) {
462
631
  updateTransactions(env).writeJournal(state, details);
463
632
  }
@@ -855,7 +1024,13 @@ function withoutCliOnlyArgs(args) {
855
1024
  const output = [];
856
1025
  for (let index = 0; index < args.length; index += 1) {
857
1026
  const value = args[index];
858
- if (value === "--yes" || value === "--auto-secrets" || value === "--no-auto-secrets") {
1027
+ if (
1028
+ value === "--yes" ||
1029
+ value === "--auto-secrets" ||
1030
+ value === "--no-auto-secrets" ||
1031
+ value === "--force" ||
1032
+ value === "--skip-enable"
1033
+ ) {
859
1034
  continue;
860
1035
  }
861
1036
  if (value === "--to") {
@@ -867,7 +1042,7 @@ function withoutCliOnlyArgs(args) {
867
1042
  }
868
1043
  output.push(value);
869
1044
  }
870
- return output;
1045
+ return stripUniversalInstallerArgs(output);
871
1046
  }
872
1047
 
873
1048
  function extractTrailingJson(text) {
@@ -1060,6 +1235,15 @@ function provisionBootKeyToKeystore(releaseRoot, env, bootKey) {
1060
1235
  return bootKeys(env).provision(releaseRoot, bootKey);
1061
1236
  }
1062
1237
 
1238
+ function printMacOSKeychainNotice(env = process.env) {
1239
+ if (process.platform !== "darwin") return;
1240
+ const preference = String(env.AGENT_WALLET_KEYSTORE_BACKEND || "auto").trim().toLowerCase();
1241
+ if (["plain", "plaintext", "plaintext-file", "file"].includes(preference)) return;
1242
+ console.error(
1243
+ "Security notice: AgentLayer uses your macOS login Keychain for the wallet boot key by default. macOS may ask for Keychain access; approve it only if you initiated this install or update.",
1244
+ );
1245
+ }
1246
+
1063
1247
  function resolveVenvPython(releaseRoot) {
1064
1248
  const candidates = [
1065
1249
  path.join(releaseRoot, "agent-wallet", ".venv", "bin", "python"),
@@ -1395,7 +1579,158 @@ function buildInstallerEnv(args) {
1395
1579
  return { env, generated, bootKeySource };
1396
1580
  }
1397
1581
 
1398
- function runInstallUnlocked(args, { commandName = "install" } = {}) {
1582
+ function openclawHostCommandArgs(args, env = process.env) {
1583
+ const runtimeRoot = currentRuntimePath(env);
1584
+ const walletRoot = path.join(runtimeRoot, "agent-wallet");
1585
+ const pythonBin = resolveVenvPython(runtimeRoot) || resolveAgentWalletPython(walletRoot);
1586
+ const commandArgs = [
1587
+ path.join(walletRoot, "scripts", "install_openclaw_local_config.py"),
1588
+ "--config-path",
1589
+ path.resolve(
1590
+ expandHome(parseFlagValue(args, "--config-path") || path.join(resolveOpenclawHome(env), "openclaw.json")),
1591
+ ),
1592
+ "--plugin-id",
1593
+ parseFlagValue(args, "--plugin-id") || "agent-wallet",
1594
+ "--user-id",
1595
+ parseFlagValue(args, "--user-id") || "",
1596
+ "--backend",
1597
+ parseFlagValue(args, "--backend") || "solana_local",
1598
+ "--network",
1599
+ parseFlagValue(args, "--network") || "mainnet",
1600
+ hasFlag(args, "--sign-only") ? "--sign-only" : "--no-sign-only",
1601
+ "--extension-path",
1602
+ path.join(runtimeRoot, ".openclaw", "extensions", "agent-wallet"),
1603
+ "--package-root",
1604
+ walletRoot,
1605
+ "--python-bin",
1606
+ pythonBin,
1607
+ ];
1608
+ for (const [flag, targetFlag] of [
1609
+ ["--rpc-url", "--rpc-url"],
1610
+ ["--rpc-urls", "--rpc-urls"],
1611
+ ["--wdk-evm-service-url", "--wdk-evm-service-url"],
1612
+ ]) {
1613
+ const value = parseFlagValue(args, flag);
1614
+ if (value) commandArgs.push(targetFlag, value);
1615
+ }
1616
+ return { pythonBin, commandArgs, configPath: commandArgs[2] };
1617
+ }
1618
+
1619
+ function runOpenclawHostInstall(args, env = process.env) {
1620
+ const { pythonBin, commandArgs, configPath } = openclawHostCommandArgs(args, env);
1621
+ const previousConfig = fs.existsSync(configPath) ? fs.readFileSync(configPath) : null;
1622
+ if (previousConfig === null) {
1623
+ writeJsonFileAtomic(configPath, {
1624
+ plugins: { entries: {} },
1625
+ tools: { alsoAllow: [] },
1626
+ });
1627
+ }
1628
+ const result = spawnSync(pythonBin, commandArgs, {
1629
+ cwd: packageRoot,
1630
+ encoding: "utf8",
1631
+ env,
1632
+ });
1633
+ if (result.error || (result.status ?? 1) !== 0) {
1634
+ if (previousConfig === null) {
1635
+ fs.rmSync(configPath, { force: true });
1636
+ } else {
1637
+ fs.writeFileSync(configPath, previousConfig, { mode: 0o600 });
1638
+ }
1639
+ return {
1640
+ name: "openclaw",
1641
+ attempted: true,
1642
+ ok: false,
1643
+ error: result.error?.message || (result.stderr || result.stdout || "").trim(),
1644
+ rolled_back: true,
1645
+ restart_required: false,
1646
+ };
1647
+ }
1648
+ let details = {};
1649
+ try {
1650
+ details = extractTrailingJson(result.stdout);
1651
+ } catch {
1652
+ details = { config_path: configPath };
1653
+ }
1654
+ recordManagedIntegration(
1655
+ "openclaw",
1656
+ {
1657
+ config_path: configPath,
1658
+ extension_path: path.join(currentRuntimePath(env), ".openclaw", "extensions", "agent-wallet"),
1659
+ package_root: path.join(currentRuntimePath(env), "agent-wallet"),
1660
+ restart_required: true,
1661
+ },
1662
+ env,
1663
+ );
1664
+ return {
1665
+ name: "openclaw",
1666
+ attempted: true,
1667
+ ok: true,
1668
+ ...details,
1669
+ restart_required: true,
1670
+ };
1671
+ }
1672
+
1673
+ function runEditorHostInstall(name, args, env = process.env) {
1674
+ const binaryName = name === "claude-code" ? "claude" : name;
1675
+ const hostArgs = [
1676
+ name,
1677
+ "install",
1678
+ ...(hasFlag(args, "--force") ? ["--force"] : []),
1679
+ ...(!commandPath(binaryName) || hasFlag(args, "--skip-enable") ? ["--skip-enable"] : []),
1680
+ ];
1681
+ const result = spawnSync(process.execPath, [cliPath, ...hostArgs], {
1682
+ cwd: packageRoot,
1683
+ encoding: "utf8",
1684
+ env,
1685
+ });
1686
+ let details = {};
1687
+ try {
1688
+ details = extractTrailingJson(result.stdout);
1689
+ } catch {
1690
+ // Preserve the host adapter's error below when it did not emit JSON.
1691
+ }
1692
+ return {
1693
+ name,
1694
+ attempted: true,
1695
+ ok: !result.error && (result.status ?? 1) === 0,
1696
+ ...details,
1697
+ error:
1698
+ result.error?.message ||
1699
+ ((result.status ?? 1) === 0 ? "" : (result.stderr || result.stdout || "").trim()),
1700
+ };
1701
+ }
1702
+
1703
+ function applyHostInstallPlan(plan, args, env = process.env) {
1704
+ const selected = new Set(plan.selected_hosts);
1705
+ const managed = new Set(plan.managed_hosts);
1706
+ const managedSelected = plan.selected_hosts.filter((name) => managed.has(name));
1707
+ const adoptLegacy =
1708
+ plan.runtime_installed_before &&
1709
+ ["managed_only", "existing_runtime_managed_only"].includes(plan.selection_reason);
1710
+ const refreshed = repairInstalledEditorIntegrations(
1711
+ env,
1712
+ adoptLegacy ? null : managedSelected,
1713
+ );
1714
+ const installed = [];
1715
+
1716
+ for (const name of plan.selected_hosts) {
1717
+ if (managed.has(name)) continue;
1718
+ installed.push(
1719
+ name === "openclaw"
1720
+ ? runOpenclawHostInstall(args, env)
1721
+ : runEditorHostInstall(name, args, env),
1722
+ );
1723
+ }
1724
+
1725
+ return {
1726
+ ok: [...refreshed, ...installed].every((entry) => entry?.ok !== false),
1727
+ selected_hosts: [...selected],
1728
+ refreshed,
1729
+ installed,
1730
+ };
1731
+ }
1732
+
1733
+ function runInstallUnlocked(args, { commandName = "install", installPlan = null } = {}) {
1399
1734
  if (!fs.existsSync(setupPath)) {
1400
1735
  console.error(`Missing bundled setup.sh at ${setupPath}`);
1401
1736
  return 1;
@@ -1407,8 +1742,10 @@ function runInstallUnlocked(args, { commandName = "install" } = {}) {
1407
1742
  : releaseRootFor(packageVersion);
1408
1743
  const currentPath = currentRuntimePath();
1409
1744
  const previousPath = previousRuntimePath();
1745
+ const hostPlan = installPlan || universalInstallPlan(args);
1410
1746
  const installerArgs = withoutCliOnlyArgs(args);
1411
1747
  const dryRun = hasFlag(args, "--dry-run");
1748
+ if (!dryRun) printMacOSKeychainNotice(process.env);
1412
1749
  const recovery = dryRun
1413
1750
  ? { attempted: false, ok: true, reason: "dry run" }
1414
1751
  : recoverInterruptedUpdate(process.env);
@@ -1425,6 +1762,9 @@ function runInstallUnlocked(args, { commandName = "install" } = {}) {
1425
1762
  if (!hasFlag(installerArgs, "--install-from-runtime")) {
1426
1763
  installerArgs.push("--install-from-runtime");
1427
1764
  }
1765
+ if (!hasFlag(installerArgs, "--configure-openclaw") && !hasFlag(installerArgs, "--no-configure-openclaw")) {
1766
+ installerArgs.push("--no-configure-openclaw");
1767
+ }
1428
1768
 
1429
1769
  let installerEnv;
1430
1770
  try {
@@ -1450,7 +1790,8 @@ function runInstallUnlocked(args, { commandName = "install" } = {}) {
1450
1790
  }
1451
1791
  const result = spawnSync("sh", [setupPath, ...installerArgs], {
1452
1792
  cwd: packageRoot,
1453
- stdio: "inherit",
1793
+ stdio: dryRun ? "pipe" : "inherit",
1794
+ encoding: dryRun ? "utf8" : undefined,
1454
1795
  env,
1455
1796
  });
1456
1797
 
@@ -1463,6 +1804,10 @@ function runInstallUnlocked(args, { commandName = "install" } = {}) {
1463
1804
  return 1;
1464
1805
  }
1465
1806
  if ((result.status ?? 1) !== 0) {
1807
+ if (dryRun) {
1808
+ if (result.stderr) process.stderr.write(result.stderr);
1809
+ if (result.stdout) process.stderr.write(result.stdout);
1810
+ }
1466
1811
  const failedRoot = failStagingRuntime(stagingRoot, `installer exited with ${result.status ?? 1}`);
1467
1812
  if (!dryRun) {
1468
1813
  writeUpdateJournal(
@@ -1475,6 +1820,27 @@ function runInstallUnlocked(args, { commandName = "install" } = {}) {
1475
1820
  }
1476
1821
 
1477
1822
  if (dryRun) {
1823
+ if (result.stderr) process.stderr.write(result.stderr);
1824
+ let installerPlan;
1825
+ try {
1826
+ installerPlan = extractTrailingJson(result.stdout);
1827
+ } catch (error) {
1828
+ if (result.stdout) process.stderr.write(result.stdout);
1829
+ console.error(error.message);
1830
+ return 1;
1831
+ }
1832
+ console.log(
1833
+ JSON.stringify(
1834
+ {
1835
+ ...installerPlan,
1836
+ command: commandName,
1837
+ dry_run: true,
1838
+ host_plan: hostPlan,
1839
+ },
1840
+ null,
1841
+ 2,
1842
+ ),
1843
+ );
1478
1844
  return 0;
1479
1845
  }
1480
1846
 
@@ -1632,19 +1998,9 @@ function runInstallUnlocked(args, { commandName = "install" } = {}) {
1632
1998
  env,
1633
1999
  );
1634
2000
 
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
- );
1646
-
1647
- const integrationRefresh = repairInstalledEditorIntegrations(env);
2001
+ const integrationRegistryRecovery = integrations(env).recoverCorruptRegistry();
2002
+ const hostInstallation = applyHostInstallPlan(hostPlan, args, env);
2003
+ const hostInstallFailed = hostInstallation.installed.some((entry) => entry?.ok === false);
1648
2004
  const globalCliRefresh = safelyRefreshIntegration(
1649
2005
  "global-cli",
1650
2006
  () => refreshGlobalCliIfNeeded(env),
@@ -1661,7 +2017,7 @@ function runInstallUnlocked(args, { commandName = "install" } = {}) {
1661
2017
  console.error(
1662
2018
  JSON.stringify(
1663
2019
  {
1664
- ok: true,
2020
+ ok: !hostInstallFailed,
1665
2021
  command: commandName,
1666
2022
  version: packageVersion,
1667
2023
  runtime_root: releaseRoot,
@@ -1672,14 +2028,23 @@ function runInstallUnlocked(args, { commandName = "install" } = {}) {
1672
2028
  staged: Boolean(stagingRoot),
1673
2029
  release_state: "verified",
1674
2030
  recovery,
1675
- integration_refresh: integrationRefresh,
2031
+ host_plan: hostPlan,
2032
+ host_installation: hostInstallation,
2033
+ integration_registry_recovery: integrationRegistryRecovery,
2034
+ integration_refresh: hostInstallation.refreshed,
1676
2035
  global_cli_refresh: globalCliRefresh,
2036
+ ...(hostInstallFailed
2037
+ ? {
2038
+ category: "host_install_failed",
2039
+ message: "The shared runtime is active, but one or more selected host plugins failed to install.",
2040
+ }
2041
+ : {}),
1677
2042
  },
1678
2043
  null,
1679
2044
  2,
1680
2045
  ),
1681
2046
  );
1682
- return 0;
2047
+ return hostInstallFailed ? 1 : 0;
1683
2048
  }
1684
2049
 
1685
2050
  function runInstall(args, options = {}) {
@@ -1717,10 +2082,11 @@ function resolveUpdatePackageSpec(env = process.env) {
1717
2082
  }
1718
2083
 
1719
2084
  function runDelegatedInstallForUpdate(args, { captureOutput = false } = {}) {
2085
+ const installArgs = hasFlag(args, "--managed-only") ? args : [...args, "--managed-only"];
1720
2086
  const localCliPath = String(process.env[UPDATE_CLI_PATH_ENV] || "").trim();
1721
2087
  if (localCliPath) {
1722
2088
  const meta = resolveCliPackageMeta(localCliPath);
1723
- const result = spawnSync("node", [localCliPath, "install", ...args], {
2089
+ const result = spawnSync("node", [localCliPath, "install", ...installArgs], {
1724
2090
  cwd: packageRoot,
1725
2091
  stdio: captureOutput ? "pipe" : "inherit",
1726
2092
  encoding: captureOutput ? "utf8" : undefined,
@@ -1743,7 +2109,7 @@ function runDelegatedInstallForUpdate(args, { captureOutput = false } = {}) {
1743
2109
  const binCommand = primaryBinCommand();
1744
2110
  const result = spawnSync(
1745
2111
  npmBin,
1746
- ["exec", "--yes", `--package=${packageSpec}`, binCommand, "--", "install", ...args],
2112
+ ["exec", "--yes", `--package=${packageSpec}`, binCommand, "--", "install", ...installArgs],
1747
2113
  {
1748
2114
  cwd: packageRoot,
1749
2115
  stdio: captureOutput ? "pipe" : "inherit",
@@ -2576,8 +2942,8 @@ function safelyRefreshIntegration(name, callback) {
2576
2942
  return integrations().safelyRefresh(name, callback);
2577
2943
  }
2578
2944
 
2579
- function repairInstalledEditorIntegrations(env = process.env) {
2580
- return hostIntegrations(env).refreshAll();
2945
+ function repairInstalledEditorIntegrations(env = process.env, names = null) {
2946
+ return hostIntegrations(env).refreshAll(names);
2581
2947
  }
2582
2948
 
2583
2949
  const args = process.argv.slice(2);
@@ -2614,11 +2980,15 @@ if (command === "status") {
2614
2980
  process.exit(runStatus(args.slice(1)));
2615
2981
  }
2616
2982
 
2983
+ if (command === "detect") {
2984
+ process.exit(runDetect(args.slice(1)));
2985
+ }
2986
+
2617
2987
  if (command === "install" || command === "setup") {
2618
2988
  const commandArgs = args.slice(1);
2619
2989
  process.exit(
2620
- runWithCliTelemetry(
2621
- () => runInstall(commandArgs, { commandName: "install" }),
2990
+ await runWithCliTelemetryAsync(
2991
+ () => runUniversalInstall(commandArgs, { commandName: "install" }),
2622
2992
  {
2623
2993
  startEvent: "install_start",
2624
2994
  successEvent: "install_success",
@@ -2721,8 +3091,8 @@ if (command === "claude-code") {
2721
3091
 
2722
3092
  if (command.startsWith("-")) {
2723
3093
  process.exit(
2724
- runWithCliTelemetry(
2725
- () => runInstall(args, { commandName: "install" }),
3094
+ await runWithCliTelemetryAsync(
3095
+ () => runUniversalInstall(args, { commandName: "install" }),
2726
3096
  {
2727
3097
  startEvent: "install_start",
2728
3098
  successEvent: "install_success",