primecli 0.5.6__tar.gz → 0.5.8__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: primecli
3
- Version: 0.5.6
3
+ Version: 0.5.8
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
@@ -137,6 +137,42 @@ ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
137
137
  # read lazily so read-only commands that don't sign never need a key at all.
138
138
  _CLI_KEY = None # set by the --key CLI flag in main()
139
139
 
140
+ # Named-wallet table shared with deltaprime/arbprime. Allows running via
141
+ # DEGENPRIME_AGENT=parakletos (or the fallback DELTAPRIME_AGENT) which is
142
+ # cleaner than passing raw keys through environment variables.
143
+ # Agent resolution also supports --as <agent> CLI flag.
144
+ AGENTS = {
145
+ "parakletos": ("/root/.openclaw/.env", "PARAKLETOS_EVM_PRIVATE_KEY"),
146
+ "paraklaudios": ("/root/paraklaudios/.credentials.env", "PARAKLAUDIOS_EVM_PRIVATE_KEY"),
147
+ }
148
+ _SELECTED_AGENT = None # set by the --as CLI flag in main()
149
+
150
+
151
+ def _read_env_var(path, var):
152
+ """Return the value of `var` from a KEY=VALUE env file, or None if absent."""
153
+ try:
154
+ for line in Path(path).read_text().splitlines():
155
+ s = line.strip()
156
+ if s.startswith(var + "="):
157
+ return s.split("=", 1)[1].strip().strip('"').strip("'")
158
+ except FileNotFoundError:
159
+ return None
160
+ return None
161
+
162
+
163
+ def _agent_key(agent):
164
+ if agent not in AGENTS:
165
+ raise RuntimeError(
166
+ f"Unknown agent '{agent}'. Known agents: {', '.join(AGENTS)}. "
167
+ f"Or set DEGENPRIME_PRIVATE_KEY, or DEGENPRIME_KEY_FILE."
168
+ )
169
+ path, var = AGENTS[agent]
170
+ key = _read_env_var(path, var)
171
+ if not key:
172
+ raise RuntimeError(f"{var} not found in {path} (agent '{agent}').")
173
+ return key
174
+
175
+
140
176
  # Core protocol addresses (verified on Base 2026-05-29).
141
177
  FACTORY_PROXY = "0x5A6a0e2702cF4603a098C3Df01f3F0DF56115456" # SmartLoansFactory TUP
142
178
  # Diamond beacon. Every Degen Account is a per-user proxy that delegates here, so the
@@ -265,12 +301,17 @@ def _set_gas_price(w3, tx_dict):
265
301
  def resolve_private_key():
