primecli 0.5.3__tar.gz → 0.5.5__tar.gz

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 (23) hide show
  1. {primecli-0.5.3 → primecli-0.5.5}/PKG-INFO +1 -1
  2. primecli-0.5.5/primecli/__init__.py +59 -0
  3. {primecli-0.5.3 → primecli-0.5.5}/primecli/arbprime.py +32 -25
  4. {primecli-0.5.3 → primecli-0.5.5}/primecli/degenprime.py +77 -5
  5. {primecli-0.5.3 → primecli-0.5.5}/primecli/deltaprime.py +32 -25
  6. {primecli-0.5.3 → primecli-0.5.5}/primecli/health_monitor.py +21 -21
  7. {primecli-0.5.3 → primecli-0.5.5}/primecli.egg-info/PKG-INFO +1 -1
  8. {primecli-0.5.3 → primecli-0.5.5}/pyproject.toml +1 -1
  9. primecli-0.5.3/primecli/__init__.py +0 -8
  10. {primecli-0.5.3 → primecli-0.5.5}/LICENSE +0 -0
  11. {primecli-0.5.3 → primecli-0.5.5}/README.md +0 -0
  12. {primecli-0.5.3 → primecli-0.5.5}/primecli.egg-info/SOURCES.txt +0 -0
  13. {primecli-0.5.3 → primecli-0.5.5}/primecli.egg-info/dependency_links.txt +0 -0
  14. {primecli-0.5.3 → primecli-0.5.5}/primecli.egg-info/entry_points.txt +0 -0
  15. {primecli-0.5.3 → primecli-0.5.5}/primecli.egg-info/requires.txt +0 -0
  16. {primecli-0.5.3 → primecli-0.5.5}/primecli.egg-info/top_level.txt +0 -0
  17. {primecli-0.5.3 → primecli-0.5.5}/setup.cfg +0 -0
  18. {primecli-0.5.3 → primecli-0.5.5}/tests/test_cross_file_identity.py +0 -0
  19. {primecli-0.5.3 → primecli-0.5.5}/tests/test_gas_pricing.py +0 -0
  20. {primecli-0.5.3 → primecli-0.5.5}/tests/test_health_monitor.py +0 -0
  21. {primecli-0.5.3 → primecli-0.5.5}/tests/test_paraswap_validator.py +0 -0
  22. {primecli-0.5.3 → primecli-0.5.5}/tests/test_redstone_encoding.py +0 -0
  23. {primecli-0.5.3 → primecli-0.5.5}/tests/test_to_wei_units.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: primecli
3
- Version: 0.5.3
3
+ Version: 0.5.5
4
4
  Summary: Agent-friendly CLI tools for the DeltaPrime (Avalanche + Arbitrum) and DegenPrime (Base) lending and leverage protocols. Preview-by-default; no Etherscan key required.
5
5
  Author: Mnemosyne-quest contributors
6
6
  License: MIT
