@agentlayer.tech/wallet 0.1.37 → 0.1.44

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 (34) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +1 -1
  3. package/.openclaw/extensions/agent-wallet/package.json +1 -1
  4. package/CHANGELOG.md +37 -0
  5. package/README.md +14 -0
  6. package/VERSION +1 -1
  7. package/agent-wallet/agent_wallet/__init__.py +1 -1
  8. package/agent-wallet/agent_wallet/evm_user_wallets.py +114 -6
  9. package/agent-wallet/agent_wallet/openclaw_adapter.py +331 -0
  10. package/agent-wallet/agent_wallet/openclaw_cli.py +22 -0
  11. package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +2 -0
  12. package/agent-wallet/agent_wallet/telemetry.py +306 -0
  13. package/agent-wallet/agent_wallet/wallet_layer/base.py +32 -0
  14. package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +156 -0
  15. package/agent-wallet/openclaw.plugin.json +1 -1
  16. package/agent-wallet/pyproject.toml +1 -1
  17. package/agent-wallet/scripts/bootstrap_openclaw_evm.py +20 -2
  18. package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
  19. package/claude-code/plugins/agent-wallet/README.md +30 -1
  20. package/claude-code/plugins/agent-wallet/commands/wallet-setup.md +24 -0
  21. package/claude-code/plugins/agent-wallet/hooks/hooks.json +16 -0
  22. package/claude-code/plugins/agent-wallet/scripts/bootstrap_backend.sh +136 -0
  23. package/claude-code/plugins/agent-wallet/scripts/run_mcp.sh +6 -1
  24. package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
  25. package/codex/plugins/agent-wallet/scripts/run_mcp.sh +5 -0
  26. package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
  27. package/hermes/plugins/agent_wallet/tools.py +3 -0
  28. package/package.json +2 -1
  29. package/wdk-btc-wallet/package.json +1 -1
  30. package/wdk-evm-wallet/README.md +25 -0
  31. package/wdk-evm-wallet/package.json +4 -2
  32. package/wdk-evm-wallet/src/config.js +47 -0
  33. package/wdk-evm-wallet/src/server.js +25 -3
  34. package/wdk-evm-wallet/src/wdk_evm_wallet.js +545 -0