266
302
  """Resolve the signing key per the documented precedence:
267
303
  1. --key <0xhex> CLI flag
268
- 2. DEGENPRIME_PRIVATE_KEY env var
269
- 3. DEGENPRIME_KEY_FILE env var (path to a file containing the 0x key)
270
- 4. DELTAPRIME_PRIVATE_KEY / DELTAPRIME_KEY_FILE (same key, both chains)
271
- Raises with a clear message if none of the four are set."""
304
+ 2. --as <agent> CLI flag
305
+ 3. DEGENPRIME_PRIVATE_KEY env var
306
+ 4. DEGENPRIME_KEY_FILE env var (path to a file containing the 0x key)
307
+ 5. DELTAPRIME_PRIVATE_KEY / DELTAPRIME_KEY_FILE (same key, both chains)
308
+ 6. DEGENPRIME_AGENT env var
309
+ 7. DELTAPRIME_AGENT env var (fallback)
310
+ Raises with a clear message if none resolve."""
272
311
  if _CLI_KEY:
273
312
  return _CLI_KEY.strip()
313
+ if _SELECTED_AGENT:
314
+ return _agent_key(_SELECTED_AGENT)
274
315
  for env_var in ("DEGENPRIME_PRIVATE_KEY", "DELTAPRIME_PRIVATE_KEY"):
275
316
  raw = os.environ.get(env_var)
276
317
  if raw:
@@ -282,6 +323,11 @@ def resolve_private_key():
282
323
  return Path(key_file).read_text().strip()
283
324
  except FileNotFoundError:
284
325
  raise RuntimeError(f"{path_var} points at {key_file} but the file does not exist.")
326
+ # Named agent via env var
327
+ for ag in ("DEGENPRIME_AGENT", "DELTAPRIME_AGENT"):
328
+ agent = os.environ.get(ag)
329
+ if agent:
330
+ return _agent_key(agent)
285
331
  raise RuntimeError(
286
332
  "No signing key found. Set DEGENPRIME_PRIVATE_KEY (raw 0x... key) or "
287
333
  "DEGENPRIME_KEY_FILE (path to a file with the key), or pass --key <0xhex>. "
@@ -2504,7 +2550,7 @@ def main():
2504
2550
  def _dispatch():
2505
2551
  args = sys.argv[1:] if len(sys.argv) > 1 else []
2506
2552
  # Global signing-key override: --key <0xhex>, stripped before command dispatch.
2507
- global _CLI_KEY
2553
+ global _SELECTED_AGENT, _CLI_KEY
2508
2554
  if "--key" in args:
2509
2555
  i = args.index("--key")
2510
2556
  if i + 1 >= len(args):
@@ -2512,6 +2558,13 @@ def _dispatch():
2512
2558
  return
2513
2559
  _CLI_KEY = args[i + 1]
2514
2560
  del args[i:i + 2]
2561
+ if "--as" in args:
2562
+ i = args.index("--as")
2563
+ if i + 1 >= len(args):
2564
+ print("--as requires an agent name. Example: --as parakletos")
2565
+ return
2566
+ _SELECTED_AGENT = args[i + 1]
2567
+ del args[i:i + 2]
2515
2568
  if not args or args[0] in ("-h", "--help"):
2516
2569
  print(__doc__)
2517
2570
  return
@@ -37,22 +37,21 @@ 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 equity-based health (0-100%) from defi --json data.
40
+ """Compute health (0-100%) using the frontend formula from DeltaPrime docs.
41
41
 
42
- PREFERS the precomputed ``health_pct`` from defi --json (which primecli >= 0.5.4
43
- includes), falling back to manual calculation when absent.
42
+ Uses the cross-margin health formula (all assets assumed same borrowing power):
43
+ equity = total_supplied_usd - total_debt_usd
44
+ health_pct = 100 * (1 - debt / (max_mult * equity))
45
+ (0% = liquidation, 100% = no debt)
44
46
 
45
- Formula:
46
- equity = total_supplied_usd - total_debt_usd
47
- max_debt = equity * (tier - 1) # PREMIUM=10, BASIC=5
48
- health% = (max_debt - debt) / max_debt * 100
47
+ Background:
48
+ The frontend uses Pr = tier / (tier + 1) and computes:
49
+ health_pct = (Pr * supplied - debt) / (Pr * equity) * 100
50
+ which simplifies to: 100 * (1 - debt / (max_mult * equity)).
49
51
 