@@ -0,0 +1,59 @@
1
+ """primecli - command-line tools for DeltaPrime (Avalanche + Arbitrum) and DegenPrime (Base)."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version as _pkg_version
4
+ import json
5
+ import os
6
+ import sys
7
+ import urllib.request
8
+
9
+ try:
10
+ __version__ = _pkg_version("primecli")
11
+ except PackageNotFoundError: # running from source without an install
12
+ __version__ = "0.0.0+unknown"
13
+
14
+ _VERSION_CHECK_URL = "https://pypi.org/pypi/primecli/json"
15
+ _VERSION_TIMEOUT = 3 # seconds
16
+
17
+
18
+ def _parse_version(v: str) -> tuple:
19
+ """Parse 'X.Y.Z' into a sortable tuple. Non-numeric suffixes become -inf."""
20
+ parts = v.split(".")
21
+ nums = []
22
+ for p in parts:
23
+ try:
24
+ nums.append(int(p))
25
+ except ValueError:
26
+ nums.append(-1)
27
+ # Pad to 3 elements
28
+ while len(nums) < 3:
29
+ nums.append(0)
30
+ return tuple(nums[:3])
31
+
32
+
33
+ def check_version(suppress_flag: bool = False) -> None:
34
+ """Print a one-line upgrade hint to stderr when the installed version is behind
35
+ the latest on PyPI. Suppress with the env var PRIMECLI_NO_VERSION_CHECK=1,
36
+ the CLI flag --no-version-check, or pass suppress_flag=True."""
37
+ if suppress_flag or os.environ.get("PRIMECLI_NO_VERSION_CHECK") == "1":
38
+ return
39
+ if "--no-version-check" in sys.argv:
40
+ return
41
+ if __version__ in ("0.0.0+unknown",):
42
+ return
43
+ try:
44
+ req = urllib.request.Request(_VERSION_CHECK_URL)
45
+ with urllib.request.urlopen(req, timeout=_VERSION_TIMEOUT) as resp:
46
+ data = json.loads(resp.read().decode())
47
+ latest = data.get("info", {}).get("version", "")
48
+ if not latest:
49
+ return
50
+ installed = _parse_version(__version__)
51
+ latest_v = _parse_version(latest)
52
+ if installed < latest_v:
53
+ print(
54
+ f"⚠️ primecli {__version__} is outdated. Latest is {latest}. "
55
+ f"Upgrade: pip install --upgrade primecli",
56
+ file=sys.stderr,
57
+ )
58
+ except Exception:
59
+ pass # network failure or parse error → silent
@@ -59,7 +59,7 @@ back to balances-only if the gateway is unreachable).
59
59
 
60
60
  NOTE: prime-summary shows TWO health metrics — don't confuse them:
61
61
  - "Health ratio (chain)": on-chain getHealthRatio. 1.0 = liquidation, >1.0 = solvent.
62
- - "Health (Bruno 0-100%)": equity-based, frontend-style. 0% = liquidation, 50% = half
62
+ - "Health (0-100%)": equity-based, frontend-style. 0% = liquidation, 50% = half
63
63
  borrowing power used, 100% = no debt.
64
64
  Formula for the latter: equity=supplied-debt, max_debt=equity*(tier-1),
65
65
  pct=(max_debt-debt)/max_debt*100. tier=5 (BASIC) or 10 (PREMIUM).
@@ -228,13 +228,19 @@ if _hm is None:
228
228
  _spec.loader.exec_module(_hm)
229
229
  health_monitor = _hm
230
230
 
231
+ # Version check (silent on network failure or old install)
232
+ try:
233
+ from primecli import check_version
234
+ except ImportError:
235
+ def check_version(*a, **kw): pass
236
+
231
237
  # Arbitrum One RPC. Override with ARBPRIME_RPC for a paid Alchemy/Infura endpoint.
232
238
  ARBITRUM_RPC = os.environ.get("ARBPRIME_RPC", "https://arb1.arbitrum.io/rpc")
233
239
  EXPLORER = "https://arbiscan.io" # display/links only — never used for ABI fetch
234
240
  CHAIN_ID = 42161
235
241
  ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
236
242
  # ── Agent / wallet selection ─────────────────────────────────────────────────
237
- # Agent-agnostic: any agent (Parakletos, Paraklaudios, or another of Bruno's) runs
243
+ # Agent-agnostic: any agent (Parakletos, Paraklaudios, or another authorized agent) runs
238
244
  # this same tool with its OWN wallet. The Prime Account is derived on-chain from the
239
245
  # wallet owner (getLoanForOwner), so each agent automatically operates on its own
240
246
  # Prime Account — no per-agent addresses are hardcoded. The same EVM key works on
@@ -317,8 +323,8 @@ REDSTONE_GATEWAYS = [
317
323
  ]
318
324
 
319
325
  # Active pool proxies — LIVE Arbitrum deployment, on-chain verified 2026-06-03.
320
- # getAllPoolAssets live = [USDC, DAI, BTC, ARB, ETH]; the DAI pool is DROPPED per
321
- # Bruno, leaving 4. The native-wrapped pool is `eth` (underlying WETH, account symbol
326
+ # getAllPoolAssets live = [USDC, DAI, BTC, ARB, ETH]; the DAI pool is DROPPED,
327
+ # leaving 4. The native-wrapped pool is `eth` (underlying WETH, account symbol
322
328
  # "ETH"): its native deposit path uses depositNativeToken() (wraps ETH -> WETH).
323
329
  POOLS = {
324
330
  "eth": {
@@ -1968,23 +1974,23 @@ def gather_lending(w3, account):
1968
1974
  r["usd"] = None
1969
1975
  return out
1970
1976
 
1971
- def _compute_bruno_health(data: dict, tier_code: int = 0) -> dict:
1972
- """Compute Bruno's 0-100% health from gather_lending data + tier.
1977
+ def _compute_health_pct(data: dict, tier_code: int = 0) -> dict:
1978
+ """Compute equity-based health (0-100%) from gather_lending data + tier.
1973
1979
 
1974
1980
  DeltaPrime has *two* health metrics that agents must not confuse:
1975
1981
 
1976
1982
  1. health_ratio (on-chain, getHealthRatio): 1.0 = liquidation, >1.0 = solvent.
1977
1983
  This is the raw weighted-collateral / debt ratio from the SolvencyFacet.
1978
1984
 
1979
- 2. bruno_pct (equity-based, 0-100%): the scale used in the DeltaPrime frontend
1985
+ 2. health_pct (equity-based, 0-100%): the scale used in the DeltaPrime frontend
1980
1986
  and the account-health-monitor cron. 0% = liquidation, 100% = no debt.
1981
1987
  Formula:
1982
1988
  equity = supplied_usd - debt_usd
1983
1989
  max_mult = 10 if PREMIUM tier else 5 if BASIC
1984
1990
  max_debt = equity * (max_mult - 1)
1985
- bruno_pct = (max_debt - debt_usd) / max_debt * 100
1991
+ health_pct = (max_debt - debt_usd) / max_debt * 100
1986
1992
 
1987
- Returns dict with keys: bruno_pct, supplied_usd, debt_usd, equity, max_debt,
1993
+ Returns dict with keys: health_pct, supplied_usd, debt_usd, equity, max_debt,
1988
1994
  tier_label, or error.
1989
1995
  """
1990
1996
  supplied_usd = sum(r.get("usd", 0) or 0 for r in data.get("supplied", []))
@@ -1995,18 +2001,18 @@ def _compute_bruno_health(data: dict, tier_code: int = 0) -> dict:
1995
2001
  max_mult = {0: 5, 1: 10}.get(tier_code, 5)
1996
2002
 
1997
2003
  if equity <= 0.01:
1998
- return {"bruno_pct": 0.0, "supplied_usd": round(supplied_usd, 2),
2004
+ return {"health_pct": 0.0, "supplied_usd": round(supplied_usd, 2),
1999
2005
  "debt_usd": round(debt_usd, 2), "equity": round(equity, 2),
2000
2006
  "max_debt": 0.0, "tier": tier_label, "error": "equity near zero"}
2001
2007
 
2002
2008
  max_debt = equity * (max_mult - 1)
2003
2009
  if max_debt > 0 and debt_usd >= 0:
2004
- bruno_pct = (max_debt - min(debt_usd, max_debt)) / max_debt * 100
2005
- bruno_pct = max(0.0, min(100.0, bruno_pct))
2010
+ health_pct = (max_debt - min(debt_usd, max_debt)) / max_debt * 100
2011
+ health_pct = max(0.0, min(100.0, health_pct))
2006
2012
  else:
2007
- bruno_pct = 100.0
2013
+ health_pct = 100.0
2008
2014
 
2009
- return {"bruno_pct": round(bruno_pct, 1), "supplied_usd": round(supplied_usd, 2),
2015
+ return {"health_pct": round(health_pct, 1), "supplied_usd": round(supplied_usd, 2),
2010
2016
  "debt_usd": round(debt_usd, 2), "equity": round(equity, 2),
2011
2017
  "max_debt": round(max_debt, 2), "tier": tier_label}
2012
2018
 
@@ -2055,22 +2061,22 @@ def cmd_prime_summary():
2055
2061
  # astronomically large there); render that as ">1000" rather than a junk number.
2056
2062
  ratio_str = ">1000.00 (negligible debt)" if ratio is None else f"{ratio:.4f}"
2057
2063
  print(f" Health ratio (chain): {ratio_str} (>1.0 = solvent, 1.0 = liquidation)")
2058
- # ─── Bruno's 0-100% health (equity-based, uses tier multiplier) ───
2059
- # Different from health_ratio! See _compute_bruno_health docstring.
2064
+ # ─── Equity-based health (0-100%) ───
2065
+ # Different from health_ratio! See _compute_health_pct docstring.
2060
2066
  # Get tier from the Prime Account (oracle-free view)
2061
2067
  try:
2062
2068
  tier_info = gather_prime_tier(w3, acct, account)
2063
2069
  tier_code = tier_info.get("tier_code", 0)
2064
2070
  except Exception:
2065
2071
  tier_code = 0
2066
- bh = _compute_bruno_health(data, tier_code)
2067
- if "error" not in bh:
2068
- print(f" Health (Bruno 0-100%): {bh['bruno_pct']:.1f}%")
2069
- print(f" (supplied=${bh['supplied_usd']:.2f}, debt=${bh['debt_usd']:.2f},"
2070
- f" equity=${bh['equity']:.2f}, max_debt=${bh['max_debt']:.2f}, {bh['tier']})")
2072
+ hp = _compute_health_pct(data, tier_code)
2073
+ if "error" not in hp:
2074
+ print(f" Health (0-100%): {hp['health_pct']:.1f}%")
2075
+ print(f" (supplied=${hp['supplied_usd']:.2f}, debt=${hp['debt_usd']:.2f},"
2076
+ f" equity=${hp['equity']:.2f}, max_debt=${hp['max_debt']:.2f}, {hp['tier']})")
2071
2077
  print(f" 0%=liquidation 50%=half borrowing power used 100%=no debt")
2072
2078
  else:
2073
- print(f" Health (Bruno 0-100%): N/A ({bh['error']})")
2079
+ print(f" Health (0-100%): N/A ({hp['error']})")
2074
2080
  print(f" Solvent: {'yes' if data['solvent'] else 'NO — liquidatable'}")
2075
2081
  else:
2076
2082
  print(f" Health/solvency: RedStone fetch/call failed ({data.get('solvency_error', 'error')}); "
@@ -5071,12 +5077,12 @@ def gather_defi() -> dict:
5071
5077
  result["total_usd"] = lending["total_value_usd"]
5072
5078
  result["health_ratio"] = lending["health_ratio"]
5073
5079
  result["solvent"] = lending["solvent"]
5074
- # Compute Bruno's 0-100% health from lending data + tier
5075
- result["bruno_pct"] = _compute_bruno_health(lending, tier.get("tier_code", 0)).get("bruno_pct")
5080
+ # Compute equity-based health (0-100%) from lending data + tier
5081
+ result["health_pct"] = _compute_health_pct(lending, tier.get("tier_code", 0)).get("health_pct")
5076
5082
  if lending["supplied"] or lending["borrowed"]:
5077
5083
  result["groups"].append({
5078
5084
  "type": "Lending / Leverage", "health_ratio": lending["health_ratio"],
5079
- "bruno_pct": result["bruno_pct"],
5085
+ "health_pct": result["health_pct"],
5080
5086
  "supplied": [{"symbol": r["symbol"], "balance": r["balance"], "usd": r.get("usd")}
5081
5087
  for r in lending["supplied"]],
5082
5088
  "borrowed": [{"symbol": r["symbol"], "balance": r["balance"], "usd": r.get("usd")}
@@ -5360,6 +5366,7 @@ def cmd_prime_bridge(from_chain: str = "avax", amount: float = None, execute: bo
5360
5366
  print(f" Tx: {src_cfg['explorer']}/{tx_hash.hex()}")
5361
5367
 
5362
5368
  def main():
5369
+ check_version()
5363
5370
  args = sys.argv[1:] if len(sys.argv) > 1 else []
5364
5371
  # Global wallet selector: --as <agent>, stripped before command dispatch.
5365
5372
  global _SELECTED_AGENT, _CLI_KEY
@@ -104,6 +104,12 @@ if _hm is None:
104
104
  _spec.loader.exec_module(_hm)
105
105
  health_monitor = _hm
106
106
 
107
+ # Version check (silent on network failure or old install)
108
+ try:
109
+ from primecli import check_version
110
+ except ImportError:
111
+ def check_version(*a, **kw): pass
112
+
107
113
  # Default Base RPC. mainnet.base.org rate-limits hard (429 within a few calls); the
108
114
  # publicnode endpoint is fronted by a load balancer with much higher anonymous limits
109
115
  # and has been the most reliable free option for this tool's traffic pattern (lots of
@@ -112,6 +118,7 @@ health_monitor = _hm
112
118
  BASE_RPC = os.environ.get("DEGENPRIME_RPC", "https://base.publicnode.com")
113
119
  EXPLORER = "https://basescan.org"
114
120
  CHAIN_ID = 8453
121
+ DEGEN_MAX_MULT = 5 # Fixed max leverage multiplier (no tier system on DegenPrime)
115
122
  ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
116
123
 
117
124
  # ── Signing key resolution ──────────────────────────────────────────────────
@@ -570,6 +577,41 @@ def _asset_meta(w3, symbol: str):
570
577
  # gateway once instead of per-feed-symbol. Cleared implicitly when the process exits.
571
578
  _redstone_gateway_cache = None
572
579
 
580
+ def _compute_health_pct(supplied: list, borrowed: list, max_mult: int = None) -> dict:
581
+ """Compute equity-based health (0-100%) from per-asset supplied/borrowed data.
582
+
583
+ 0% = liquidation, 50% = half borrowing power used, 100% = no debt.
584
+ Formula:
585
+ equity = supplied_usd - debt_usd
586
+ max_mult = 5 (fixed for DegenPrime; no tier system)
587
+ max_debt = equity * (max_mult - 1)
588
+ health_pct = (max_debt - debt_usd) / max_debt * 100
589
+
590
+ Returns dict with keys: health_pct, supplied_usd, debt_usd, equity, max_debt, or error.
591
+ """
592
+ if max_mult is None:
593
+ max_mult = DEGEN_MAX_MULT
594
+ supplied_usd = sum(r.get("usd", 0) or 0 for r in supplied)
595
+ debt_usd = sum(r.get("usd", 0) or 0 for r in borrowed)
596
+ equity = supplied_usd - debt_usd
597
+
598
+ if equity <= 0.01:
599
+ return {"health_pct": 0.0, "supplied_usd": round(supplied_usd, 2),
600
+ "debt_usd": round(debt_usd, 2), "equity": round(equity, 2),
601
+ "max_debt": 0.0, "error": "equity near zero"}
602
+
603
+ max_debt = equity * (max_mult - 1)
604
+ if max_debt > 0 and debt_usd >= 0:
605
+ health_pct = (max_debt - min(debt_usd, max_debt)) / max_debt * 100
606
+ health_pct = max(0.0, min(100.0, health_pct))
607
+ else:
608
+ health_pct = 100.0
609
+
610
+ return {"health_pct": round(health_pct, 1), "supplied_usd": round(supplied_usd, 2),
611
+ "debt_usd": round(debt_usd, 2), "equity": round(equity, 2),
612
+ "max_debt": round(max_debt, 2), "tier": "FIXED_5X"}
613
+
614
+
573
615
  def _redstone_fetch_packages(use_cache: bool = True) -> dict:
574
616
  """Fetch the latest signed price packages from the RedStone gateway. Returns the
575
617
  per-feed map: {feedSymbol: [package, ...]} with one package per signer. The per-run
@@ -1458,11 +1500,19 @@ def gather_defi() -> dict:
1458
1500
  row["usd"] = round(amt * usd, 2)
1459
1501
  return row
1460
1502
 
1503
+ _rows_supplied = [_row(r) for r in supplied]
1504
+ _rows_borrowed = [_row(r) for r in borrowed]
1505
+
1506
+ # Compute equity-based health (0-100%)
1507
+ _hp = _compute_health_pct(_rows_supplied, _rows_borrowed)
1508
+ result["health_pct"] = _hp.get("health_pct")
1509
+
1461
1510
  if supplied or borrowed:
1462
1511
  result["groups"].append({
1463
1512
  "type": "Lending / Leverage", "health_ratio": solvency["ratio"],
1464
- "supplied": [_row(r) for r in supplied],
1465
- "borrowed": [_row(r) for r in borrowed],
1513
+ "health_pct": result["health_pct"],
1514
+ "supplied": _rows_supplied,
1515
+ "borrowed": _rows_borrowed,
1466
1516
  })
1467
1517
 
1468
1518
  # Savings: the EOA's own pool deposits ("Diamond Hands"), independent of the Degen
@@ -1578,16 +1628,22 @@ def cmd_summary(as_json: bool = False):
1578
1628
  row["usd"] = round(row["amount"] * usd, 2)
1579
1629
  return row
1580
1630
 
1631
+ # Compute equity-based health (0-100%)
1632
+ _hp_rows = [_asset_row(r) for r in supplied]
1633
+ _hp_borrowed = [_asset_row(r) for r in borrowed]
1634
+ _hp = _compute_health_pct(_hp_rows, _hp_borrowed)
1635
+
1581
1636
  out = {
1582
1637
  "wallet": acct.address,
1583
1638
  "account": pa,
1584
1639
  "nativeBalance": pa_eth if pa_eth >= 1e-9 else None,
1585
- "supplied": [_asset_row(r) for r in supplied],
1586
- "borrowed": [_asset_row(r) for r in borrowed],
1640
+ "supplied": _hp_rows,
1641
+ "borrowed": _hp_borrowed,
1587
1642
  "poolDeposits": [_asset_row(r) for r in pool_deposits],
1588
1643
  "totalValueUsd": solvency["total"],
1589
1644
  "debtUsd": solvency["debt"],
1590
1645
  "healthRatio": solvency["ratio"],
1646
+ "healthPct": _hp.get("health_pct"),
1591
1647
  "solvent": solvency["solvent"],
1592
1648
  "solvencyError": solvency["error"],
1593
1649
  }
@@ -1626,7 +1682,22 @@ def cmd_summary(as_json: bool = False):
1626
1682
  print(f" Total value: ${solvency['total']:,.2f}")
1627
1683
  print(f" Debt: ${solvency['debt']:,.2f}")
1628
1684
  ratio_str = ">1000.00 (negligible debt)" if solvency["ratio"] is None else f"{solvency['ratio']:.4f}"
1629
- print(f" Health ratio: {ratio_str} (>1.0 = solvent)")
1685
+ print(f" Health ratio (chain): {ratio_str} (>1.0 = solvent, 1.0 = liquidation)")
1686
+ # ─── Equity-based health (0-100%) ───
1687
+ # Different from health_ratio!
1688
+ hp = _compute_health_pct(
1689
+ [{"symbol": r["symbol"], "balance": "0", "usd": (r["raw"] / 10**r["decimals"]) * solvency["prices"].get(r["symbol"], 0)}
1690
+ for r in supplied if solvency["prices"].get(r["symbol"])],
1691
+ [{"symbol": r["symbol"], "balance": "0", "usd": (r["raw"] / 10**r["decimals"]) * solvency["prices"].get(r["symbol"], 0)}
1692
+ for r in borrowed if solvency["prices"].get(r["symbol"])],
1693
+ )
1694
+ if "error" not in hp:
1695
+ print(f" Health (0-100%): {hp['health_pct']:.1f}%")
1696
+ print(f" (supplied=${hp['supplied_usd']:.2f}, debt=${hp['debt_usd']:.2f},"
1697
+ f" equity=${hp['equity']:.2f}, max_debt=${hp['max_debt']:.2f}, {hp.get('tier','')})")
1698
+ print(f" 0%=liquidation 50%=half borrowing power used 100%=no debt")
1699
+ else:
1700
+ print(f" Health (0-100%): N/A ({hp['error']})")
1630
1701
  print(f" Solvent: {'yes' if solvency['solvent'] else 'NO - liquidatable'}")
1631
1702
  else:
1632
1703
  print(f" Health/solvency: RedStone fetch/call failed ({solvency['error']}); showing balances only")
@@ -2360,6 +2431,7 @@ def cmd_aerodrome_positions():
2360
2431
  print(" v1 lists tokenIds only. Composition + write paths deferred to v2.")
2361
2432
 
2362
2433
  def main():
2434
+ check_version()
2363
2435
  try:
2364
2436
  _dispatch()
2365
2437
  except RuntimeError as e:
@@ -52,7 +52,7 @@ back to balances-only if the gateway is unreachable).
52
52
 
53
53
  NOTE: prime-summary shows TWO health metrics — don't confuse them:
54
54
  - "Health ratio (chain)": on-chain getHealthRatio. 1.0 = liquidation, >1.0 = solvent.
55
- - "Health (Bruno 0-100%)": equity-based, frontend-style. 0% = liquidation, 50% = half
55
+ - "Health (0-100%)": equity-based, frontend-style. 0% = liquidation, 50% = half
56
56
  borrowing power used, 100% = no debt.
57
57
  Formula for the latter: equity=supplied-debt, max_debt=equity*(tier-1),
58
58
  pct=(max_debt-debt)/max_debt*100. tier=5 (BASIC) or 10 (PREMIUM).
@@ -237,12 +237,18 @@ if _hm is None:
237
237
  _spec.loader.exec_module(_hm)
238
238
  health_monitor = _hm
239
239
 
240
+ # Version check (silent on network failure or old install)
241
+ try:
242
+ from primecli import check_version
243
+ except ImportError:
244
+ def check_version(*a, **kw): pass
245
+
240
246
  AVALANCHE_RPC = os.environ.get("DELTAPRIME_RPC", "https://api.avax.network/ext/bc/C/rpc")
241
247
  EXPLORER = "https://snowtrace.io"
242
248
  CHAIN_ID = 43114
243
249
  ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
244
250
  # ── Agent / wallet selection ─────────────────────────────────────────────────
245
- # Agent-agnostic: any agent (Parakletos, Paraklaudios, or another of Bruno's) runs
251
+ # Agent-agnostic: any agent (Parakletos, Paraklaudios, or another authorized agent) runs
246
252
  # this same tool with its OWN wallet. The Prime Account is derived on-chain from the
247
253
  # wallet owner (getLoanForOwner), so each agent automatically operates on its own
248
254
  # Prime Account — no per-agent addresses are hardcoded.
@@ -441,12 +447,12 @@ TJ_ROUTER_V21 = "0xb4315e873dBcf96Ffd0acd8EA43f689D8c20fB30"
441
447
  TJ_ROUTER_V22 = "0x18556DA13313f3532c54711497A8FedAC273220E"
442
448
  TJ_MAX_BINS = 80
443
449
 
444
- # Whitelisted LB pairs exposed as tool keys, matching Bruno's live frontend (bin step in
450
+ # Whitelisted LB pairs exposed as tool keys, matching the live frontend (bin step in
445
451
  # the key suffix where a pair exists at two steps). For each: the LBPair address, the
446
452
  # router version the pair belongs to, the canonical (tokenX, tokenY) order read on-chain,
447
453
  # and the binStep. tokenX/tokenY carry the ERC20 address, the account bytes32 symbol (for
448
454
  # the in-account balance read + RedStone feed), and decimals. The 13 source-whitelisted
449
- # pairs include 4 aUSD pairs not on Bruno's frontend; those are omitted (no clean
455
+ # pairs include 4 aUSD pairs not on the live frontend; those are omitted (no clean
450
456
  # symbol/decimals + out of scope).
451
457
  _WAVAX = {"addr": "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", "symbol": "AVAX", "decimals": 18}
452
458
  _USDC = {"addr": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", "symbol": "USDC", "decimals": 6}
@@ -1995,23 +2001,23 @@ def gather_lending(w3, account):
1995
2001
  r["usd"] = None
1996
2002
  return out
1997
2003
 
1998
- def _compute_bruno_health(data: dict, tier_code: int = 0) -> dict:
1999
- """Compute Bruno's 0-100% health from gather_lending data + tier.
2004
+ def _compute_health_pct(data: dict, tier_code: int = 0) -> dict:
2005
+ """Compute equity-based health (0-100%) from gather_lending data + tier.
2000
2006
 
2001
2007
  DeltaPrime has *two* health metrics that agents must not confuse:
2002
2008
 
2003
2009
  1. health_ratio (on-chain, getHealthRatio): 1.0 = liquidation, >1.0 = solvent.
2004
2010
  This is the raw weighted-collateral / debt ratio from the SolvencyFacet.
2005
2011
 
2006
- 2. bruno_pct (equity-based, 0-100%): the scale used in the DeltaPrime frontend
2012
+ 2. health_pct (equity-based, 0-100%): the scale used in the DeltaPrime frontend
2007
2013
  and the account-health-monitor cron. 0% = liquidation, 100% = no debt.
2008
2014
  Formula:
2009
2015
  equity = supplied_usd - debt_usd
2010
2016
  max_mult = 10 if PREMIUM tier else 5 if BASIC
2011
2017
  max_debt = equity * (max_mult - 1)
2012
- bruno_pct = (max_debt - debt_usd) / max_debt * 100
2018
+ health_pct = (max_debt - debt_usd) / max_debt * 100
2013
2019
 
2014
- Returns dict with keys: bruno_pct, supplied_usd, debt_usd, equity, max_debt,
2020
+ Returns dict with keys: health_pct, supplied_usd, debt_usd, equity, max_debt,
2015
2021
  tier_label, or error.
2016
2022
  """
2017
2023
  supplied_usd = sum(r.get("usd", 0) or 0 for r in data.get("supplied", []))
@@ -2022,18 +2028,18 @@ def _compute_bruno_health(data: dict, tier_code: int = 0) -> dict:
2022
2028
  max_mult = {0: 5, 1: 10}.get(tier_code, 5)
2023
2029
 
2024
2030
  if equity <= 0.01:
2025
- return {"bruno_pct": 0.0, "supplied_usd": round(supplied_usd, 2),
2031
+ return {"health_pct": 0.0, "supplied_usd": round(supplied_usd, 2),
2026
2032
  "debt_usd": round(debt_usd, 2), "equity": round(equity, 2),
2027
2033
  "max_debt": 0.0, "tier": tier_label, "error": "equity near zero"}
2028
2034
 
2029
2035
  max_debt = equity * (max_mult - 1)
2030
2036
  if max_debt > 0 and debt_usd >= 0:
2031
- bruno_pct = (max_debt - min(debt_usd, max_debt)) / max_debt * 100
2032
- bruno_pct = max(0.0, min(100.0, bruno_pct))
2037
+ health_pct = (max_debt - min(debt_usd, max_debt)) / max_debt * 100
2038
+ health_pct = max(0.0, min(100.0, health_pct))
2033
2039
  else:
2034
- bruno_pct = 100.0
2040
+ health_pct = 100.0
2035
2041
 
2036
- return {"bruno_pct": round(bruno_pct, 1), "supplied_usd": round(supplied_usd, 2),
2042
+ return {"health_pct": round(health_pct, 1), "supplied_usd": round(supplied_usd, 2),
2037
2043
  "debt_usd": round(debt_usd, 2), "equity": round(equity, 2),
2038
2044
  "max_debt": round(max_debt, 2), "tier": tier_label}
2039
2045
 
@@ -2082,22 +2088,22 @@ def cmd_prime_summary():
2082
2088
  # astronomically large there); render that as ">1000" rather than a junk number.
2083
2089
  ratio_str = ">1000.00 (negligible debt)" if ratio is None else f"{ratio:.4f}"
2084
2090
  print(f" Health ratio (chain): {ratio_str} (>1.0 = solvent, 1.0 = liquidation)")
2085
- # ─── Bruno's 0-100% health (equity-based, uses tier multiplier) ───
2086
- # Different from health_ratio! See _compute_bruno_health docstring.
2091
+ # ─── Equity-based health (0-100%) ───
2092
+ # Different from health_ratio! See _compute_health_pct docstring.
2087
2093
  # Get tier from the Prime Account (oracle-free view)
2088
2094
  try:
2089
2095
  tier_info = gather_prime_tier(w3, acct, account)
2090
2096
  tier_code = tier_info.get("tier_code", 0)
2091
2097
  except Exception:
2092
2098
  tier_code = 0
2093
- bh = _compute_bruno_health(data, tier_code)
2094
- if "error" not in bh:
2095
- print(f" Health (Bruno 0-100%): {bh['bruno_pct']:.1f}%")
2096
- print(f" (supplied=${bh['supplied_usd']:.2f}, debt=${bh['debt_usd']:.2f},"
2097
- f" equity=${bh['equity']:.2f}, max_debt=${bh['max_debt']:.2f}, {bh['tier']})")
2099
+ hp = _compute_health_pct(data, tier_code)
2100
+ if "error" not in hp:
2101
+ print(f" Health (0-100%): {hp['health_pct']:.1f}%")
2102
+ print(f" (supplied=${hp['supplied_usd']:.2f}, debt=${hp['debt_usd']:.2f},"
2103
+ f" equity=${hp['equity']:.2f}, max_debt=${hp['max_debt']:.2f}, {hp['tier']})")
2098
2104
  print(f" 0%=liquidation 50%=half borrowing power used 100%=no debt")
2099
2105
  else:
2100
- print(f" Health (Bruno 0-100%): N/A ({bh['error']})")
2106
+ print(f" Health (0-100%): N/A ({hp['error']})")
2101
2107
  print(f" Solvent: {'yes' if data['solvent'] else 'NO — liquidatable'}")
2102
2108
  else:
2103
2109
  print(f" Health/solvency: RedStone fetch/call failed ({data.get('solvency_error', 'error')}); "
@@ -5003,12 +5009,12 @@ def gather_defi() -> dict:
5003
5009
  result["total_usd"] = lending["total_value_usd"]
5004
5010
  result["health_ratio"] = lending["health_ratio"]
5005
5011
  result["solvent"] = lending["solvent"]
5006
- # Compute Bruno's 0-100% health from lending data + tier
5007
- result["bruno_pct"] = _compute_bruno_health(lending, tier.get("tier_code", 0)).get("bruno_pct")
5012
+ # Compute equity-based health (0-100%) from lending data + tier
5013
+ result["health_pct"] = _compute_health_pct(lending, tier.get("tier_code", 0)).get("health_pct")
5008
5014
  if lending["supplied"] or lending["borrowed"]:
5009
5015
  result["groups"].append({
5010
5016
  "type": "Lending / Leverage", "health_ratio": lending["health_ratio"],
5011
- "bruno_pct": result["bruno_pct"],
5017
+ "health_pct": result["health_pct"],
5012
5018
  "supplied": [{"symbol": r["symbol"], "balance": r["balance"], "usd": r.get("usd")}
5013
5019
  for r in lending["supplied"]],
5014
5020
  "borrowed": [{"symbol": r["symbol"], "balance": r["balance"], "usd": r.get("usd")}
@@ -5145,6 +5151,7 @@ def cmd_defi(as_json: bool = True):
5145
5151
  print(json.dumps(_trim_defi_json(data), indent=2))
5146
5152
 
5147
5153
  def main():
5154
+ check_version()
5148
5155
  args = sys.argv[1:] if len(sys.argv) > 1 else []
5149
5156
  # Global wallet selector: --as <agent>, stripped before command dispatch.
5150
5157
  global _SELECTED_AGENT, _CLI_KEY
@@ -37,10 +37,10 @@ TIER_MAX = {"basic": 5, "premium": 10}
37
37
  # ════════════════════════════════════════════════════════════════════
38
38
 
39
39
  def compute_health(defi_data: dict, max_mult: int = 10) -> dict:
40
- """Compute Bruno's 0-100% health scale from defi --json data.
40
+ """Compute equity-based health (0-100%) from defi --json data.
41
41
 
42
- PREFERS the precomputed ``bruno_pct`` from defi --json (which primecli now
43
- includes as of 2026-06-04), falling back to manual calculation when absent.
42
+ PREFERS the precomputed ``health_pct`` from defi --json (which primecli >= 0.5.4
43
+ includes), falling back to manual calculation when absent.
44
44
 
45
45
  Formula:
46
46
  equity = total_supplied_usd - total_debt_usd
@@ -49,7 +49,7 @@ def compute_health(defi_data: dict, max_mult: int = 10) -> dict:
49
49
 
50
50
  This is DIFFERENT from getHealthRatio (the on-chain ratio where 1.0 = liquidation).
51
51
  Do NOT convert between the two. The ``health_ratio`` metric is the on-chain value
52
- (1.0=liquidation, >1.0=solvent). The ``bruno_pct`` is the equity-based frontend
52
+ (1.0=liquidation, >1.0=solvent). The ``health_pct`` is the equity-based frontend
53
53
  measurement (0%=liquidation, 50%=half borrowing power used, 100%=no debt).
54
54
 
55
55
  Returns dict with health metrics or error.
@@ -61,8 +61,8 @@ def compute_health(defi_data: dict, max_mult: int = 10) -> dict:
61
61
  supplied = g.get("supplied", [])
62
62
  borrowed = g.get("borrowed", [])
63
63
  health_ratio = g.get("health_ratio", 0) or 0
64
- # Use precomputed bruno_pct from defi --json if available (primecli >= 0.5.0)
65
- precomputed = g.get("bruno_pct")
64
+ # Use precomputed health_pct from defi --json if available (primecli >= 0.5.4)
65
+ precomputed = g.get("health_pct") or g.get("bruno_pct") # bruno_pct for backward compat
66
66
  if precomputed is not None:
67
67
  # Early return: precomputed value exists, enrich with detail fields
68
68
  supplied_usd = sum(s.get("usd", 0) or 0 for s in supplied)
@@ -74,7 +74,7 @@ def compute_health(defi_data: dict, max_mult: int = 10) -> dict:
74
74
  has_lb = any(sym in ("LB_AVAX_USDC", "LB_WAVAX_USDC", "JOE") or "TRADERJOE" in sym.upper() for sym in symbols)
75
75
  has_aero = any("AERO" in sym.upper() or "CL_POSITION" in sym.upper() for sym in symbols)
76
76
  return {
77
- "bruno_pct": float(precomputed),
77
+ "health_pct": float(precomputed),
78
78
  "health_ratio": round(health_ratio, 4),
79
79
  "supplied_usd": round(supplied_usd, 2),
80
80
  "debt_usd": round(debt_usd, 2),
@@ -84,7 +84,7 @@ def compute_health(defi_data: dict, max_mult: int = 10) -> dict:
84
84
  "has_gmx": has_gmx,
85
85
  "has_lb": has_lb,
86
86
  "has_aero": has_aero,
87
- "action": "computed from defi --json bruno_pct",
87
+ "action": "computed from defi --json health_pct",
88
88
  }
89
89
  else:
90
90
  supplied = defi_data.get("supplied", [])
@@ -97,7 +97,7 @@ def compute_health(defi_data: dict, max_mult: int = 10) -> dict:
97
97
 
98
98
  if equity <= 0.01:
99
99
  return {
100
- "bruno_pct": 0.0,
100
+ "health_pct": 0.0,
101
101
  "health_ratio": round(health_ratio, 4),
102
102
  "supplied_usd": round(supplied_usd, 2),
103
103
  "debt_usd": round(debt_usd, 2),
@@ -121,15 +121,15 @@ def compute_health(defi_data: dict, max_mult: int = 10) -> dict:
121
121
  has_aero = any("AERO" in sym.upper() or "CL_POSITION" in sym.upper() for sym in symbols)
122
122
 
123
123
  if max_debt > 0 and debt_usd >= 0:
124
- bruno_pct = (max_debt - min(debt_usd, max_debt)) / max_debt * 100
125
- bruno_pct = max(0.0, min(100.0, bruno_pct))
124
+ health_pct = (max_debt - min(debt_usd, max_debt)) / max_debt * 100
125
+ health_pct = max(0.0, min(100.0, health_pct))
126
126
  else:
127
- bruno_pct = 100.0
127
+ health_pct = 100.0
128
128
 
129
129
  delta_debt = (max_debt * 0.5) - debt_usd # center target = 50%
130
130
 
131
131
  return {
132
- "bruno_pct": round(bruno_pct, 1),
132
+ "health_pct": round(health_pct, 1),
133
133
  "health_ratio": round(health_ratio, 4),
134
134
  "supplied_usd": round(supplied_usd, 2),
135
135
  "debt_usd": round(debt_usd, 2),
@@ -320,7 +320,7 @@ def run_tick(
320
320
  "reason": "equity_near_zero",
321
321
  "equity": health["equity"],
322
322
  "debt": health["debt_usd"],
323
- "health_pct": health["bruno_pct"],
323
+ "health_pct": health["health_pct"],
324
324
  "label": label,
325
325
  })
326
326
  result.update(health)
@@ -336,23 +336,23 @@ def run_tick(
336
336
 
337
337
  # 5. Health swing detection (always)
338
338
  last_pct = load_last_health(state_dir)
339
- if last_pct is not None and health["bruno_pct"] is not None:
340
- diff = abs(health["bruno_pct"] - last_pct)
339
+ if last_pct is not None and health["health_pct"] is not None:
340
+ diff = abs(health["health_pct"] - last_pct)
341
341
  if diff > 10:
342
342
  write_escalation(state_dir, "health-swing", {
343
343
  "reason": "health_swing",
344
344
  "from_pct": last_pct,
345
- "to_pct": health["bruno_pct"],
345
+ "to_pct": health["health_pct"],
346
346
  "delta": diff,
347
347
  "label": label,
348
348
  })
349
349
  result["escalation"] = "health_swing"
350
- save_last_health(state_dir, health["bruno_pct"] or 0.0)
350
+ save_last_health(state_dir, health["health_pct"] or 0.0)
351
351
 
352
352
  # 6. Append to history
353
353
  entry = {
354
354
  "ts": now_iso, "mode": mode,
355
- "pct": health["bruno_pct"],
355
+ "pct": health["health_pct"],
356
356
  "equity": health["equity"],
357
357
  "debt": health["debt_usd"],
358
358
  "hr": health["health_ratio"],
@@ -369,7 +369,7 @@ def run_tick(
369
369
  write_escalation(state_dir, "incomplete-valuation", {
370
370
  "reason": "incomplete_valuation",
371
371
  "detail": val_reason,
372
- "health_pct": health["bruno_pct"],
372
+ "health_pct": health["health_pct"],
373
373
  "equity": health["equity"],
374
374
  "debt": health["debt_usd"],
375
375
  "label": label,
@@ -387,7 +387,7 @@ def run_tick(
387
387
  side = strategy.get("side", "short")
388
388
  low, high = target_range[0], target_range[1]
389
389
 
390
- pct = health["bruno_pct"]
390
+ pct = health["health_pct"]
391
391
  equity = health["equity"]
392
392
  debt = health["debt_usd"]
393
393
  raw_usdc = health["raw_usdc"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: primecli
3
- Version: 0.5.3
3
+ Version: 0.5.5
4
4
  Summary: Agent-friendly CLI tools for the DeltaPrime (Avalanche + Arbitrum) and DegenPrime (Base) lending and leverage protocols. Preview-by-default; no Etherscan key required.
5
5
  Author: Mnemosyne-quest contributors
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "primecli"
7
- version = "0.5.3"
7
+ version = "0.5.5"
8
8
  description = "Agent-friendly CLI tools for the DeltaPrime (Avalanche + Arbitrum) and DegenPrime (Base) lending and leverage protocols. Preview-by-default; no Etherscan key required."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,8 +0,0 @@
1
- """primecli - command-line tools for DeltaPrime (Avalanche + Arbitrum) and DegenPrime (Base)."""
2
-
3
- from importlib.metadata import PackageNotFoundError, version as _pkg_version
4
-
5
- try:
6
- __version__ = _pkg_version("primecli")
7
- except PackageNotFoundError: # running from source without an install
8
- __version__ = "0.0.0+unknown"
File without changes
File without changes
File without changes