opentradex 0.1.0
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/.env.example +8 -0
- package/CLAUDE.md +98 -0
- package/README.md +246 -0
- package/SOUL.md +79 -0
- package/SPEC.md +317 -0
- package/SUBMISSION.md +30 -0
- package/architecture.excalidraw +170 -0
- package/architecture.png +0 -0
- package/bin/opentradex.mjs +4 -0
- package/data/.gitkeep +0 -0
- package/data/strategy_notes.md +158 -0
- package/gossip/__init__.py +0 -0
- package/gossip/dashboard.py +150 -0
- package/gossip/db.py +358 -0
- package/gossip/kalshi.py +492 -0
- package/gossip/news.py +235 -0
- package/gossip/trader.py +646 -0
- package/main.py +287 -0
- package/package.json +47 -0
- package/requirements.txt +7 -0
- package/src/cli.mjs +124 -0
- package/src/index.mjs +420 -0
- package/web/AGENTS.md +5 -0
- package/web/CLAUDE.md +1 -0
- package/web/README.md +36 -0
- package/web/components.json +25 -0
- package/web/eslint.config.mjs +18 -0
- package/web/next.config.ts +7 -0
- package/web/package-lock.json +11626 -0
- package/web/package.json +37 -0
- package/web/postcss.config.mjs +7 -0
- package/web/public/file.svg +1 -0
- package/web/public/globe.svg +1 -0
- package/web/public/next.svg +1 -0
- package/web/public/vercel.svg +1 -0
- package/web/public/window.svg +1 -0
- package/web/src/app/api/agent/route.ts +77 -0
- package/web/src/app/api/agent/stream/route.ts +87 -0
- package/web/src/app/api/markets/route.ts +15 -0
- package/web/src/app/api/news/live/route.ts +77 -0
- package/web/src/app/api/news/reddit/route.ts +118 -0
- package/web/src/app/api/news/route.ts +10 -0
- package/web/src/app/api/news/tiktok/route.ts +115 -0
- package/web/src/app/api/news/truthsocial/route.ts +116 -0
- package/web/src/app/api/news/twitter/route.ts +186 -0
- package/web/src/app/api/portfolio/route.ts +50 -0
- package/web/src/app/api/prices/route.ts +18 -0
- package/web/src/app/api/trades/route.ts +10 -0
- package/web/src/app/favicon.ico +0 -0
- package/web/src/app/globals.css +170 -0
- package/web/src/app/layout.tsx +36 -0
- package/web/src/app/page.tsx +366 -0
- package/web/src/components/AgentLog.tsx +71 -0
- package/web/src/components/LiveStream.tsx +394 -0
- package/web/src/components/MarketScanner.tsx +111 -0
- package/web/src/components/NewsFeed.tsx +561 -0
- package/web/src/components/PortfolioStrip.tsx +139 -0
- package/web/src/components/PositionsPanel.tsx +219 -0
- package/web/src/components/TopBar.tsx +127 -0
- package/web/src/components/ui/badge.tsx +52 -0
- package/web/src/components/ui/button.tsx +60 -0
- package/web/src/components/ui/card.tsx +103 -0
- package/web/src/components/ui/scroll-area.tsx +55 -0
- package/web/src/components/ui/separator.tsx +25 -0
- package/web/src/components/ui/tabs.tsx +82 -0
- package/web/src/components/ui/tooltip.tsx +66 -0
- package/web/src/lib/db.ts +81 -0
- package/web/src/lib/types.ts +130 -0
- package/web/src/lib/utils.ts +6 -0
- package/web/tsconfig.json +34 -0
package/main.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Open Trademaxxxing - agent orchestrator.
|
|
3
|
+
|
|
4
|
+
Spawns Claude Code as a subprocess. The agent reads SOUL.md for personality,
|
|
5
|
+
uses tools directly (web search, Apify, Kalshi API), and persists state to SQLite.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python3 main.py # single research + trade cycle
|
|
9
|
+
python3 main.py --loop # continuous loop
|
|
10
|
+
python3 main.py --loop --interval 300 # 5 min interval
|
|
11
|
+
python3 main.py --rationale "I think tariffs will escalate next week"
|
|
12
|
+
python3 main.py --prompt "check my positions and update strategy notes"
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
import time
|
|
23
|
+
from datetime import datetime, timezone
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from dotenv import load_dotenv
|
|
27
|
+
|
|
28
|
+
load_dotenv(Path(__file__).resolve().parent / ".env")
|
|
29
|
+
|
|
30
|
+
PROJECT_DIR = Path(__file__).resolve().parent
|
|
31
|
+
DATA_DIR = PROJECT_DIR / "data"
|
|
32
|
+
SESSION_FILE = DATA_DIR / "session_id.txt"
|
|
33
|
+
|
|
34
|
+
# The agent prompt: agentic, not scripted. Agent uses its own tools.
|
|
35
|
+
CYCLE_PROMPT = """Read SOUL.md for your identity and strategy principles.
|
|
36
|
+
Read data/strategy_notes.md for lessons from past sessions.
|
|
37
|
+
Check data/user_rationales.json for any pending user theses to research.
|
|
38
|
+
|
|
39
|
+
Then run a full trading cycle:
|
|
40
|
+
|
|
41
|
+
1. AUTO-SETTLE resolved markets first:
|
|
42
|
+
`PYTHONPATH=. python3 gossip/trader.py check-settled`
|
|
43
|
+
This checks Kalshi for any markets that have resolved and auto-settles them, returning capital to bankroll.
|
|
44
|
+
|
|
45
|
+
2. Check portfolio and live prices:
|
|
46
|
+
`PYTHONPATH=. python3 gossip/trader.py portfolio`
|
|
47
|
+
`PYTHONPATH=. python3 gossip/trader.py prices`
|
|
48
|
+
Review each open position's unrealized P&L and current market price vs your entry.
|
|
49
|
+
|
|
50
|
+
3. POSITION REVIEW — for each open position:
|
|
51
|
+
- If current price moved significantly toward your thesis (e.g. YES at 95c+ when you entered at 82c), consider selling now to lock profit vs waiting for settlement.
|
|
52
|
+
- If thesis has weakened or news contradicts it, EXIT: `PYTHONPATH=. python3 gossip/trader.py exit TICKER --reasoning "..."`
|
|
53
|
+
- If thesis still holds and edge remains, HOLD.
|
|
54
|
+
- Use web search to verify — don't just check prices, check if the underlying event happened.
|
|
55
|
+
|
|
56
|
+
4. MARKET DISCOVERY — use targeted searches, not just broad scans:
|
|
57
|
+
- `PYTHONPATH=. python3 gossip/kalshi.py quick --limit 60` for broad overview
|
|
58
|
+
- `PYTHONPATH=. python3 gossip/kalshi.py search "specific topic"` for targeted lookups
|
|
59
|
+
- Focus on categories where news creates edge: Politics, Economics, Macro events
|
|
60
|
+
- Search for current events you already know about from news/web search
|
|
61
|
+
- Skip sports, entertainment, and illiquid markets (volume < 500, spread > 15c)
|
|
62
|
+
|
|
63
|
+
5. RESEARCH — pick 3-5 promising markets:
|
|
64
|
+
- Use web search to find relevant news and primary sources
|
|
65
|
+
- Use `PYTHONPATH=. python3 gossip/news.py --keywords "..."` for broader news scraping
|
|
66
|
+
- Estimate the true probability based on evidence
|
|
67
|
+
- Look for near-arbitrage: events that already happened but market hasn't caught up
|
|
68
|
+
|
|
69
|
+
6. TRADE if you find edge > 10pp with clear reasoning:
|
|
70
|
+
Before executing, answer in one line: "Evidence type: [hard/soft/speculation]. Weakest assumption: [X]."
|
|
71
|
+
If the evidence is speculation, PASS unless edge is overwhelming (>25pp).
|
|
72
|
+
If you can't name what would make you wrong, your thesis isn't specific enough — PASS.
|
|
73
|
+
`PYTHONPATH=. python3 gossip/trader.py trade TICKER --side yes/no --estimate 0.XX --confidence high/medium --reasoning "..."`
|
|
74
|
+
|
|
75
|
+
7. Update data/strategy_notes.md with what you learned this cycle.
|
|
76
|
+
|
|
77
|
+
EXECUTION DISCIPLINE:
|
|
78
|
+
- Be decisive. Research → conclude → act. Don't loop endlessly.
|
|
79
|
+
- For each market: reach a YES/NO/PASS decision within 2-3 tool calls.
|
|
80
|
+
- Evaluate 3-5 markets per cycle, trade the best 1-2. Don't try to cover everything.
|
|
81
|
+
- If you can't find edge after 5 minutes on a market, pass and move on.
|
|
82
|
+
- Write your conclusion even when you pass — future cycles benefit from it.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def build_rationale_prompt(rationale: str) -> str:
|
|
87
|
+
return f"""Read SOUL.md for your identity and strategy principles.
|
|
88
|
+
|
|
89
|
+
A user has submitted this thesis for you to research and potentially trade on:
|
|
90
|
+
|
|
91
|
+
USER THESIS: {rationale}
|
|
92
|
+
|
|
93
|
+
Your job:
|
|
94
|
+
1. Research this thesis thoroughly using web search and news scraping.
|
|
95
|
+
2. Find evidence for AND against.
|
|
96
|
+
3. Estimate the probability if there's a relevant Kalshi market.
|
|
97
|
+
4. If you find a market with edge based on this thesis, trade it.
|
|
98
|
+
5. If the thesis doesn't hold up, explain why and pass.
|
|
99
|
+
6. Update data/user_rationales.json with your findings.
|
|
100
|
+
7. Update data/strategy_notes.md if you learned something new.
|
|
101
|
+
|
|
102
|
+
Check portfolio first: `PYTHONPATH=. python3 gossip/trader.py portfolio`
|
|
103
|
+
Scan markets: `PYTHONPATH=. python3 gossip/kalshi.py scan` or `PYTHONPATH=. python3 gossip/kalshi.py search "relevant keywords"`
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
LIVE_LOG = DATA_DIR / "agent_live.jsonl"
|
|
108
|
+
LIVE_STATUS = DATA_DIR / "agent_status.json"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def write_status(status: str, **extra) -> None:
|
|
112
|
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
LIVE_STATUS.write_text(json.dumps({"status": status, "timestamp": _now(), **extra}))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def run_agent(prompt: str, timeout: int = 600) -> dict:
|
|
117
|
+
"""Spawn Claude Code as a subprocess. Stream output to live log file."""
|
|
118
|
+
cmd = [
|
|
119
|
+
"claude",
|
|
120
|
+
"--print", "-",
|
|
121
|
+
"--output-format", "stream-json",
|
|
122
|
+
"--verbose",
|
|
123
|
+
"--max-turns", "80",
|
|
124
|
+
"--dangerously-skip-permissions",
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
env = {k: v for k, v in os.environ.items() if k not in {
|
|
128
|
+
"CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT",
|
|
129
|
+
}}
|
|
130
|
+
env["PYTHONPATH"] = str(PROJECT_DIR)
|
|
131
|
+
|
|
132
|
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
133
|
+
# Clear live log for this cycle
|
|
134
|
+
LIVE_LOG.write_text("")
|
|
135
|
+
write_status("running")
|
|
136
|
+
|
|
137
|
+
start = time.time()
|
|
138
|
+
session_id = None
|
|
139
|
+
agent_output = ""
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
proc = subprocess.Popen(
|
|
143
|
+
cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
144
|
+
stderr=subprocess.PIPE, text=True, env=env, cwd=str(PROJECT_DIR),
|
|
145
|
+
)
|
|
146
|
+
# Send prompt and close stdin
|
|
147
|
+
proc.stdin.write(prompt)
|
|
148
|
+
proc.stdin.close()
|
|
149
|
+
|
|
150
|
+
# Stream stdout line by line to live log
|
|
151
|
+
with open(LIVE_LOG, "a") as logf:
|
|
152
|
+
for line in proc.stdout:
|
|
153
|
+
line = line.strip()
|
|
154
|
+
if not line:
|
|
155
|
+
continue
|
|
156
|
+
# Write raw line to live log for streaming
|
|
157
|
+
logf.write(line + "\n")
|
|
158
|
+
logf.flush()
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
msg = json.loads(line)
|
|
162
|
+
if msg.get("type") == "system" and "session_id" in msg:
|
|
163
|
+
session_id = msg["session_id"]
|
|
164
|
+
if msg.get("type") == "result":
|
|
165
|
+
agent_output = msg.get("result", "")
|
|
166
|
+
if msg.get("type") == "assistant" and msg.get("message"):
|
|
167
|
+
for block in msg["message"].get("content", []):
|
|
168
|
+
if isinstance(block, dict) and block.get("type") == "text":
|
|
169
|
+
text = block.get("text", "")
|
|
170
|
+
agent_output += text + "\n"
|
|
171
|
+
write_status("running", last_text=text[:200])
|
|
172
|
+
if msg.get("type") == "assistant" and msg.get("message"):
|
|
173
|
+
for block in msg["message"].get("content", []):
|
|
174
|
+
if isinstance(block, dict) and block.get("type") == "tool_use":
|
|
175
|
+
tool = block.get("name", "")
|
|
176
|
+
write_status("running", tool=tool)
|
|
177
|
+
except json.JSONDecodeError:
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
if time.time() - start > timeout:
|
|
181
|
+
proc.kill()
|
|
182
|
+
write_status("timeout")
|
|
183
|
+
return {"status": "timeout", "duration_s": timeout, "timestamp": _now()}
|
|
184
|
+
|
|
185
|
+
proc.wait()
|
|
186
|
+
|
|
187
|
+
except Exception as e:
|
|
188
|
+
write_status("error", error=str(e))
|
|
189
|
+
return {"status": "error", "duration_s": round(time.time() - start, 1), "timestamp": _now(), "error": str(e)}
|
|
190
|
+
|
|
191
|
+
duration = round(time.time() - start, 1)
|
|
192
|
+
|
|
193
|
+
cycle_result = {
|
|
194
|
+
"timestamp": _now(),
|
|
195
|
+
"status": "ok" if proc.returncode == 0 else "error",
|
|
196
|
+
"duration_s": duration,
|
|
197
|
+
"session_id": session_id,
|
|
198
|
+
"output": agent_output[:2000] if agent_output else "",
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
write_status("idle", last_cycle=duration)
|
|
202
|
+
|
|
203
|
+
# Log to DB
|
|
204
|
+
try:
|
|
205
|
+
from gossip.db import GossipDB
|
|
206
|
+
db = GossipDB()
|
|
207
|
+
db.log_cycle(
|
|
208
|
+
session_id=session_id or "",
|
|
209
|
+
duration_s=duration,
|
|
210
|
+
status=cycle_result["status"],
|
|
211
|
+
output_summary=agent_output[:1000] if agent_output else "",
|
|
212
|
+
)
|
|
213
|
+
except Exception:
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
return cycle_result
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def submit_rationale(thesis: str) -> None:
|
|
220
|
+
"""Add a user rationale to the queue."""
|
|
221
|
+
rationale_file = DATA_DIR / "user_rationales.json"
|
|
222
|
+
data = {"rationales": []}
|
|
223
|
+
if rationale_file.exists():
|
|
224
|
+
try:
|
|
225
|
+
data = json.loads(rationale_file.read_text())
|
|
226
|
+
except Exception:
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
data["rationales"].append({
|
|
230
|
+
"id": len(data["rationales"]) + 1,
|
|
231
|
+
"timestamp": _now(),
|
|
232
|
+
"thesis": thesis,
|
|
233
|
+
"status": "pending",
|
|
234
|
+
"agent_response": None,
|
|
235
|
+
})
|
|
236
|
+
rationale_file.write_text(json.dumps(data, indent=2))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _now() -> str:
|
|
240
|
+
return datetime.now(timezone.utc).isoformat()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def main():
|
|
244
|
+
parser = argparse.ArgumentParser(description="Open Trademaxxxing agent")
|
|
245
|
+
parser.add_argument("--loop", action="store_true", help="Run continuously")
|
|
246
|
+
parser.add_argument("--interval", type=int, default=None, help="Cycle interval in seconds")
|
|
247
|
+
parser.add_argument("--prompt", type=str, default=None, help="Custom prompt")
|
|
248
|
+
parser.add_argument("--rationale", type=str, default=None, help="Submit a trading thesis")
|
|
249
|
+
parser.add_argument("--timeout", type=int, default=1200, help="Agent timeout per cycle (default 20min)")
|
|
250
|
+
parser.add_argument("--dry-run", action="store_true")
|
|
251
|
+
|
|
252
|
+
args = parser.parse_args()
|
|
253
|
+
interval = args.interval or int(os.getenv("CYCLE_INTERVAL", "900"))
|
|
254
|
+
|
|
255
|
+
if args.rationale:
|
|
256
|
+
submit_rationale(args.rationale)
|
|
257
|
+
prompt = build_rationale_prompt(args.rationale)
|
|
258
|
+
else:
|
|
259
|
+
prompt = args.prompt or CYCLE_PROMPT
|
|
260
|
+
|
|
261
|
+
if args.dry_run:
|
|
262
|
+
print(prompt)
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
print(f"[Open Trademaxxxing] Starting agent", file=sys.stderr)
|
|
266
|
+
print(f" Mode: {'loop (' + str(interval) + 's)' if args.loop else 'single cycle'}", file=sys.stderr)
|
|
267
|
+
|
|
268
|
+
while True:
|
|
269
|
+
ts = datetime.now(timezone.utc).strftime('%H:%M:%S')
|
|
270
|
+
print(f"\n[{ts}] Starting cycle...", file=sys.stderr)
|
|
271
|
+
|
|
272
|
+
result = run_agent(prompt, timeout=args.timeout)
|
|
273
|
+
|
|
274
|
+
print(f"[{datetime.now(timezone.utc).strftime('%H:%M:%S')}] Done: {result['status']} ({result['duration_s']}s)", file=sys.stderr)
|
|
275
|
+
if result.get("output"):
|
|
276
|
+
print(result["output"][:500], file=sys.stderr)
|
|
277
|
+
|
|
278
|
+
if not args.loop:
|
|
279
|
+
print(json.dumps(result, indent=2))
|
|
280
|
+
break
|
|
281
|
+
|
|
282
|
+
print(f" Next cycle in {interval}s...", file=sys.stderr)
|
|
283
|
+
time.sleep(interval)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
if __name__ == "__main__":
|
|
287
|
+
main()
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opentradex",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenTradex CLI and onboarding flow for Open Trademaxxxing.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"opentradex": "./bin/opentradex.mjs"
|
|
8
|
+
},
|
|
9
|
+
"main": "./src/index.mjs",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.mjs"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin/",
|
|
15
|
+
"src/",
|
|
16
|
+
".env.example",
|
|
17
|
+
"README.md",
|
|
18
|
+
"CLAUDE.md",
|
|
19
|
+
"SOUL.md",
|
|
20
|
+
"SPEC.md",
|
|
21
|
+
"SUBMISSION.md",
|
|
22
|
+
"main.py",
|
|
23
|
+
"requirements.txt",
|
|
24
|
+
"gossip/",
|
|
25
|
+
"data/.gitkeep",
|
|
26
|
+
"data/strategy_notes.md",
|
|
27
|
+
"web/",
|
|
28
|
+
"architecture.excalidraw",
|
|
29
|
+
"architecture.png"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"opentradex": "node ./bin/opentradex.mjs",
|
|
33
|
+
"test:cli": "node ./bin/opentradex.mjs --help && node ./bin/opentradex.mjs doctor --workspace ."
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"kalshi",
|
|
37
|
+
"prediction-markets",
|
|
38
|
+
"trading",
|
|
39
|
+
"agent",
|
|
40
|
+
"claude-code",
|
|
41
|
+
"opentradex"
|
|
42
|
+
],
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=22.0.0"
|
|
45
|
+
},
|
|
46
|
+
"license": "UNLICENSED"
|
|
47
|
+
}
|
package/requirements.txt
ADDED
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import {
|
|
3
|
+
collectDoctor,
|
|
4
|
+
formatDoctorReport,
|
|
5
|
+
onboard,
|
|
6
|
+
resolveWorkspace,
|
|
7
|
+
runDashboard,
|
|
8
|
+
runSingleCycle,
|
|
9
|
+
runTradingLoop
|
|
10
|
+
} from "./index.mjs";
|
|
11
|
+
|
|
12
|
+
export async function runCli(argv = process.argv.slice(2)) {
|
|
13
|
+
const [command = "help", ...rest] = argv;
|
|
14
|
+
const options = parseArgs(rest);
|
|
15
|
+
|
|
16
|
+
switch (command) {
|
|
17
|
+
case "help":
|
|
18
|
+
case "--help":
|
|
19
|
+
case "-h":
|
|
20
|
+
printHelp();
|
|
21
|
+
return;
|
|
22
|
+
case "onboard": {
|
|
23
|
+
const result = await onboard(options);
|
|
24
|
+
printLines([
|
|
25
|
+
"OpenTradex is ready.",
|
|
26
|
+
`workspace: ${result.workspace}`,
|
|
27
|
+
`mode: ${result.mode}`,
|
|
28
|
+
"",
|
|
29
|
+
"Next commands:",
|
|
30
|
+
" opentradex doctor",
|
|
31
|
+
" opentradex start",
|
|
32
|
+
" opentradex web"
|
|
33
|
+
]);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
case "doctor": {
|
|
37
|
+
const report = collectDoctor(options.workspace);
|
|
38
|
+
console.log(formatDoctorReport(report));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
case "start":
|
|
42
|
+
await runTradingLoop(options);
|
|
43
|
+
return;
|
|
44
|
+
case "cycle":
|
|
45
|
+
await runSingleCycle(options);
|
|
46
|
+
return;
|
|
47
|
+
case "web":
|
|
48
|
+
await runDashboard(options);
|
|
49
|
+
return;
|
|
50
|
+
case "where":
|
|
51
|
+
console.log(resolveWorkspace(options.workspace));
|
|
52
|
+
return;
|
|
53
|
+
default:
|
|
54
|
+
throw new Error(`Unknown opentradex command: ${command}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseArgs(argv) {
|
|
59
|
+
const options = {};
|
|
60
|
+
|
|
61
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
62
|
+
const token = argv[index];
|
|
63
|
+
if (!token.startsWith("--")) {
|
|
64
|
+
if (!options._) {
|
|
65
|
+
options._ = [];
|
|
66
|
+
}
|
|
67
|
+
options._.push(token);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const normalized = token.slice(2);
|
|
72
|
+
const [key, explicitValue] = normalized.split("=", 2);
|
|
73
|
+
if (explicitValue !== undefined) {
|
|
74
|
+
options[toCamel(key)] = explicitValue;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (["skip-install", "yes", "install", "force", "paper", "live"].includes(key)) {
|
|
79
|
+
options[toCamel(key)] = true;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const next = argv[index + 1];
|
|
84
|
+
if (!next || next.startsWith("--")) {
|
|
85
|
+
options[toCamel(key)] = true;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
options[toCamel(key)] = next;
|
|
90
|
+
index += 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return options;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function toCamel(value) {
|
|
97
|
+
return value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function printHelp() {
|
|
101
|
+
printLines([
|
|
102
|
+
"opentradex",
|
|
103
|
+
"",
|
|
104
|
+
"OpenClaw-style onboarding and runtime wrapper for Open Trademaxxxing.",
|
|
105
|
+
"",
|
|
106
|
+
"Commands:",
|
|
107
|
+
" opentradex onboard [--workspace <dir>] [--paper|--live] [--skip-install]",
|
|
108
|
+
" opentradex doctor [--workspace <dir>]",
|
|
109
|
+
" opentradex start [--workspace <dir>] [--interval <seconds>] [--rationale <text>]",
|
|
110
|
+
" opentradex cycle [--workspace <dir>] [--rationale <text>]",
|
|
111
|
+
" opentradex web [--workspace <dir>] [--install]",
|
|
112
|
+
" opentradex where [--workspace <dir>]",
|
|
113
|
+
"",
|
|
114
|
+
"Examples:",
|
|
115
|
+
" npm install -g .",
|
|
116
|
+
" opentradex onboard",
|
|
117
|
+
" opentradex doctor",
|
|
118
|
+
" opentradex cycle --rationale \"Tariffs will escalate next week\""
|
|
119
|
+
]);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function printLines(lines) {
|
|
123
|
+
console.log(lines.join("\n"));
|
|
124
|
+
}
|