@agentlayer.tech/wallet 0.1.19 → 0.1.21
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/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/README.md +110 -0
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/scripts/flash-sdk-bridge/bridge.mjs +21 -11
- package/agent-wallet/scripts/install_agent_wallet.py +247 -14
- package/bin/openclaw-agent-wallet.mjs +298 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
[](https://www.npmjs.com/package/@agentlayer.tech/wallet)
|
|
5
5
|
[](https://www.npmjs.com/package/@agentlayer.tech/wallet)
|
|
6
|
+
[](https://docs.agent-layer.tech/)
|
|
6
7
|
[](https://github.com/lopushok9/Agent-Layer/blob/main/LICENSE)
|
|
7
8
|
|
|
8
9
|
```bash
|
|
@@ -82,9 +83,13 @@ wallet status
|
|
|
82
83
|
wallet doctor
|
|
83
84
|
wallet hermes install --yes
|
|
84
85
|
wallet update --yes
|
|
86
|
+
wallet update --yes --dry-run
|
|
85
87
|
wallet rollback
|
|
86
88
|
```
|
|
87
89
|
|
|
90
|
+
`wallet update --yes` now delegates to the latest published npm package and reuses shared Python and Node dependency snapshots when they have not changed, so frequent upgrades do not need to rebuild every runtime dependency from scratch.
|
|
91
|
+
Use `wallet update --yes --dry-run` to inspect the target runtime version and whether Python/Node dependency snapshots will be reused or rebuilt before switching `current`.
|
|
92
|
+
|
|
88
93
|
## Native OpenClaw plugin installs
|
|
89
94
|
|
|
90
95
|
Use ClawHub when you want the plugin itself to be installed through OpenClaw:
|
|
@@ -115,6 +120,111 @@ sh ./setup.sh
|
|
|
115
120
|
|
|
116
121
|
If you want the installer to finish the OpenClaw plugin wiring in the same pass, provide the runtime secrets before running it:
|
|
117
122
|
|
|
123
|
+
## Wallet capabilities through external services
|
|
124
|
+
|
|
125
|
+
AgentLayer keeps keys, approvals, and signing local, but the wallet can still operate through a set of registered provider-backed tools. These tools are exposed through the OpenClaw wallet plugin as explicit service integrations rather than raw shell access, config editing, or backend switching.
|
|
126
|
+
|
|
127
|
+
### x402 paid APIs
|
|
128
|
+
|
|
129
|
+
The x402 bundle turns the wallet into a buyer for metered APIs and paid HTTP endpoints:
|
|
130
|
+
|
|
131
|
+
- `x402_search_services` - search x402-paid services through discovery providers such as CDP Bazaar and Agentic Market without spending funds.
|
|
132
|
+
- `x402_get_service_details` - resolve one discovered service or resource into a normalized detail payload before attempting payment.
|
|
133
|
+
- `x402_preview_request` - make an unpaid request, detect `402 Payment Required`, and summarize payment terms and supported payment options.
|
|
134
|
+
- `x402_pay_request` - prepare or execute the paid retry through the active wallet backend. The current flow executes the Solana exact-buyer path and keeps EVM as prepare-only.
|
|
135
|
+
|
|
136
|
+
This gives the wallet a direct bridge from service discovery to paid API consumption while preserving approval-token checks before execution.
|
|
137
|
+
|
|
138
|
+
### LI.FI cross-chain routing
|
|
139
|
+
|
|
140
|
+
The LI.FI bundle covers discovery, quote inspection, transfer tracking, and routed execution across Solana, Ethereum, and Base:
|
|
141
|
+
|
|
142
|
+
- `get_lifi_supported_chains` - list the chains currently allowed for LI.FI routing in the wallet surface.
|
|
143
|
+
- `get_lifi_quote` - fetch a read-only cross-chain quote before any execution planning.
|
|
144
|
+
- `get_lifi_transfer_status` - inspect a routed transfer by transaction hash or LI.FI step id.
|
|
145
|
+
- `swap_solana_lifi_cross_chain_tokens` - preview, prepare, or execute a Solana-origin cross-chain route into Ethereum or Base.
|
|
146
|
+
- `swap_evm_lifi_cross_chain_tokens` - preview, prepare, or execute an EVM-origin cross-chain route across Ethereum, Base, and Solana when LI.FI returns a route.
|
|
147
|
+
|
|
148
|
+
### Jupiter trading and yield
|
|
149
|
+
|
|
150
|
+
On Solana, Jupiter-backed tools cover market pricing, swaps, and Jupiter Earn vault flows:
|
|
151
|
+
|
|
152
|
+
- `get_solana_token_prices` - fetch current Solana token pricing through Jupiter.
|
|
153
|
+
- `swap_solana_tokens` - preview, prepare, or execute a Jupiter-routed Solana token swap.
|
|
154
|
+
- `get_jupiter_earn_tokens` - list Jupiter Earn vault assets currently supported on mainnet.
|
|
155
|
+
- `get_jupiter_earn_positions` - inspect wallet positions in Jupiter Earn vaults.
|
|
156
|
+
- `get_jupiter_earn_earnings` - fetch earnings for one or more Jupiter Earn positions.
|
|
157
|
+
- `jupiter_earn_deposit` - preview, prepare, or execute a Jupiter Earn deposit.
|
|
158
|
+
- `jupiter_earn_withdraw` - preview, prepare, or execute a Jupiter Earn withdrawal.
|
|
159
|
+
|
|
160
|
+
### Houdini private payouts
|
|
161
|
+
|
|
162
|
+
For privacy-preserving Solana payout flows, the wallet exposes a Houdini-backed bundle:
|
|
163
|
+
|
|
164
|
+
- `swap_solana_privately` - create a preview or approved private payout through Houdini routing. The current MVP supports same-token flows such as `SOL -> SOL` and `USDC -> USDC`.
|
|
165
|
+
- `continue_solana_private_swap` - continue a previously created Houdini order and submit the local funding transfer to the returned deposit address.
|
|
166
|
+
- `get_solana_private_swap_status` - check Houdini status for an existing private payout.
|
|
167
|
+
- `list_pending_solana_private_swaps` - list cached pending Houdini orders for the current OpenClaw session.
|
|
168
|
+
|
|
169
|
+
This flow is intentionally optimized for `preview -> execute` rather than adding a no-op prepare step.
|
|
170
|
+
|
|
171
|
+
### Kamino lending
|
|
172
|
+
|
|
173
|
+
Kamino integration gives the wallet a structured Solana lending surface:
|
|
174
|
+
|
|
175
|
+
- `get_kamino_lend_markets` - list Kamino lending markets available on Solana mainnet.
|
|
176
|
+
- `get_kamino_lend_market_reserves` - inspect reserve metrics for one Kamino market.
|
|
177
|
+
- `get_kamino_lend_user_obligations` - inspect the wallet's obligations inside a Kamino market.
|
|
178
|
+
- `get_kamino_lend_user_rewards` - fetch the wallet's Kamino rewards summary.
|
|
179
|
+
- `kamino_lend_deposit` - preview, prepare, or execute a lending deposit.
|
|
180
|
+
- `kamino_lend_withdraw` - preview, prepare, or execute a lending withdrawal.
|
|
181
|
+
- `kamino_lend_borrow` - preview, prepare, or execute a borrow.
|
|
182
|
+
- `kamino_lend_repay` - preview, prepare, or execute a repay.
|
|
183
|
+
|
|
184
|
+
### Flash Trade perps
|
|
185
|
+
|
|
186
|
+
Flash Trade integration adds a managed perpetuals surface on Solana mainnet:
|
|
187
|
+
|
|
188
|
+
- `get_flash_trade_markets` - list currently available Flash Trade markets.
|
|
189
|
+
- `get_flash_trade_positions` - inspect the wallet's open Flash Trade positions.
|
|
190
|
+
- `flash_trade_open_position` - preview, prepare, or execute a perp position open.
|
|
191
|
+
- `flash_trade_close_position` - preview, prepare, or execute a perp position close.
|
|
192
|
+
|
|
193
|
+
### Bags launch and fee-share
|
|
194
|
+
|
|
195
|
+
Bags-backed tools cover token launch and post-launch fee analytics:
|
|
196
|
+
|
|
197
|
+
- `get_bags_claimable_positions` - inspect claimable Bags fee-share positions for a Solana wallet.
|
|
198
|
+
- `get_bags_fee_analytics` - fetch analytics and optional claim history for a launched token.
|
|
199
|
+
- `claim_bags_fees` - preview, prepare, or execute a Bags fee-share claim.
|
|
200
|
+
- `launch_bags_token` - preview, prepare, or execute a Bags token launch with fee-share configuration.
|
|
201
|
+
|
|
202
|
+
### EVM DeFi integrations
|
|
203
|
+
|
|
204
|
+
The EVM wallet surface includes named DeFi integrations on `ethereum` and `base`, without exposing arbitrary calldata execution.
|
|
205
|
+
|
|
206
|
+
Velora swap routing:
|
|
207
|
+
|
|
208
|
+
- `get_evm_swap_quote` - fetch a read-only EVM swap quote.
|
|
209
|
+
- `swap_evm_tokens` - preview, prepare, or execute a routed EVM token swap.
|
|
210
|
+
|
|
211
|
+
Aave V3:
|
|
212
|
+
|
|
213
|
+
- `get_evm_aave_account` - inspect the wallet's Aave account state.
|
|
214
|
+
- `get_evm_aave_reserves` - fetch reserve data for supported Aave markets.
|
|
215
|
+
- `get_evm_aave_positions` - inspect the wallet's open Aave positions.
|
|
216
|
+
- `manage_evm_aave_position` - preview, prepare, or execute Aave position changes through the managed wallet flow.
|
|
217
|
+
|
|
218
|
+
Lido:
|
|
219
|
+
|
|
220
|
+
- `get_evm_lido_overview` - fetch Lido protocol overview data relevant to the wallet surface.
|
|
221
|
+
- `get_evm_lido_positions` - inspect the wallet's Lido positions.
|
|
222
|
+
- `get_evm_lido_withdrawal_requests` - inspect outstanding Lido withdrawal requests.
|
|
223
|
+
- `manage_evm_lido_position` - preview, prepare, or execute a Lido staking position change.
|
|
224
|
+
- `manage_evm_lido_withdrawal` - preview, prepare, or execute a Lido withdrawal management action.
|
|
225
|
+
|
|
226
|
+
Across these service-backed flows, read operations remain directly callable, while write operations stay behind preview, explicit intent, and host-issued approval tokens before execution.
|
|
227
|
+
|
|
118
228
|
Solana:
|
|
119
229
|
|
|
120
230
|
```bash
|
|
@@ -350,14 +350,22 @@ function variantToSide(sideVariant) {
|
|
|
350
350
|
return String(sideVariant ?? "");
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
+
function safeTokenSymbol(poolConfig, mintPk) {
|
|
354
|
+
try {
|
|
355
|
+
return poolConfig.getTokenFromMintPk(mintPk)?.symbol ?? null;
|
|
356
|
+
} catch {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
353
361
|
function buildMarketSnapshot(poolConfig, marketConfig, deprecated = false) {
|
|
354
|
-
const
|
|
355
|
-
const
|
|
362
|
+
const targetSymbol = safeTokenSymbol(poolConfig, marketConfig.targetMint);
|
|
363
|
+
const collateralSymbol = safeTokenSymbol(poolConfig, marketConfig.collateralMint);
|
|
356
364
|
return {
|
|
357
365
|
pool_name: poolConfig.poolName,
|
|
358
|
-
symbol:
|
|
359
|
-
market_symbol:
|
|
360
|
-
collateral_symbol:
|
|
366
|
+
symbol: targetSymbol,
|
|
367
|
+
market_symbol: targetSymbol,
|
|
368
|
+
collateral_symbol: collateralSymbol,
|
|
361
369
|
side: variantToSide(marketConfig.side),
|
|
362
370
|
market_id: marketConfig.marketId,
|
|
363
371
|
market_address: marketConfig.marketAccount.toBase58(),
|
|
@@ -374,14 +382,16 @@ function buildMarketSnapshot(poolConfig, marketConfig, deprecated = false) {
|
|
|
374
382
|
|
|
375
383
|
function buildPositionSnapshot(poolConfig, positionAccount) {
|
|
376
384
|
const marketConfig = poolConfig.getMarketConfigByPk(positionAccount.market);
|
|
377
|
-
const
|
|
378
|
-
const
|
|
385
|
+
const targetSymbol = marketConfig ? safeTokenSymbol(poolConfig, marketConfig.targetMint) : null;
|
|
386
|
+
const collateralSymbol = marketConfig
|
|
387
|
+
? safeTokenSymbol(poolConfig, marketConfig.collateralMint)
|
|
388
|
+
: null;
|
|
379
389
|
return {
|
|
380
390
|
pool_name: poolConfig.poolName,
|
|
381
|
-
symbol:
|
|
382
|
-
market_symbol:
|
|
383
|
-
collateral_symbol:
|
|
384
|
-
side: variantToSide(marketConfig
|
|
391
|
+
symbol: targetSymbol,
|
|
392
|
+
market_symbol: targetSymbol,
|
|
393
|
+
collateral_symbol: collateralSymbol,
|
|
394
|
+
side: variantToSide(marketConfig?.side),
|
|
385
395
|
is_active: Boolean(positionAccount.isActive),
|
|
386
396
|
position_address: positionAccount.pubkey.toBase58(),
|
|
387
397
|
market_address: positionAccount.market.toBase58(),
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
+
import hashlib
|
|
6
7
|
import json
|
|
7
8
|
import os
|
|
9
|
+
import platform
|
|
8
10
|
import shutil
|
|
9
11
|
import subprocess
|
|
10
12
|
import sys
|
|
@@ -114,6 +116,21 @@ def _resolve_sealed_keys_path() -> Path:
|
|
|
114
116
|
return _resolve_openclaw_home() / "sealed_keys.json"
|
|
115
117
|
|
|
116
118
|
|
|
119
|
+
def _runtime_base_for(runtime_root: Path) -> Path:
|
|
120
|
+
resolved = runtime_root.expanduser().resolve()
|
|
121
|
+
if resolved.parent.name == "releases":
|
|
122
|
+
return resolved.parent.parent
|
|
123
|
+
return resolved.parent
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _shared_runtime_root(runtime_root: Path) -> Path:
|
|
127
|
+
return _runtime_base_for(runtime_root) / "shared"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _shared_dependency_links_supported() -> bool:
|
|
131
|
+
return os.name != "nt"
|
|
132
|
+
|
|
133
|
+
|
|
117
134
|
def _atomic_write_text(path: Path, content: str, *, mode: int = 0o600) -> None:
|
|
118
135
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
119
136
|
fd, temp_path = tempfile.mkstemp(prefix=f".{path.name}.", dir=str(path.parent))
|
|
@@ -139,6 +156,21 @@ def _chmod_if_exists(path: Path, mode: int = 0o600) -> None:
|
|
|
139
156
|
return
|
|
140
157
|
|
|
141
158
|
|
|
159
|
+
def _sha256_text(parts: list[str]) -> str:
|
|
160
|
+
digest = hashlib.sha256()
|
|
161
|
+
for part in parts:
|
|
162
|
+
digest.update(part.encode("utf-8"))
|
|
163
|
+
digest.update(b"\0")
|
|
164
|
+
return digest.hexdigest()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _file_text_or_empty(path: Path) -> str:
|
|
168
|
+
try:
|
|
169
|
+
return path.read_text(encoding="utf-8")
|
|
170
|
+
except FileNotFoundError:
|
|
171
|
+
return ""
|
|
172
|
+
|
|
173
|
+
|
|
142
174
|
def build_parser() -> argparse.ArgumentParser:
|
|
143
175
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
144
176
|
parser.add_argument("--config-path", default=str(_default_config_path()))
|
|
@@ -272,6 +304,14 @@ def _sync_runtime_tree(source_root: Path, runtime_root: Path) -> dict[str, objec
|
|
|
272
304
|
def _ensure_env_file(env_path: Path, env_example_path: Path) -> bool:
|
|
273
305
|
if env_path.exists():
|
|
274
306
|
return False
|
|
307
|
+
if not env_example_path.exists():
|
|
308
|
+
source_candidate = _package_root() / ".env.example"
|
|
309
|
+
if source_candidate.exists():
|
|
310
|
+
env_example_path = source_candidate
|
|
311
|
+
else:
|
|
312
|
+
raise SystemExit(
|
|
313
|
+
f"Missing env example template at '{env_example_path}'."
|
|
314
|
+
)
|
|
275
315
|
env_path.parent.mkdir(parents=True, exist_ok=True)
|
|
276
316
|
shutil.copyfile(env_example_path, env_path)
|
|
277
317
|
_chmod_if_exists(env_path, 0o600)
|
|
@@ -366,8 +406,95 @@ def _ensure_python_wrapper(venv_path: Path) -> Path:
|
|
|
366
406
|
return wrapper
|
|
367
407
|
|
|
368
408
|
|
|
369
|
-
def
|
|
409
|
+
def _python_runtime_fingerprint(package_root: Path, python_bin: Path) -> str:
|
|
410
|
+
version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
411
|
+
return _sha256_text(
|
|
412
|
+
[
|
|
413
|
+
f"python-bin:{python_bin}",
|
|
414
|
+
f"python-version:{version}",
|
|
415
|
+
f"platform:{platform.system()}",
|
|
416
|
+
f"machine:{platform.machine()}",
|
|
417
|
+
_file_text_or_empty(package_root / "pyproject.toml"),
|
|
418
|
+
]
|
|
419
|
+
)[:24]
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _python_runtime_plan(
|
|
423
|
+
venv_path: Path,
|
|
424
|
+
package_root: Path,
|
|
425
|
+
runtime_root: Path,
|
|
426
|
+
) -> dict[str, object]:
|
|
427
|
+
if _shared_dependency_links_supported():
|
|
428
|
+
fingerprint = _python_runtime_fingerprint(package_root, Path(sys.executable))
|
|
429
|
+
shared_root = _shared_runtime_root(runtime_root) / "python" / fingerprint
|
|
430
|
+
shared_venv_path = shared_root / "venv"
|
|
431
|
+
shared_wrapper = _venv_python_wrapper(shared_venv_path)
|
|
432
|
+
return {
|
|
433
|
+
"shared": True,
|
|
434
|
+
"fingerprint": fingerprint,
|
|
435
|
+
"shared_root": str(shared_root),
|
|
436
|
+
"venv_path": str(shared_venv_path),
|
|
437
|
+
"release_link_path": str(venv_path),
|
|
438
|
+
"python_bin": str(venv_path / shared_wrapper.relative_to(shared_venv_path)),
|
|
439
|
+
"action": "reuse" if shared_venv_path.exists() else "create",
|
|
440
|
+
"exists": shared_venv_path.exists(),
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
"shared": False,
|
|
444
|
+
"fingerprint": None,
|
|
445
|
+
"shared_root": None,
|
|
446
|
+
"venv_path": str(venv_path),
|
|
447
|
+
"release_link_path": str(venv_path),
|
|
448
|
+
"python_bin": str(_venv_python_wrapper(venv_path)),
|
|
449
|
+
"action": "install",
|
|
450
|
+
"exists": _venv_python(venv_path).exists(),
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _replace_with_directory_symlink(link_path: Path, target_path: Path) -> None:
|
|
455
|
+
target_resolved = target_path.resolve()
|
|
456
|
+
if link_path.is_symlink():
|
|
457
|
+
existing_target = link_path.resolve()
|
|
458
|
+
if existing_target == target_resolved:
|
|
459
|
+
return
|
|
460
|
+
link_path.unlink()
|
|
461
|
+
elif link_path.exists():
|
|
462
|
+
if link_path.is_dir():
|
|
463
|
+
shutil.rmtree(link_path)
|
|
464
|
+
else:
|
|
465
|
+
link_path.unlink()
|
|
466
|
+
link_path.parent.mkdir(parents=True, exist_ok=True)
|
|
467
|
+
link_path.symlink_to(target_resolved, target_is_directory=True)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _ensure_python_runtime(
|
|
471
|
+
venv_path: Path,
|
|
472
|
+
package_root: Path,
|
|
473
|
+
runtime_root: Path,
|
|
474
|
+
) -> tuple[Path, bool, dict[str, object]]:
|
|
370
475
|
created = False
|
|
476
|
+
plan = _python_runtime_plan(venv_path, package_root, runtime_root)
|
|
477
|
+
if bool(plan["shared"]):
|
|
478
|
+
shared_root = Path(str(plan["shared_root"]))
|
|
479
|
+
shared_venv_path = Path(str(plan["venv_path"]))
|
|
480
|
+
python_bin = _venv_python(shared_venv_path)
|
|
481
|
+
if not python_bin.exists():
|
|
482
|
+
venv.EnvBuilder(with_pip=True).create(shared_venv_path)
|
|
483
|
+
created = True
|
|
484
|
+
subprocess.run(
|
|
485
|
+
[str(python_bin), "-m", "pip", "install", "-e", str(package_root)],
|
|
486
|
+
check=True,
|
|
487
|
+
)
|
|
488
|
+
shared_wrapper = _ensure_python_wrapper(shared_venv_path)
|
|
489
|
+
_replace_with_directory_symlink(venv_path, shared_venv_path)
|
|
490
|
+
plan["action"] = "create" if created else "reuse"
|
|
491
|
+
plan["exists"] = True
|
|
492
|
+
return (
|
|
493
|
+
venv_path / shared_wrapper.relative_to(shared_venv_path),
|
|
494
|
+
created,
|
|
495
|
+
plan,
|
|
496
|
+
)
|
|
497
|
+
|
|
371
498
|
python_bin = _venv_python(venv_path)
|
|
372
499
|
if not python_bin.exists():
|
|
373
500
|
venv.EnvBuilder(with_pip=True).create(venv_path)
|
|
@@ -377,10 +504,79 @@ def _ensure_python_runtime(venv_path: Path, package_root: Path) -> tuple[Path, b
|
|
|
377
504
|
[str(python_bin), "-m", "pip", "install", "-e", str(package_root)],
|
|
378
505
|
check=True,
|
|
379
506
|
)
|
|
380
|
-
return
|
|
507
|
+
return (
|
|
508
|
+
_ensure_python_wrapper(venv_path),
|
|
509
|
+
created,
|
|
510
|
+
plan,
|
|
511
|
+
)
|
|
381
512
|
|
|
382
513
|
|
|
383
|
-
def
|
|
514
|
+
def _node_version() -> str:
|
|
515
|
+
result = subprocess.run(
|
|
516
|
+
["node", "--version"],
|
|
517
|
+
capture_output=True,
|
|
518
|
+
text=True,
|
|
519
|
+
check=True,
|
|
520
|
+
)
|
|
521
|
+
return result.stdout.strip()
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _node_runtime_fingerprint(project_root: Path) -> str:
|
|
525
|
+
return _sha256_text(
|
|
526
|
+
[
|
|
527
|
+
f"node-version:{_node_version()}",
|
|
528
|
+
f"platform:{platform.system()}",
|
|
529
|
+
f"machine:{platform.machine()}",
|
|
530
|
+
_file_text_or_empty(project_root / "package.json"),
|
|
531
|
+
_file_text_or_empty(project_root / "package-lock.json"),
|
|
532
|
+
]
|
|
533
|
+
)[:24]
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _node_runtime_plan(project_root: Path, runtime_root: Path) -> dict[str, object]:
|
|
537
|
+
package_json = project_root / "package.json"
|
|
538
|
+
package_lock = project_root / "package-lock.json"
|
|
539
|
+
command = ["npm", "ci"] if package_lock.exists() else ["npm", "install"]
|
|
540
|
+
if _shared_dependency_links_supported():
|
|
541
|
+
fingerprint = _node_runtime_fingerprint(project_root)
|
|
542
|
+
shared_project_root = _shared_runtime_root(runtime_root) / "node" / project_root.name / fingerprint
|
|
543
|
+
shared_node_modules = shared_project_root / "node_modules"
|
|
544
|
+
return {
|
|
545
|
+
"project_root": str(project_root),
|
|
546
|
+
"package_json": str(package_json),
|
|
547
|
+
"package_lock": str(package_lock) if package_lock.exists() else None,
|
|
548
|
+
"command": command,
|
|
549
|
+
"shared": True,
|
|
550
|
+
"fingerprint": fingerprint,
|
|
551
|
+
"shared_root": str(shared_project_root),
|
|
552
|
+
"node_modules_path": str(shared_node_modules),
|
|
553
|
+
"release_link_path": str(project_root / "node_modules"),
|
|
554
|
+
"action": "reuse" if shared_node_modules.exists() else "create",
|
|
555
|
+
"exists": shared_node_modules.exists(),
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
"project_root": str(project_root),
|
|
559
|
+
"package_json": str(package_json),
|
|
560
|
+
"package_lock": str(package_lock) if package_lock.exists() else None,
|
|
561
|
+
"command": command,
|
|
562
|
+
"shared": False,
|
|
563
|
+
"fingerprint": None,
|
|
564
|
+
"shared_root": None,
|
|
565
|
+
"node_modules_path": str(project_root / "node_modules"),
|
|
566
|
+
"release_link_path": str(project_root / "node_modules"),
|
|
567
|
+
"action": "install",
|
|
568
|
+
"exists": (project_root / "node_modules").exists(),
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _copy_if_exists(source: Path, target: Path) -> None:
|
|
573
|
+
if not source.exists():
|
|
574
|
+
return
|
|
575
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
576
|
+
shutil.copy2(source, target)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _ensure_node_runtime(npm_bin: str, project_root: Path, runtime_root: Path) -> dict[str, object]:
|
|
384
580
|
package_json = project_root / "package.json"
|
|
385
581
|
if not package_json.exists():
|
|
386
582
|
raise SystemExit(f"Missing package.json for Node runtime at '{project_root}'.")
|
|
@@ -394,14 +590,30 @@ def _ensure_node_runtime(npm_bin: str, project_root: Path) -> dict[str, object]:
|
|
|
394
590
|
env["NPM_CONFIG_CACHE"] = cache_dir
|
|
395
591
|
env["npm_config_cache"] = cache_dir
|
|
396
592
|
Path(cache_dir).expanduser().mkdir(parents=True, exist_ok=True)
|
|
593
|
+
plan = _node_runtime_plan(project_root, runtime_root)
|
|
594
|
+
if bool(plan["shared"]):
|
|
595
|
+
shared_project_root = Path(str(plan["shared_root"]))
|
|
596
|
+
shared_node_modules = Path(str(plan["node_modules_path"]))
|
|
597
|
+
created = False
|
|
598
|
+
if not shared_node_modules.exists():
|
|
599
|
+
shared_project_root.mkdir(parents=True, exist_ok=True)
|
|
600
|
+
_copy_if_exists(package_json, shared_project_root / "package.json")
|
|
601
|
+
_copy_if_exists(package_lock, shared_project_root / "package-lock.json")
|
|
602
|
+
_copy_if_exists(project_root / ".npmrc", shared_project_root / ".npmrc")
|
|
603
|
+
subprocess.run(command, cwd=shared_project_root, check=True, env=env)
|
|
604
|
+
created = True
|
|
605
|
+
_replace_with_directory_symlink(project_root / "node_modules", shared_node_modules)
|
|
606
|
+
plan["cache_dir"] = cache_dir
|
|
607
|
+
plan["created"] = created
|
|
608
|
+
plan["action"] = "create" if created else "reuse"
|
|
609
|
+
plan["exists"] = True
|
|
610
|
+
return plan
|
|
611
|
+
|
|
397
612
|
subprocess.run(command, cwd=project_root, check=True, env=env)
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
"command": command,
|
|
403
|
-
"cache_dir": cache_dir,
|
|
404
|
-
}
|
|
613
|
+
plan["cache_dir"] = cache_dir
|
|
614
|
+
plan["created"] = True
|
|
615
|
+
plan["exists"] = True
|
|
616
|
+
return plan
|
|
405
617
|
|
|
406
618
|
|
|
407
619
|
def _pending_env_names() -> list[str]:
|
|
@@ -526,13 +738,28 @@ def main() -> None:
|
|
|
526
738
|
python_bin = Path(sys.executable)
|
|
527
739
|
venv_created = False
|
|
528
740
|
existing_wrapper = _venv_python_wrapper(venv_path)
|
|
741
|
+
python_runtime: dict[str, object] = {
|
|
742
|
+
"shared": False,
|
|
743
|
+
"fingerprint": None,
|
|
744
|
+
"shared_root": None,
|
|
745
|
+
"venv_path": str(venv_path),
|
|
746
|
+
"release_link_path": str(venv_path),
|
|
747
|
+
"python_bin": str(_venv_python_wrapper(venv_path)),
|
|
748
|
+
"action": "skipped",
|
|
749
|
+
"exists": existing_wrapper.exists(),
|
|
750
|
+
}
|
|
529
751
|
if args.skip_python_setup and args.install_from_runtime and existing_wrapper.exists():
|
|
530
752
|
python_bin = existing_wrapper
|
|
531
753
|
elif not args.skip_python_setup:
|
|
532
754
|
if not args.dry_run:
|
|
533
|
-
python_bin, venv_created = _ensure_python_runtime(
|
|
755
|
+
python_bin, venv_created, python_runtime = _ensure_python_runtime(
|
|
756
|
+
venv_path,
|
|
757
|
+
package_root,
|
|
758
|
+
runtime_root,
|
|
759
|
+
)
|
|
534
760
|
else:
|
|
535
|
-
|
|
761
|
+
python_runtime = _python_runtime_plan(venv_path, package_root, runtime_root)
|
|
762
|
+
python_bin = Path(str(python_runtime["python_bin"]))
|
|
536
763
|
|
|
537
764
|
node_runtime = {
|
|
538
765
|
"skipped": bool(args.skip_node_setup),
|
|
@@ -548,17 +775,22 @@ def main() -> None:
|
|
|
548
775
|
if args.dry_run:
|
|
549
776
|
node_runtime["projects"] = [
|
|
550
777
|
{
|
|
551
|
-
|
|
778
|
+
**_node_runtime_plan(project_root, runtime_root),
|
|
552
779
|
"command": [
|
|
553
780
|
args.npm_bin,
|
|
554
781
|
"ci" if (project_root / "package-lock.json").exists() else "install",
|
|
555
782
|
],
|
|
783
|
+
"cache_dir": (
|
|
784
|
+
os.environ.get("OPENCLAW_AGENT_WALLET_NPM_CACHE")
|
|
785
|
+
or str(_resolve_openclaw_home() / "npm-cache")
|
|
786
|
+
),
|
|
787
|
+
"created": False,
|
|
556
788
|
}
|
|
557
789
|
for project_root in node_projects
|
|
558
790
|
]
|
|
559
791
|
else:
|
|
560
792
|
node_runtime["projects"] = [
|
|
561
|
-
_ensure_node_runtime(args.npm_bin, project_root)
|
|
793
|
+
_ensure_node_runtime(args.npm_bin, project_root, runtime_root)
|
|
562
794
|
for project_root in node_projects
|
|
563
795
|
]
|
|
564
796
|
|
|
@@ -600,6 +832,7 @@ def main() -> None:
|
|
|
600
832
|
"install_from_runtime": bool(args.install_from_runtime),
|
|
601
833
|
"python_bin": str(python_bin),
|
|
602
834
|
"venv_created": venv_created,
|
|
835
|
+
"python_runtime": python_runtime,
|
|
603
836
|
"node_runtime": node_runtime,
|
|
604
837
|
"runtime_sync": runtime_sync,
|
|
605
838
|
"configured": configured,
|
|
@@ -13,6 +13,8 @@ const setupPath = path.join(packageRoot, "setup.sh");
|
|
|
13
13
|
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
14
14
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
15
15
|
const packageVersion = packageJson.version;
|
|
16
|
+
const UPDATE_CLI_PATH_ENV = "OPENCLAW_AGENT_WALLET_UPDATE_CLI_PATH";
|
|
17
|
+
const UPDATE_PACKAGE_SPEC_ENV = "OPENCLAW_AGENT_WALLET_UPDATE_PACKAGE_SPEC";
|
|
16
18
|
|
|
17
19
|
function printHelp() {
|
|
18
20
|
console.log(`openclaw-agent-wallet
|
|
@@ -37,6 +39,7 @@ Examples:
|
|
|
37
39
|
npx @agentlayer.tech/wallet hermes install --yes
|
|
38
40
|
npx @agentlayer.tech/wallet install --backend none
|
|
39
41
|
npx @agentlayer.tech/wallet update --yes
|
|
42
|
+
npx @agentlayer.tech/wallet update --yes --dry-run
|
|
40
43
|
npx @agentlayer.tech/wallet status
|
|
41
44
|
|
|
42
45
|
The installer writes a versioned runtime under:
|
|
@@ -46,7 +49,23 @@ After a successful install it switches:
|
|
|
46
49
|
~/.openclaw/agent-wallet-runtime/current
|
|
47
50
|
|
|
48
51
|
Wallet files and sealed secrets remain under OPENCLAW_HOME and are not replaced
|
|
49
|
-
by updates
|
|
52
|
+
by updates. The update command fetches the latest published npm package and
|
|
53
|
+
reuses shared dependency snapshots when possible.`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function primaryBinCommand(pkg = packageJson) {
|
|
57
|
+
const bin = pkg?.bin;
|
|
58
|
+
if (!bin) return "wallet";
|
|
59
|
+
if (typeof bin === "string") {
|
|
60
|
+
const packageName = String(pkg?.name || "").trim();
|
|
61
|
+
if (packageName) {
|
|
62
|
+
const parts = packageName.split("/");
|
|
63
|
+
return parts[parts.length - 1] || "wallet";
|
|
64
|
+
}
|
|
65
|
+
return "wallet";
|
|
66
|
+
}
|
|
67
|
+
const names = Object.keys(bin);
|
|
68
|
+
return names[0] || "wallet";
|
|
50
69
|
}
|
|
51
70
|
|
|
52
71
|
function expandHome(value) {
|
|
@@ -207,6 +226,80 @@ function activeVersion(env = process.env) {
|
|
|
207
226
|
return path.basename(path.resolve(path.dirname(current), link));
|
|
208
227
|
}
|
|
209
228
|
|
|
229
|
+
function listDirectories(rootPath) {
|
|
230
|
+
try {
|
|
231
|
+
return fs
|
|
232
|
+
.readdirSync(rootPath, { withFileTypes: true })
|
|
233
|
+
.filter((entry) => entry.isDirectory())
|
|
234
|
+
.map((entry) => entry.name)
|
|
235
|
+
.sort();
|
|
236
|
+
} catch (error) {
|
|
237
|
+
if (error?.code === "ENOENT") return [];
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function activePythonRuntimeInfo(env = process.env) {
|
|
243
|
+
const currentRoot = resolvedCurrentRuntimeRoot(env);
|
|
244
|
+
if (!currentRoot) return null;
|
|
245
|
+
const linkPath = path.join(currentRoot, "agent-wallet", ".runtime-venv");
|
|
246
|
+
const exists = fs.existsSync(linkPath);
|
|
247
|
+
const symlinkTarget = readLinkOrNull(linkPath);
|
|
248
|
+
const resolvedTarget = exists ? path.resolve(path.dirname(linkPath), symlinkTarget || ".") : null;
|
|
249
|
+
return {
|
|
250
|
+
link_path: linkPath,
|
|
251
|
+
exists,
|
|
252
|
+
symlink: Boolean(symlinkTarget),
|
|
253
|
+
target: symlinkTarget || null,
|
|
254
|
+
resolved_target: resolvedTarget,
|
|
255
|
+
shared: Boolean(resolvedTarget && resolvedTarget.includes(`${path.sep}shared${path.sep}python${path.sep}`)),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function activeNodeRuntimeInfo(env = process.env) {
|
|
260
|
+
const currentRoot = resolvedCurrentRuntimeRoot(env);
|
|
261
|
+
if (!currentRoot) return [];
|
|
262
|
+
const projects = [
|
|
263
|
+
path.join(currentRoot, "wdk-btc-wallet"),
|
|
264
|
+
path.join(currentRoot, "wdk-evm-wallet"),
|
|
265
|
+
path.join(currentRoot, "agent-wallet", "scripts", "flash-sdk-bridge"),
|
|
266
|
+
];
|
|
267
|
+
return projects
|
|
268
|
+
.filter((projectRoot) => fs.existsSync(path.join(projectRoot, "package.json")))
|
|
269
|
+
.map((projectRoot) => {
|
|
270
|
+
const linkPath = path.join(projectRoot, "node_modules");
|
|
271
|
+
const exists = fs.existsSync(linkPath);
|
|
272
|
+
const symlinkTarget = readLinkOrNull(linkPath);
|
|
273
|
+
const resolvedTarget = exists ? path.resolve(path.dirname(linkPath), symlinkTarget || ".") : null;
|
|
274
|
+
return {
|
|
275
|
+
project_root: projectRoot,
|
|
276
|
+
project_name: path.basename(projectRoot),
|
|
277
|
+
link_path: linkPath,
|
|
278
|
+
exists,
|
|
279
|
+
symlink: Boolean(symlinkTarget),
|
|
280
|
+
target: symlinkTarget || null,
|
|
281
|
+
resolved_target: resolvedTarget,
|
|
282
|
+
shared: Boolean(resolvedTarget && resolvedTarget.includes(`${path.sep}shared${path.sep}node${path.sep}`)),
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function sharedSnapshotInventory(env = process.env) {
|
|
288
|
+
const runtimeBase = resolveRuntimeBase(env);
|
|
289
|
+
const sharedRoot = path.join(runtimeBase, "shared");
|
|
290
|
+
const pythonRoot = path.join(sharedRoot, "python");
|
|
291
|
+
const nodeRoot = path.join(sharedRoot, "node");
|
|
292
|
+
const nodeProjects = listDirectories(nodeRoot).map((projectName) => ({
|
|
293
|
+
project_name: projectName,
|
|
294
|
+
snapshots: listDirectories(path.join(nodeRoot, projectName)),
|
|
295
|
+
}));
|
|
296
|
+
return {
|
|
297
|
+
shared_root: sharedRoot,
|
|
298
|
+
python_snapshots: listDirectories(pythonRoot),
|
|
299
|
+
node_projects: nodeProjects,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
210
303
|
function switchSymlink(linkPath, targetPath) {
|
|
211
304
|
const absoluteTarget = path.resolve(targetPath);
|
|
212
305
|
if (!fs.existsSync(absoluteTarget)) {
|
|
@@ -216,7 +309,7 @@ function switchSymlink(linkPath, targetPath) {
|
|
|
216
309
|
fs.mkdirSync(path.dirname(linkPath), { recursive: true });
|
|
217
310
|
const tempLink = `${linkPath}.tmp-${process.pid}`;
|
|
218
311
|
try {
|
|
219
|
-
fs.rmSync(tempLink, { force: true, recursive:
|
|
312
|
+
fs.rmSync(tempLink, { force: true, recursive: true });
|
|
220
313
|
} catch {
|
|
221
314
|
// ignored
|
|
222
315
|
}
|
|
@@ -225,7 +318,7 @@ function switchSymlink(linkPath, targetPath) {
|
|
|
225
318
|
try {
|
|
226
319
|
const existing = fs.lstatSync(linkPath);
|
|
227
320
|
if (!existing.isSymbolicLink()) {
|
|
228
|
-
fs.rmSync(tempLink, { force: true });
|
|
321
|
+
fs.rmSync(tempLink, { force: true, recursive: true });
|
|
229
322
|
throw new Error(`${linkPath} exists and is not a symlink. Refusing to replace it.`);
|
|
230
323
|
}
|
|
231
324
|
} catch (error) {
|
|
@@ -268,6 +361,60 @@ function withoutCliOnlyArgs(args) {
|
|
|
268
361
|
return output;
|
|
269
362
|
}
|
|
270
363
|
|
|
364
|
+
function extractTrailingJson(text) {
|
|
365
|
+
const raw = String(text || "");
|
|
366
|
+
const newlineStart = raw.lastIndexOf("\n{");
|
|
367
|
+
const start = newlineStart >= 0 ? newlineStart + 1 : raw.indexOf("{");
|
|
368
|
+
if (start < 0) {
|
|
369
|
+
throw new Error("Could not find JSON payload in command output.");
|
|
370
|
+
}
|
|
371
|
+
return JSON.parse(raw.slice(start));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function pathVersionFromRuntimeRoot(runtimeRoot) {
|
|
375
|
+
if (!runtimeRoot) return null;
|
|
376
|
+
const normalized = path.resolve(String(runtimeRoot));
|
|
377
|
+
if (path.basename(path.dirname(normalized)) !== "releases") return null;
|
|
378
|
+
return path.basename(normalized);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function resolveCliPackageMeta(cliPath) {
|
|
382
|
+
try {
|
|
383
|
+
const root = path.resolve(path.dirname(cliPath), "..");
|
|
384
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
385
|
+
return {
|
|
386
|
+
name: String(pkg.name || packageJson.name),
|
|
387
|
+
version: String(pkg.version || ""),
|
|
388
|
+
root,
|
|
389
|
+
};
|
|
390
|
+
} catch {
|
|
391
|
+
return {
|
|
392
|
+
name: packageJson.name,
|
|
393
|
+
version: "",
|
|
394
|
+
root: "",
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function summarizeDependencyPlan(payload) {
|
|
400
|
+
const python = payload?.python_runtime && typeof payload.python_runtime === "object"
|
|
401
|
+
? {
|
|
402
|
+
action: payload.python_runtime.action || "unknown",
|
|
403
|
+
shared: Boolean(payload.python_runtime.shared),
|
|
404
|
+
fingerprint: payload.python_runtime.fingerprint || null,
|
|
405
|
+
}
|
|
406
|
+
: null;
|
|
407
|
+
const nodeProjects = Array.isArray(payload?.node_runtime?.projects)
|
|
408
|
+
? payload.node_runtime.projects.map((project) => ({
|
|
409
|
+
project_root: project.project_root,
|
|
410
|
+
action: project.action || "unknown",
|
|
411
|
+
shared: Boolean(project.shared),
|
|
412
|
+
fingerprint: project.fingerprint || null,
|
|
413
|
+
}))
|
|
414
|
+
: [];
|
|
415
|
+
return { python, node_projects: nodeProjects };
|
|
416
|
+
}
|
|
417
|
+
|
|
271
418
|
function token() {
|
|
272
419
|
return crypto.randomBytes(32).toString("base64url");
|
|
273
420
|
}
|
|
@@ -442,24 +589,25 @@ function runDoctor() {
|
|
|
442
589
|
return missing.length === 0 ? 0 : 1;
|
|
443
590
|
}
|
|
444
591
|
|
|
445
|
-
function runStatus() {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
)
|
|
462
|
-
|
|
592
|
+
function runStatus(args = []) {
|
|
593
|
+
const payload = {
|
|
594
|
+
ok: true,
|
|
595
|
+
package_name: packageJson.name,
|
|
596
|
+
package_version: packageVersion,
|
|
597
|
+
openclaw_home: resolveOpenclawHome(),
|
|
598
|
+
runtime_base: resolveRuntimeBase(),
|
|
599
|
+
current_runtime: currentRuntimePath(),
|
|
600
|
+
previous_runtime: readLinkOrNull(previousRuntimePath()),
|
|
601
|
+
active_version: activeVersion(),
|
|
602
|
+
available_releases: listReleases(),
|
|
603
|
+
};
|
|
604
|
+
if (hasFlag(args, "--verbose")) {
|
|
605
|
+
payload.verbose = true;
|
|
606
|
+
payload.active_python_runtime = activePythonRuntimeInfo();
|
|
607
|
+
payload.active_node_runtimes = activeNodeRuntimeInfo();
|
|
608
|
+
payload.shared_snapshot_inventory = sharedSnapshotInventory();
|
|
609
|
+
}
|
|
610
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
463
611
|
return 0;
|
|
464
612
|
}
|
|
465
613
|
|
|
@@ -467,14 +615,19 @@ function buildInstallerEnv(args) {
|
|
|
467
615
|
const env = { ...process.env };
|
|
468
616
|
const sealedKeysPath = path.join(resolveOpenclawHome(env), "sealed_keys.json");
|
|
469
617
|
const sealedKeysExist = fs.existsSync(sealedKeysPath);
|
|
618
|
+
const dryRun = hasFlag(args, "--dry-run");
|
|
470
619
|
if (!env.AGENT_WALLET_BOOT_KEY) {
|
|
471
|
-
const existingBootKey =
|
|
620
|
+
const existingBootKey =
|
|
621
|
+
resolveBootKeyFromFile(env) ||
|
|
622
|
+
readTextIfExists(defaultBootKeyFile(env)).trim() ||
|
|
623
|
+
currentBootKey(env);
|
|
472
624
|
if (existingBootKey) {
|
|
473
625
|
env.AGENT_WALLET_BOOT_KEY = existingBootKey;
|
|
474
626
|
}
|
|
475
627
|
}
|
|
476
628
|
|
|
477
629
|
const shouldGenerateSecrets =
|
|
630
|
+
!dryRun &&
|
|
478
631
|
!hasFlag(args, "--no-auto-secrets") &&
|
|
479
632
|
(hasFlag(args, "--yes") || !env.AGENT_WALLET_BOOT_KEY);
|
|
480
633
|
|
|
@@ -554,6 +707,14 @@ function runInstall(args, { commandName = "install" } = {}) {
|
|
|
554
707
|
});
|
|
555
708
|
}
|
|
556
709
|
|
|
710
|
+
const pythonInfo = activePythonRuntimeInfo(env);
|
|
711
|
+
const nodeInfo = activeNodeRuntimeInfo(env)
|
|
712
|
+
.map((item) => `${item.project_name}:${item.shared ? "shared" : item.exists ? "local" : "missing"}`)
|
|
713
|
+
.join(", ");
|
|
714
|
+
console.error(
|
|
715
|
+
`Update summary: version=${packageVersion} active=${activeVersion(env) || packageVersion} python=${pythonInfo?.shared ? "shared" : pythonInfo?.exists ? "local" : "missing"} node=[${nodeInfo}]`,
|
|
716
|
+
);
|
|
717
|
+
|
|
557
718
|
console.error(
|
|
558
719
|
JSON.stringify(
|
|
559
720
|
{
|
|
@@ -572,6 +733,119 @@ function runInstall(args, { commandName = "install" } = {}) {
|
|
|
572
733
|
return 0;
|
|
573
734
|
}
|
|
574
735
|
|
|
736
|
+
function resolveUpdatePackageSpec(env = process.env) {
|
|
737
|
+
const explicit = String(env[UPDATE_PACKAGE_SPEC_ENV] || "").trim();
|
|
738
|
+
if (explicit) return explicit;
|
|
739
|
+
return `${packageJson.name}@latest`;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function runDelegatedInstallForUpdate(args, { captureOutput = false } = {}) {
|
|
743
|
+
const localCliPath = String(process.env[UPDATE_CLI_PATH_ENV] || "").trim();
|
|
744
|
+
if (localCliPath) {
|
|
745
|
+
const meta = resolveCliPackageMeta(localCliPath);
|
|
746
|
+
const result = spawnSync("node", [localCliPath, "install", ...args], {
|
|
747
|
+
cwd: packageRoot,
|
|
748
|
+
stdio: captureOutput ? "pipe" : "inherit",
|
|
749
|
+
encoding: captureOutput ? "utf8" : undefined,
|
|
750
|
+
env: process.env,
|
|
751
|
+
});
|
|
752
|
+
return {
|
|
753
|
+
result,
|
|
754
|
+
delegated_via: "cli_path",
|
|
755
|
+
target_package_spec: meta.name || packageJson.name,
|
|
756
|
+
target_version_hint: meta.version || null,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const npmBin = commandPath("npm");
|
|
761
|
+
if (!npmBin) {
|
|
762
|
+
throw new Error("npm is required for `wallet update`. Install npm or run `npx @agentlayer.tech/wallet install --yes`.");
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const packageSpec = resolveUpdatePackageSpec();
|
|
766
|
+
const binCommand = primaryBinCommand();
|
|
767
|
+
const result = spawnSync(
|
|
768
|
+
npmBin,
|
|
769
|
+
["exec", "--yes", `--package=${packageSpec}`, binCommand, "--", "install", ...args],
|
|
770
|
+
{
|
|
771
|
+
cwd: packageRoot,
|
|
772
|
+
stdio: captureOutput ? "pipe" : "inherit",
|
|
773
|
+
encoding: captureOutput ? "utf8" : undefined,
|
|
774
|
+
env: process.env,
|
|
775
|
+
},
|
|
776
|
+
);
|
|
777
|
+
return {
|
|
778
|
+
result,
|
|
779
|
+
delegated_via: "npm_exec",
|
|
780
|
+
target_package_spec: packageSpec,
|
|
781
|
+
target_version_hint: null,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function runUpdate(args) {
|
|
786
|
+
const dryRun = hasFlag(args, "--dry-run");
|
|
787
|
+
if (dryRun) {
|
|
788
|
+
try {
|
|
789
|
+
const delegated = runDelegatedInstallForUpdate(args, { captureOutput: true });
|
|
790
|
+
const { result } = delegated;
|
|
791
|
+
if (result.error) {
|
|
792
|
+
console.error(result.error.message);
|
|
793
|
+
return 1;
|
|
794
|
+
}
|
|
795
|
+
if ((result.status ?? 1) !== 0) {
|
|
796
|
+
const stderr = String(result.stderr || "").trim();
|
|
797
|
+
const stdout = String(result.stdout || "").trim();
|
|
798
|
+
if (stderr) process.stderr.write(`${stderr}\n`);
|
|
799
|
+
if (stdout) process.stdout.write(`${stdout}\n`);
|
|
800
|
+
return result.status ?? 1;
|
|
801
|
+
}
|
|
802
|
+
const payload = extractTrailingJson(result.stdout || "");
|
|
803
|
+
const targetVersion =
|
|
804
|
+
delegated.target_version_hint ||
|
|
805
|
+
pathVersionFromRuntimeRoot(payload.runtime_root) ||
|
|
806
|
+
null;
|
|
807
|
+
console.log(
|
|
808
|
+
JSON.stringify(
|
|
809
|
+
{
|
|
810
|
+
ok: true,
|
|
811
|
+
command: "update",
|
|
812
|
+
dry_run: true,
|
|
813
|
+
current_version: activeVersion(),
|
|
814
|
+
installed_cli_version: packageVersion,
|
|
815
|
+
target_package_spec: delegated.target_package_spec,
|
|
816
|
+
target_version: targetVersion,
|
|
817
|
+
delegated_via: delegated.delegated_via,
|
|
818
|
+
runtime_base: resolveRuntimeBase(),
|
|
819
|
+
current_runtime: currentRuntimePath(),
|
|
820
|
+
target_runtime_root: payload.runtime_root,
|
|
821
|
+
dependency_plan: summarizeDependencyPlan(payload),
|
|
822
|
+
install_plan: payload,
|
|
823
|
+
},
|
|
824
|
+
null,
|
|
825
|
+
2,
|
|
826
|
+
),
|
|
827
|
+
);
|
|
828
|
+
return 0;
|
|
829
|
+
} catch (error) {
|
|
830
|
+
console.error(error.message);
|
|
831
|
+
return 1;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
let delegated;
|
|
836
|
+
try {
|
|
837
|
+
delegated = runDelegatedInstallForUpdate(args, { captureOutput: false });
|
|
838
|
+
} catch (error) {
|
|
839
|
+
console.error(error.message);
|
|
840
|
+
return 1;
|
|
841
|
+
}
|
|
842
|
+
if (delegated.result.error) {
|
|
843
|
+
console.error(delegated.result.error.message);
|
|
844
|
+
return 1;
|
|
845
|
+
}
|
|
846
|
+
return delegated.result.status ?? 1;
|
|
847
|
+
}
|
|
848
|
+
|
|
575
849
|
function runRollback(args) {
|
|
576
850
|
const requested = parseFlagValue(args, "--to");
|
|
577
851
|
const current = activeVersion();
|
|
@@ -755,7 +1029,7 @@ if (command === "doctor") {
|
|
|
755
1029
|
}
|
|
756
1030
|
|
|
757
1031
|
if (command === "status") {
|
|
758
|
-
process.exit(runStatus());
|
|
1032
|
+
process.exit(runStatus(args.slice(1)));
|
|
759
1033
|
}
|
|
760
1034
|
|
|
761
1035
|
if (command === "install" || command === "setup") {
|
|
@@ -763,7 +1037,7 @@ if (command === "install" || command === "setup") {
|
|
|
763
1037
|
}
|
|
764
1038
|
|
|
765
1039
|
if (command === "update") {
|
|
766
|
-
process.exit(
|
|
1040
|
+
process.exit(runUpdate(args.slice(1)));
|
|
767
1041
|
}
|
|
768
1042
|
|
|
769
1043
|
if (command === "rollback") {
|