50
- This is DIFFERENT from getHealthRatio (the on-chain ratio where 1.0 = liquidation).
51
- Do NOT convert between the two. The ``health_ratio`` metric is the on-chain value
52
- (1.0=liquidation, >1.0=solvent). The ``health_pct`` is the equity-based frontend
53
- measurement (0%=liquidation, 50%=half borrowing power used, 100%=no debt).
54
-
55
- Returns dict with health metrics or error.
52
+ DIFFERENT from the equity-based "health_pct" in defi --json / prime-summary
53
+ (which uses max_debt = equity * (tier - 1)).
54
+ The on-chain health_ratio (1.0=liquidation) is NOT used here.
56
55
  """
57
56
  # Parse groups (DeltaPrime format) or flat format (DegenPrime)
58
57
  groups = defi_data.get("groups", [])
@@ -64,22 +63,25 @@ def compute_health(defi_data: dict, max_mult: int = 10) -> dict:
64
63
  # Use precomputed health_pct from defi --json if available (primecli >= 0.5.4)
65
64
  precomputed = g.get("health_pct")
66
65
  if precomputed is not None:
67
- # Early return: precomputed value exists, enrich with detail fields
66
+ # Override health_pct with frontend formula (ignores precomputed value)
68
67
  supplied_usd = sum(s.get("usd", 0) or 0 for s in supplied)
69
68
  debt_usd = sum(b.get("usd", 0) or 0 for b in borrowed)
70
- equity = supplied_usd - debt_usd
69
+ equity = max(supplied_usd - debt_usd, 0.01)
71
70
  raw_usdc = sum(s.get("usd", 0) for s in supplied if s.get("symbol") == "USDC")
72
71
  symbols = [s.get("symbol", "") for s in supplied]
73
- has_gmx = any("GM_" in sym for sym in symbols)
72
+ has_gmx = sum(s.get("usd", 0) for s in supplied if "GM_" in s.get("symbol", "")) > 1.0
74
73
  has_lb = any(sym in ("LB_AVAX_USDC", "LB_WAVAX_USDC", "JOE") or "TRADERJOE" in sym.upper() for sym in symbols)
75
74
  has_aero = any("AERO" in sym.upper() or "CL_POSITION" in sym.upper() for sym in symbols)
75
+ # Frontend formula: health_pct = 100 * (1 - debt / (max_mult * equity))
76
+ fe_health = max(0.0, 100.0 * (1.0 - round(debt_usd, 2) / (max_mult * equity)))
77
+ fe_max_debt = round(max_mult * equity, 2)
76
78
  return {
77
- "health_pct": float(precomputed),
79
+ "health_pct": round(fe_health, 1),
78
80
  "health_ratio": round(health_ratio, 4),
79
81
  "supplied_usd": round(supplied_usd, 2),
80
82
  "debt_usd": round(debt_usd, 2),
81
83
  "equity": round(equity, 2),
82
- "max_debt": round(max(0, equity * (max_mult - 1)), 2),
84
+ "max_debt": round(max(0, max_mult * equity), 2),
83
85
  "raw_usdc": round(raw_usdc, 2),
84
86
  "has_gmx": has_gmx,
85
87
  "has_lb": has_lb,
@@ -105,14 +107,14 @@ def compute_health(defi_data: dict, max_mult: int = 10) -> dict:
105
107
  "error": "equity near zero",
106
108
  }
107
109
 
108
- max_debt = equity * (max_mult - 1)
110
+ max_debt = round(max_mult * equity, 2) # frontend formula: max debt before liquidation
109
111
 
110
112
  # Raw USDC in account
111
113
  raw_usdc = sum(s.get("usd", 0) for s in supplied if s.get("symbol") == "USDC")
112
114
 
113
115
  # Position type detection
114
116
  symbols = [s.get("symbol", "") for s in supplied]
115
- has_gmx = any("GM_" in sym for sym in symbols)
117
+ has_gmx = sum(s.get("usd", 0) for s in supplied if "GM_" in s.get("symbol", "")) > 1.0
116
118
  has_lb = any(
117
119
  sym in ("LB_AVAX_USDC", "LB_WAVAX_USDC", "JOE")
118
120
  or "TRADERJOE" in sym.upper()
@@ -120,13 +122,13 @@ def compute_health(defi_data: dict, max_mult: int = 10) -> dict:
120
122
  )
121
123
  has_aero = any("AERO" in sym.upper() or "CL_POSITION" in sym.upper() for sym in symbols)
122
124
 
123
- if max_debt > 0 and debt_usd >= 0:
124
- health_pct = (max_debt - min(debt_usd, max_debt)) / max_debt * 100
125
- health_pct = max(0.0, min(100.0, health_pct))
125
+ if max_debt > 0.01 and debt_usd >= 0:
126
+ health_pct = max(0.0, 100.0 * (1.0 - round(debt_usd, 2) / max_debt))
126
127
  else:
127
128
  health_pct = 100.0
128
129
 
129
- delta_debt = (max_debt * 0.5) - debt_usd # center target = 50%
130
+ # Center target (50% health): target_debt = max_debt * 0.5
131
+ delta_debt = (max_debt * 0.5) - debt_usd
130
132
 
131
133
  return {
132
134
  "health_pct": round(health_pct, 1),
@@ -306,28 +308,32 @@ def run_tick(
306
308
  max_mult = TIER_MAX.get("basic", 5)
307
309
  tier = "basic"
308
310
  else:
309
- max_mult = TIER_MAX.get("premium", 10)
310
- tier = "premium"
311
+ max_mult = TIER_MAX.get("basic", 5)
312
+ tier = "basic"
311
313
  except Exception:
312
- max_mult = 10
313
- tier = "premium"
314
+ max_mult = 5
315
+ tier = "basic"
314
316
 
315
317
  # 3. Compute health
316
318
  health = compute_health(defi_data, max_mult)
317
319
  health["tier"] = tier
318
320
  if health.get("error") == "equity near zero":
319
- write_escalation(state_dir, "equity-near-zero", {
320
- "reason": "equity_near_zero",
321
- "equity": health["equity"],
322
- "debt": health["debt_usd"],
323
- "health_pct": health["health_pct"],
324
- "label": label,
325
- })
321
+ # Only escalate if there's actual debt — an empty unfunded wallet is not an emergency
322
+ if health.get("debt_usd", 0) and health["debt_usd"] > 0.5:
323
+ write_escalation(state_dir, "equity-near-zero", {
324
+ "reason": "equity_near_zero",
325
+ "equity": health["equity"],
326
+ "debt": health["debt_usd"],
327
+ "health_pct": health["health_pct"],
328
+ "label": label,
329
+ })
330
+ result["mode"] = "escalated"
331
+ else:
332
+ result["action"] = "none (unfunded account)"
326
333
  result.update(health)
327
- result["mode"] = "escalated"
328
334
  return result
329
335
 
330
- # 4. Load strategy
336
+ # 4. Load strategy (position/market/side are optional hints now — auto-detected from defi_data)
331
337
  strategy = load_strategy(strategy_path)
332
338
  mode = strategy.get("mode", "observer")
333
339
  health["mode"] = mode
@@ -390,7 +396,7 @@ def run_tick(
390
396
  pct = health["health_pct"]
391
397
  equity = health["equity"]
392
398
  debt = health["debt_usd"]
393
- raw_usdc = health["raw_usdc"]
399
+ raw_usdc = health.get("raw_usdc", 0)
394
400
 
395
401
  # ── Stop-loss: equity drawdown ──────────────────────────────
396
402
  if stop_loss_drawdown > 0:
@@ -513,32 +519,59 @@ def run_tick(
513
519
  result["error"] = f"borrow error: {e}"
514
520
  return result
515
521
 
516
- # Deploy into GMX (default position type)
517
- if position_type == "gmx":
518
- try:
519
- r = subprocess.run(
520
- [sys.executable, tool_path, "gmx-deposit",
521
- "--market", market, "--amount", f"{borrow_amt:.2f}",
522
- "--side", side, "--fee-buffer", "1.5", "--execute"],
523
- capture_output=True, text=True, timeout=120,
524
- )
525
- if r.returncode == 0:
526
- cooldown_file.write_text(str(int(time.time())))
527
- result["action"] = f"borrowed ${borrow_amt:.2f} + GMX deposit"
528
- else:
529
- # Partial state: borrowed but deposit failed
530
- result["warning"] = f"borrow ok but deposit failed: {r.stderr[:200]}"
531
- result["action"] = "partial (borrowed, deposit failed)"
532
- except Exception as e:
533
- result["error"] = f"gmx deposit error: {e}"
522
+ # Deploy into whatever positions are open (detected dynamically from defi_data)
523
+ has_gmx = health.get("has_gmx", False)
524
+ has_lb = health.get("has_lb", False)
525
+ has_aero = health.get("has_aero", False)
526
+ open_positions = []
527
+ if has_gmx: open_positions.append("gmx")
528
+ if has_lb: open_positions.append("lb")
529
+ if has_aero: open_positions.append("aero")
530
+
531
+ if not open_positions:
532
+ # No open positions — just borrow and leave as USDC (or deploy to default)
533
+ result["action"] = f"borrowed ${borrow_amt:.2f} (no positions to deploy into)"
534
+ cooldown_file.write_text(str(int(time.time())))
534
535
  else:
535
- result["action"] = f"escalate (unsupported position: {position_type})"
536
- write_escalation(state_dir, "unsupported-position", {
537
- "reason": "lever_unsupported_position",
538
- "position": position_type,
539
- "borrow_amt": borrow_amt,
540
- "label": label,
541
- })
536
+ # Split borrow amount proportionally across open positions
537
+ split_amt = borrow_amt / len(open_positions)
538
+ deployed_ok = 0
539
+ deployed_fail = 0
540
+
541
+ for pos_type in open_positions:
542
+ if pos_type == "gmx":
543
+ # Use market/side from strategy as hint, fall back to sensible defaults
544
+ mkt = strategy.get("market", "avax-usdc") if tool_path else "avax-usdc"
545
+ sd = strategy.get("side", "long") if tool_path else "long"
546
+ try:
547
+ r = subprocess.run(
548
+ [sys.executable, tool_path, "gmx-deposit",
549
+ "--market", mkt, "--amount", f"{split_amt:.2f}",
550
+ "--side", sd, "--fee-buffer", "1.5", "--execute"],
551
+ capture_output=True, text=True, timeout=120,
552
+ )
553
+ if r.returncode == 0:
554
+ deployed_ok += 1
555
+ else:
556
+ result["warning"] = f"gmx deposit failed: {r.stderr[:200]}"
557
+ deployed_fail += 1
558
+ except Exception as e:
559
+ result["error"] = f"gmx deposit error: {e}"
560
+ deployed_fail += 1
561
+
562
+ elif pos_type == "lb":
563
+ # LB deposits need pair + amount-x + amount-y (not a single amount),
564
+ # so just leave as USDC for now — manual deployment required.
565
+ result["action"] = f"lb-add needs pair + dual amounts — leaving ${split_amt:.2f} as USDC"
566
+
567
+ elif pos_type == "aero":
568
+ result["action"] = f"aero deposit not yet supported by tool — leaving ${split_amt:.2f} as USDC"
569
+
570
+ if deployed_ok > 0:
571
+ cooldown_file.write_text(str(int(time.time())))
572
+ result["action"] = f"borrowed ${borrow_amt:.2f}, deployed ${split_amt:.2f} to {deployed_ok} position(s)"
573
+ else:
574
+ result["warning"] = f"borrow ok but all deposits failed"
542
575
 
543
576
  return result
544
577
 
@@ -573,7 +606,7 @@ def cli():
573
606
  [sys.executable, tool_path, "defi", "--json"],
574
607
  capture_output=True, text=True, timeout=90,
575
608
  )
576
- tier = "premium" # default
609
+ tier = "basic" # default
577
610
  try:
578
611
  t = subprocess.run(
579
612
  [sys.executable, tool_path, "prime-tier"],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: primecli
3
- Version: 0.5.6
3
+ Version: 0.5.8
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.6"
7
+ version = "0.5.8"
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"
File without changes
File without changes
File without changes
File without changes
File without changes