@@ -26,6 +26,8 @@ LONG_RUNNING_POST_PATHS = {
26
26
  "/v1/evm/lido/claim_withdrawal/send",
27
27
  "/v1/evm/swap/quote",
28
28
  "/v1/evm/swap/send",
29
+ "/v1/evm/uniswap/swap/quote",
30
+ "/v1/evm/uniswap/swap/send",
29
31
  "/v1/evm/lifi/quote",
30
32
  "/v1/evm/lifi/send",
31
33
  "/v1/evm/transfer/send",
@@ -0,0 +1,306 @@
1
+ """Anonymous, privacy-first adoption telemetry for the AgentLayer wallet.
2
+
3
+ Why this exists: we want to know how many people use the wallet and through which
4
+ host (Claude Code / Codex / Hermes / OpenClaw) — nothing more. It deliberately
5
+ records NO PII: no wallet addresses, balances, amounts, tx hashes, tool
6
+ arguments, or secrets. Only a random local install id, the host name, the
7
+ invoked tool's registered name, the backend family, the plugin version, and a
8
+ success flag.
9
+
10
+ Design for a short-lived CLI subprocess:
11
+ - `record()` appends one JSON line to a local spool file. This is instant and
12
+ durable: a single small append is atomic on POSIX, and the event survives
13
+ even if this process exits before any network call completes.
14
+ - A throttled, best-effort flush claims the spool (atomic rename), POSTs each
15
+ event to the provider-gateway, and re-spools anything that failed. Because
16
+ durability lives in the spool, a killed flush never loses data — the next
17
+ invocation (or a longer-lived process) ships it.
18
+
19
+ Everything here swallows its own errors. Telemetry must never slow down or break
20
+ a wallet operation. Users opt out with AGENT_WALLET_NO_TELEMETRY=1.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import os
27
+ import subprocess
28
+ import sys
29
+ import time
30
+ import urllib.request
31
+ import uuid
32
+ from pathlib import Path
33
+ from typing import Any
34
+
35
+ from . import __version__
36
+ from .config import DEFAULT_PROVIDER_GATEWAY_URL, resolve_openclaw_home
37
+
38
+ ALLOWED_HOSTS = {"claude-code", "codex", "hermes", "openclaw"}
39
+
40
+ SPOOL_NAME = "telemetry_spool.jsonl"
41
+ ID_NAME = "telemetry_id"
42
+ LAST_FLUSH_NAME = "telemetry_last_flush"
43
+
44
+ # Keep the spool bounded if the gateway is unreachable for a long time.
45
+ MAX_SPOOL_LINES = 500
46
+ # Don't attempt a network flush more than this often (seconds), unless the spool
47
+ # has grown past the soft cap below.
48
+ FLUSH_THROTTLE_SECONDS = 20
49
+ FLUSH_FORCE_LINES = 50
50
+ # Tight network bounds: telemetry never blocks meaningfully.
51
+ HTTP_TIMEOUT_SECONDS = 1.5
52
+ MAX_EVENTS_PER_FLUSH = 100
53
+
54
+
55
+ def _enabled() -> bool:
56
+ raw = os.getenv("AGENT_WALLET_NO_TELEMETRY", "").strip().lower()
57
+ return raw not in ("1", "true", "yes", "on")
58
+
59
+
60
+ def _home() -> Path:
61
+ return resolve_openclaw_home()
62
+
63
+
64
+ def _gateway_url() -> str:
65
+ url = os.getenv("PROVIDER_GATEWAY_URL", "").strip() or DEFAULT_PROVIDER_GATEWAY_URL
66
+ return url.rstrip("/")
67
+
68
+
69
+ def _host() -> str:
70
+ host = os.getenv("AGENT_WALLET_HOST", "").strip().lower()
71
+ return host if host in ALLOWED_HOSTS else "unknown"
72
+
73
+
74
+ def _install_id() -> str:
75
+ """Stable, random, non-identifying id for this install. Created once."""
76
+ path = _home() / ID_NAME
77
+ try:
78
+ existing = path.read_text(encoding="utf-8").strip()
79
+ if existing:
80
+ return existing
81
+ except OSError:
82
+ pass
83
+ new_id = uuid.uuid4().hex
84
+ try:
85
+ path.parent.mkdir(parents=True, exist_ok=True)
86
+ path.write_text(new_id, encoding="utf-8")
87
+ except OSError:
88
+ pass
89
+ return new_id
90
+
91
+
92
+ def record(
93
+ tool: str,
94
+ *,
95
+ backend: str = "",
96
+ ok: bool = True,
97
+ event: str = "tool_invoke",
98
+ ) -> None:
99
+ """Append an anonymous event to the spool and best-effort flush. Never raises."""
100
+ try:
101
+ if not _enabled():
102
+ return
103
+ payload = {
104
+ "event": event,
105
+ "install_id": _install_id(),
106
+ "host": _host(),
107
+ "tool": tool or "",
108
+ "backend": backend or "",
109
+ "plugin_version": __version__,
110
+ "ok": bool(ok),
111
+ "ts": int(time.time()),
112
+ }
113
+ _append_spool(payload)
114
+ _maybe_spawn_flush()
115
+ except Exception:
116
+ # Telemetry is never allowed to affect the wallet call.
117
+ pass
118
+
119
+
120
+ # --- spool I/O --------------------------------------------------------------
121
+
122
+
123
+ def _spool_path() -> Path:
124
+ return _home() / SPOOL_NAME
125
+
126
+
127
+ def _append_spool(payload: dict[str, Any]) -> None:
128
+ path = _spool_path()
129
+ line = json.dumps(payload, separators=(",", ":")) + "\n"
130
+ path.parent.mkdir(parents=True, exist_ok=True)
131
+ # A single write of a short line is atomic across processes on POSIX.
132
+ with open(path, "a", encoding="utf-8") as fh:
133
+ fh.write(line)
134
+
135
+
136
+ def _spool_line_count() -> int:
137
+ try:
138
+ with open(_spool_path(), "r", encoding="utf-8") as fh:
139
+ return sum(1 for _ in fh)
140
+ except OSError:
141
+ return 0
142
+
143
+
144
+ def _should_flush_now() -> bool:
145
+ count = _spool_line_count()
146
+ if count == 0:
147
+ return False
148
+ if count >= FLUSH_FORCE_LINES:
149
+ return True
150
+ last_path = _home() / LAST_FLUSH_NAME
151
+ try:
152
+ last = float(last_path.read_text(encoding="utf-8").strip() or "0")
153
+ except (OSError, ValueError):
154
+ last = 0.0
155
+ return (time.time() - last) >= FLUSH_THROTTLE_SECONDS
156
+
157
+
158
+ def _mark_flush_attempt() -> None:
159
+ try:
160
+ (_home() / LAST_FLUSH_NAME).write_text(str(time.time()), encoding="utf-8")
161
+ except OSError:
162
+ pass
163
+
164
+
165
+ def _maybe_spawn_flush() -> None:
166
+ """Trigger a flush in a DETACHED subprocess that outlives this process.
167
+
168
+ A per-tool CLI invocation exits microseconds after recording, so a thread
169
+ would be killed mid-flush. A detached child (its own session, no wait) runs
170
+ the network call on its own time and re-spools failures reliably. Throttling
171
+ keeps this to roughly one spawn per FLUSH_THROTTLE_SECONDS, so most calls
172
+ just append to the spool and return.
173
+ """
174
+ if not _should_flush_now():
175
+ return
176
+ _mark_flush_attempt()
177
+ try:
178
+ subprocess.Popen(
179
+ [sys.executable, "-m", "agent_wallet.telemetry", "--flush"],
180
+ stdout=subprocess.DEVNULL,
181
+ stderr=subprocess.DEVNULL,
182
+ stdin=subprocess.DEVNULL,
183
+ start_new_session=True, # detach so parent exit doesn't kill it
184
+ )
185
+ except Exception:
186
+ pass
187
+
188
+
189
+ def _pid_alive(pid: int) -> bool:
190
+ try:
191
+ os.kill(pid, 0)
192
+ except ProcessLookupError:
193
+ return False
194
+ except PermissionError:
195
+ return True # exists but owned by another user
196
+ except OSError:
197
+ return True
198
+ return True
199
+
200
+
201
+ def _claim_suffix_pid(path: Path) -> int | None:
202
+ # ...telemetry_spool.jsonl.flushing.<pid>
203
+ try:
204
+ return int(path.name.rsplit(".flushing.", 1)[1])
205
+ except (IndexError, ValueError):
206
+ return None
207
+
208
+
209
+ def _flush() -> None:
210
+ """Claim the spool atomically, adopt dead-PID orphans, POST, re-spool failures."""
211
+ spool = _spool_path()
212
+ home = _home()
213
+ claim = spool.with_suffix(spool.suffix + f".flushing.{os.getpid()}")
214
+
215
+ claims: list[Path] = []
216
+ try:
217
+ os.rename(spool, claim) # atomic; only one process wins the live spool
218
+ claims.append(claim)
219
+ except OSError:
220
+ pass # nothing to flush, or another process claimed it
221
+
222
+ # Recover orphans left by a flush that died mid-run (crash/exit after the
223
+ # rename but before re-spooling). Only adopt claims whose PID is gone so we
224
+ # never steal a batch a live sibling is still working.
225
+ try:
226
+ for orphan in home.glob(SPOOL_NAME + ".flushing.*"):
227
+ if orphan == claim:
228
+ continue
229
+ pid = _claim_suffix_pid(orphan)
230
+ if pid is None or not _pid_alive(pid):
231
+ claims.append(orphan)
232
+ except OSError:
233
+ pass
234
+
235
+ if not claims:
236
+ return
237
+
238
+ lines: list[str] = []
239
+ for c in claims:
240
+ try:
241
+ lines.extend(c.read_text(encoding="utf-8").splitlines())
242
+ except OSError:
243
+ pass
244
+
245
+ # Bound a long backlog (gateway down for a while): keep the most recent.
246
+ if len(lines) > MAX_SPOOL_LINES:
247
+ lines = lines[-MAX_SPOOL_LINES:]
248
+
249
+ url = _gateway_url() + "/v1/telemetry"
250
+ failed: list[str] = []
251
+ sent = 0
252
+ for line in lines:
253
+ line = line.strip()
254
+ if not line:
255
+ continue
256
+ if sent >= MAX_EVENTS_PER_FLUSH:
257
+ failed.append(line) # defer the rest to the next flush
258
+ continue
259
+ if _post(url, line):
260
+ sent += 1
261
+ else:
262
+ failed.append(line)
263
+
264
+ # Re-spool failures BEFORE removing claims: favors at-least-once delivery
265
+ # (a crash here re-sends, never drops). Successes are gone from the claims.
266
+ if failed:
267
+ try:
268
+ with open(spool, "a", encoding="utf-8") as fh:
269
+ fh.write("\n".join(failed) + "\n")
270
+ except OSError:
271
+ pass
272
+
273
+ for c in claims:
274
+ try:
275
+ c.unlink()
276
+ except OSError:
277
+ pass
278
+
279
+
280
+ def _post(url: str, body: str) -> bool:
281
+ try:
282
+ req = urllib.request.Request(
283
+ url,
284
+ data=body.encode("utf-8"),
285
+ headers={"Content-Type": "application/json"},
286
+ method="POST",
287
+ )
288
+ with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT_SECONDS) as resp:
289
+ return 200 <= resp.status < 300
290
+ except Exception:
291
+ return False
292
+
293
+
294
+ def _flush_main() -> int:
295
+ """Entry point for the detached flush subprocess (`-m agent_wallet.telemetry --flush`)."""
296
+ try:
297
+ _flush()
298
+ except Exception:
299
+ pass
300
+ return 0
301
+
302
+
303
+ if __name__ == "__main__":
304
+ if "--flush" in sys.argv[1:]:
305
+ raise SystemExit(_flush_main())
306
+ raise SystemExit(0)
@@ -226,6 +226,38 @@ class AgentWalletBackend(ABC):
226
226
  ) -> dict[str, Any]:
227
227
  raise WalletBackendError(f"{self.name} does not support EVM LI.FI cross-chain swaps.")
228
228
 
229
+ async def get_uniswap_swap_quote(
230
+ self,
231
+ *,
232
+ token_in: str,
233
+ token_out: str,
234
+ amount_in_raw: str,
235
+ slippage_bps: int | None = None,
236
+ ) -> dict[str, Any]:
237
+ raise WalletBackendError(f"{self.name} does not support Uniswap swap quotes.")
238
+
239
+ async def preview_uniswap_swap(
240
+ self,
241
+ *,
242
+ token_in: str,
243
+ token_out: str,
244
+ amount_in_raw: str,
245
+ slippage_bps: int | None = None,
246
+ ) -> dict[str, Any]:
247
+ raise WalletBackendError(f"{self.name} does not support Uniswap swap previews.")
248
+
249
+ async def send_uniswap_swap(
250
+ self,
251
+ *,
252
+ token_in: str,
253
+ token_out: str,
254
+ amount_in_raw: str,
255
+ slippage_bps: int | None = None,
256
+ expected_quote_fingerprint: str | None = None,
257
+ minimum_output_amount_raw: str | None = None,
258
+ ) -> dict[str, Any]:
259
+ raise WalletBackendError(f"{self.name} does not support Uniswap swaps.")
260
+
229
261
  async def preview_evm_native_transfer(
230
262
  self,
231
263
  *,
@@ -1382,6 +1382,162 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
1382
1382
  "source": "wdk-evm-wallet",
1383
1383
  }
1384
1384
 
1385
+ def _normalize_uniswap_quote_payload(self, data: dict[str, Any], token_in: str, token_out: str, amount_in_raw: str) -> dict[str, Any]:
1386
+ return {
1387
+ "chain": self.chain,
1388
+ "network": self.network,
1389
+ "address": str(data.get("address") or ""),
1390
+ "token_in": str(data.get("tokenIn") or token_in),
1391
+ "token_out": str(data.get("tokenOut") or token_out),
1392
+ "amount_in_raw": str(data.get("tokenInAmount") or amount_in_raw),
1393
+ "amount_in_ui": str(data.get("inputAmountFormatted")) if data.get("inputAmountFormatted") is not None else None,
1394
+ "estimated_output_amount_raw": str(data.get("outputAmount") or "0"),
1395
+ "estimated_output_amount_ui": str(data.get("outputAmountFormatted")) if data.get("outputAmountFormatted") is not None else None,
1396
+ "minimum_output_amount_raw": str(data.get("minimumOutputAmountRaw")) if data.get("minimumOutputAmountRaw") is not None else None,
1397
+ "slippage_bps": int(data.get("slippageBps")) if data.get("slippageBps") is not None else None,
1398
+ "protocol": str(data.get("protocol") or "uniswap"),
1399
+ "routing": str(data.get("routing") or "CLASSIC"),
1400
+ "permit_required": bool(data.get("permitRequired")),
1401
+ "allowance": _normalize_swap_allowance(data.get("allowance")),
1402
+ "router": str(data.get("router") or "").strip() or None,
1403
+ "quote_fingerprint": str(data.get("quoteFingerprint") or "").strip() or None,
1404
+ "gas_fee_usd": str(data.get("gasFeeUSD")) if data.get("gasFeeUSD") is not None else None,
1405
+ "estimated_fee_wei": str(data.get("estimatedFeeWei")) if data.get("estimatedFeeWei") is not None else None,
1406
+ "execution_supported": bool(data.get("executionSupported")) and not self.sign_only,
1407
+ "token_in_metadata": _normalize_token_metadata(data.get("tokenInMetadata"), token_in),
1408
+ "token_out_metadata": _normalize_token_metadata(data.get("tokenOutMetadata"), token_out),
1409
+ "chain_id": int(data.get("chainId") or 0),
1410
+ "source": "wdk-evm-wallet",
1411
+ }
1412
+
1413
+ async def get_uniswap_swap_quote(
1414
+ self,
1415
+ *,
1416
+ token_in: str,
1417
+ token_out: str,
1418
+ amount_in_raw: str,
1419
+ slippage_bps: int | None = None,
1420
+ ) -> dict[str, Any]:
1421
+ resolved_address = await self.get_address()
1422
+ body: dict[str, Any] = {
1423
+ "walletId": self.wallet_id,
1424
+ "address": resolved_address,
1425
+ "accountIndex": self.account_index,
1426
+ "network": self.network,
1427
+ "tokenIn": token_in,
1428
+ "tokenOut": token_out,
1429
+ "tokenInAmount": amount_in_raw,
1430
+ }
1431
+ if slippage_bps is not None:
1432
+ body["slippageBps"] = slippage_bps
1433
+ data = await self.client.post("/v1/evm/uniswap/swap/quote", body)
1434
+ return self._normalize_uniswap_quote_payload(data, token_in, token_out, amount_in_raw)
1435
+
1436
+ async def preview_uniswap_swap(
1437
+ self,
1438
+ *,
1439
+ token_in: str,
1440
+ token_out: str,
1441
+ amount_in_raw: str,
1442
+ slippage_bps: int | None = None,
1443
+ ) -> dict[str, Any]:
1444
+ resolved_address = await self.get_address()
1445
+ body: dict[str, Any] = {
1446
+ "walletId": self.wallet_id,
1447
+ "address": resolved_address,
1448
+ "accountIndex": self.account_index,
1449
+ "network": self.network,
1450
+ "tokenIn": token_in,
1451
+ "tokenOut": token_out,
1452
+ "tokenInAmount": amount_in_raw,
1453
+ }
1454
+ if slippage_bps is not None:
1455
+ body["slippageBps"] = slippage_bps
1456
+ data = await self.client.post("/v1/evm/uniswap/swap/quote", body)
1457
+ payload = self._normalize_uniswap_quote_payload(data, token_in, token_out, amount_in_raw)
1458
+ return {
1459
+ **payload,
1460
+ "asset_type": "evm-uniswap-swap",
1461
+ "asset": "ERC20",
1462
+ "wallet": self.wallet_id,
1463
+ "from_address": resolved_address,
1464
+ "input_amount_raw": payload["amount_in_raw"],
1465
+ "input_amount_ui": payload["amount_in_ui"],
1466
+ "swap_provider": payload["protocol"],
1467
+ "route_plan": None,
1468
+ "swap_transaction": {
1469
+ "to": payload["router"],
1470
+ "value": "0",
1471
+ "data_hash": None,
1472
+ },
1473
+ }
1474
+
1475
+ async def send_uniswap_swap(
1476
+ self,
1477
+ *,
1478
+ token_in: str,
1479
+ token_out: str,
1480
+ amount_in_raw: str,
1481
+ slippage_bps: int | None = None,
1482
+ expected_quote_fingerprint: str | None = None,
1483
+ minimum_output_amount_raw: str | None = None,
1484
+ ) -> dict[str, Any]:
1485
+ if self.sign_only:
1486
+ raise WalletBackendError("wdk_evm_local is configured as sign_only.")
1487
+ body: dict[str, Any] = {
1488
+ "walletId": self.wallet_id,
1489
+ "accountIndex": self.account_index,
1490
+ "network": self.network,
1491
+ "tokenIn": token_in,
1492
+ "tokenOut": token_out,
1493
+ "tokenInAmount": amount_in_raw,
1494
+ }
1495
+ if slippage_bps is not None:
1496
+ body["slippageBps"] = slippage_bps
1497
+ if isinstance(expected_quote_fingerprint, str) and expected_quote_fingerprint.strip():
1498
+ body["expectedQuoteFingerprint"] = expected_quote_fingerprint.strip()
1499
+ if isinstance(minimum_output_amount_raw, str) and minimum_output_amount_raw.strip():
1500
+ body["minimumTokenOutAmount"] = minimum_output_amount_raw.strip()
1501
+ data = await self.client.post("/v1/evm/uniswap/swap/send", body)
1502
+ result = dict(data.get("result") or {})
1503
+ return {
1504
+ "chain": self.chain,
1505
+ "network": self.network,
1506
+ "asset_type": "evm-uniswap-swap",
1507
+ "asset": "ERC20",
1508
+ "wallet": self.wallet_id,
1509
+ "from_address": await self.get_address(),
1510
+ "token_in": str(data.get("tokenIn") or token_in),
1511
+ "token_out": str(data.get("tokenOut") or token_out),
1512
+ "input_amount_raw": str(data.get("tokenInAmount") or amount_in_raw),
1513
+ "input_amount_ui": str(data.get("inputAmountFormatted")) if data.get("inputAmountFormatted") is not None else None,
1514
+ "estimated_output_amount_raw": str(data.get("outputAmount") or "0"),
1515
+ "estimated_output_amount_ui": str(data.get("outputAmountFormatted")) if data.get("outputAmountFormatted") is not None else None,
1516
+ "slippage_bps": int(data.get("slippageBps")) if data.get("slippageBps") is not None else None,
1517
+ "minimum_output_amount_raw": str(data.get("minimumOutputAmountRaw")) if data.get("minimumOutputAmountRaw") is not None else None,
1518
+ "swap_provider": str(data.get("protocol") or "uniswap"),
1519
+ "routing": str(data.get("routing") or "CLASSIC"),
1520
+ "quote_fingerprint": str(data.get("quoteFingerprint") or "").strip() or None,
1521
+ "router": str(data.get("router") or "").strip() or None,
1522
+ "allowance": _normalize_swap_allowance(data.get("allowance")),
1523
+ "simulation": _normalize_swap_simulation(data.get("simulation")),
1524
+ "swap_transaction": {
1525
+ "to": str((data.get("swapTransaction") or {}).get("to") or "").strip() or None,
1526
+ "value": str((data.get("swapTransaction") or {}).get("value") or "0"),
1527
+ "data_hash": str((data.get("swapTransaction") or {}).get("dataHash") or "").strip() or None,
1528
+ },
1529
+ "token_in_metadata": _normalize_token_metadata(data.get("tokenInMetadata"), token_in),
1530
+ "token_out_metadata": _normalize_token_metadata(data.get("tokenOutMetadata"), token_out),
1531
+ "hash": result.get("hash"),
1532
+ "approve_hash": result.get("approveHash"),
1533
+ "reset_allowance_hash": result.get("resetAllowanceHash"),
1534
+ "result": result,
1535
+ "chain_id": int(data.get("chainId") or 0),
1536
+ "broadcasted": True,
1537
+ "confirmed": False,
1538
+ "source": "wdk-evm-wallet",
1539
+ }
1540
+
1385
1541
  async def preview_evm_lifi_cross_chain_swap(
1386
1542
  self,
1387
1543
  *,
@@ -2,7 +2,7 @@
2
2
  "id": "agent-wallet",
3
3
  "name": "Agent Wallet",
4
4
  "description": "Plugin-friendly wallet backend for OpenClaw agents with safe wallet tools and runtime instructions across Solana, local BTC, and local EVM.",
5
- "version": "0.1.37",
5
+ "version": "0.1.44",
6
6
  "skills": ["skills/wallet-operator"],
7
7
  "configSchema": {
8
8
  "type": "object",
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openclaw-agent-wallet"
7
- version = "0.1.37"
7
+ version = "0.1.44"
8
8
  description = "Plugin-friendly wallet backend for OpenClaw agents"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -137,8 +137,25 @@ def _auto_start_local_service(
137
137
  wdk_wallet_root: Path,
138
138
  config_path: Path,
139
139
  ) -> dict[str, object]:
140
- if _service_is_healthy(service_url):
141
- return {"started": False, "already_healthy": True}
140
+ # Restart a running-but-stale daemon (old code in memory after a release) by
141
+ # comparing its /health version to the on-disk launcher version. Idempotent:
142
+ # steady state is a no-op. See agent_wallet.evm_user_wallets for the shared
143
+ # health/version/stop helpers.
144
+ from agent_wallet.evm_user_wallets import (
145
+ _read_on_disk_service_version,
146
+ _service_health,
147
+ _stop_local_service,
148
+ )
149
+
150
+ restarted = False
151
+ health = _service_health(service_url)
152
+ if health is not None:
153
+ expected_version = _read_on_disk_service_version(wdk_wallet_root)
154
+ running_version = str(health.get("version") or "").strip()
155
+ if expected_version is None or running_version == expected_version:
156
+ return {"started": False, "already_healthy": True}
157
+ _stop_local_service(service_url)
158
+ restarted = True
142
159
 
143
160
  if not _is_local_service_url(service_url):
144
161
  raise SystemExit(
@@ -178,6 +195,7 @@ def _auto_start_local_service(
178
195
  return {
179
196
  "started": True,
180
197
  "already_healthy": False,
198
+ "restarted": restarted,
181
199
  "pid": process.pid,
182
200
  "log_path": str(log_path),
183
201
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-wallet",
3
3
  "displayName": "Agent Wallet",
4
- "version": "0.1.37",
4
+ "version": "0.1.44",
5
5
  "description": "Claude Code bridge for the existing AgentLayer wallet runtime. Connects to Solana, Bitcoin, and EVM wallets without creating a new one.",
6
6
  "author": {
7
7
  "name": "AgentLayer"
@@ -30,7 +30,36 @@ Primary design rules:
30
30
 
31
31
  ## Installation
32
32
 
33
- ### Automated (recommended)
33
+ ### From inside Claude Code (no terminal needed)
34
+
35
+ The plugin ships in a git marketplace at the repo root, so you can install it —
36
+ and its backend runtime — without leaving the CLI. Two commands, then restart:
37
+
38
+ ```text
39
+ /plugin marketplace add lopushok9/Agent-Layer
40
+ /plugin install agent-wallet@agentlayer
41
+ ```
42
+
43
+ Restart Claude Code (or `/reload-plugins`). On the next session start a
44
+ `SessionStart` hook runs `scripts/bootstrap_backend.sh`, a thin bridge to the
45
+ npm installer (`npx @agentlayer.tech/wallet install --yes`). The marketplace only
46
+ copies this MCP bridge into Claude Code's cache; the bootstrap step lays down the
47
+ Python backend runtime (venv + `agent_wallet` + `server.py`) that the bridge
48
+ talks to, with the wallet configured out of the box. It is idempotent and a
49
+ no-op once the backend is healthy.
50
+
51
+ - `/wallet-setup` — install (or repair) the backend explicitly instead of
52
+ waiting for the hook (requires `/reload-plugins` first so the command is
53
+ registered).
54
+ - `AGENT_WALLET_AUTO_BOOTSTRAP=0` — opt out of the auto-install: the
55
+ `SessionStart` hook then only reminds you to run `/wallet-setup` instead of
56
+ installing the backend itself.
57
+
58
+ For near-zero typed commands, pre-register the marketplace in
59
+ `.claude/settings.json` with `extraKnownMarketplaces` + `enabledPlugins` so
60
+ Claude Code prompts to install on trust.
61
+
62
+ ### Automated via npm
34
63
 
35
64
  ```bash
36
65
  npx @agentlayer.tech/wallet install --yes
@@ -0,0 +1,24 @@
1
+ ---
2
+ description: Install or repair the AgentLayer wallet backend runtime without leaving Claude Code.
3
+ allowed-tools: Bash(sh:*), Bash(npx:*)
4
+ ---
5
+
6
+ Install (or repair) the AgentLayer wallet backend that this plugin bridges to.
7
+
8
+ Run the bootstrap bridge to the npm installer:
9
+
10
+ ```
11
+ sh "${CLAUDE_PLUGIN_ROOT}/scripts/bootstrap_backend.sh" install
12
+ ```
13
+
14
+ Then report the outcome to the user:
15
+
16
+ - If it succeeded, tell them the backend is installed and that they should
17
+ restart Claude Code (or reload the `agent-wallet` plugin) so the wallet tools
18
+ become available.
19
+ - If it failed because Node.js or Python is missing, relay the exact requirement
20
+ (Node.js 18+, Python >= 3.10 with venv) and the manual fallback command from
21
+ the script output.
22
+
23
+ Do not pass or generate any secrets. The npm installer manages the local wallet
24
+ keys and sealed secrets under `OPENCLAW_HOME` on its own.
@@ -0,0 +1,16 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "matcher": "startup|resume|clear",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "timeout": 300,
10
+ "command": "sh -c 'if [ \"${AGENT_WALLET_AUTO_BOOTSTRAP:-1}\" = \"1\" ]; then \"${CLAUDE_PLUGIN_ROOT}/scripts/bootstrap_backend.sh\" install >&2; else \"${CLAUDE_PLUGIN_ROOT}/scripts/bootstrap_backend.sh\" check >/dev/null 2>&1 || echo \"AgentLayer wallet backend is not installed. Run /wallet-setup to install it.\"; fi'"
11
+ }
12
+ ]
13
+ }
14
+ ]
15
+ }
16
+ }