o2-cli 0.1.0__py3-none-any.whl
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.
- o2_cli/__init__.py +30 -0
- o2_cli/__main__.py +6 -0
- o2_cli/cli.py +94 -0
- o2_cli/client.py +307 -0
- o2_cli/commands/__init__.py +1 -0
- o2_cli/commands/_helpers.py +65 -0
- o2_cli/commands/account.py +46 -0
- o2_cli/commands/admin.py +147 -0
- o2_cli/commands/auth.py +140 -0
- o2_cli/commands/balance.py +64 -0
- o2_cli/commands/deposits.py +89 -0
- o2_cli/commands/fees.py +73 -0
- o2_cli/commands/markets.py +129 -0
- o2_cli/commands/mm.py +182 -0
- o2_cli/commands/notifications.py +136 -0
- o2_cli/commands/orders.py +331 -0
- o2_cli/commands/positions.py +158 -0
- o2_cli/commands/settings.py +129 -0
- o2_cli/commands/setup_cmd.py +78 -0
- o2_cli/commands/trades.py +86 -0
- o2_cli/commands/withdrawals.py +175 -0
- o2_cli/config.py +87 -0
- o2_cli/exceptions.py +31 -0
- o2_cli/output.py +224 -0
- o2_cli/setup.py +561 -0
- o2_cli-0.1.0.dist-info/METADATA +141 -0
- o2_cli-0.1.0.dist-info/RECORD +31 -0
- o2_cli-0.1.0.dist-info/WHEEL +5 -0
- o2_cli-0.1.0.dist-info/entry_points.txt +2 -0
- o2_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- o2_cli-0.1.0.dist-info/top_level.txt +1 -0
o2_cli/setup.py
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
"""O2 CLI Setup Wizard - Install skill files for different vibe coding tools.
|
|
2
|
+
|
|
3
|
+
Supports:
|
|
4
|
+
- Claude Code → ~/.claude/skills/ or .claude/skills/
|
|
5
|
+
- Cursor → .cursor/rules/
|
|
6
|
+
- Codex (OpenAI) → AGENTS.md
|
|
7
|
+
- Windsurf → .windsurfrules
|
|
8
|
+
- Cline → .clinerules
|
|
9
|
+
- Trae → .trae/rules/
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
o2 setup # Interactive wizard
|
|
13
|
+
o2 setup --tool claude-code --scope global # Non-interactive
|
|
14
|
+
o2 setup --update # Re-install all configured tools
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
from rich.console import Console
|
|
24
|
+
from rich.panel import Panel
|
|
25
|
+
from rich.table import Table
|
|
26
|
+
|
|
27
|
+
from o2_cli import __version__
|
|
28
|
+
|
|
29
|
+
console = Console()
|
|
30
|
+
|
|
31
|
+
# ── Skill content (single source of truth) ──────────────────────────────────
|
|
32
|
+
|
|
33
|
+
SKILL_CONTENT = """\
|
|
34
|
+
# O2 CLI Trading Tool
|
|
35
|
+
|
|
36
|
+
**Category**: Trading
|
|
37
|
+
**Severity**: Normal
|
|
38
|
+
**Auto-trigger**: Yes
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## When to Use
|
|
43
|
+
|
|
44
|
+
当用户提到以下操作时,使用 O2 CLI 而不是直接调用 API:
|
|
45
|
+
|
|
46
|
+
- 查询 O2 余额、订单、持仓、市场数据
|
|
47
|
+
- 创建/取消/修改订单
|
|
48
|
+
- 充值/提币操作
|
|
49
|
+
- 账户设置(杠杆、保证金模式)
|
|
50
|
+
- 查看 K 线、订单簿、手续费
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Quick Reference
|
|
55
|
+
|
|
56
|
+
**安装**(首次使用前检查):
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
which o2 || pip install o2-cli
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 核心规则
|
|
63
|
+
|
|
64
|
+
1. **`--json` 必须放在命令前面**: `o2 --json balance show` / `o2 balance show --json` ❌
|
|
65
|
+
2. **公开命令无需登录**: `markets list`, `fees rates`
|
|
66
|
+
3. **其他命令需要先登录**: `o2 auth test-login`
|
|
67
|
+
4. **退出码**: 0=成功, 1=失败(错误到 stderr,数据到 stdout)
|
|
68
|
+
|
|
69
|
+
### 常用命令
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# 认证
|
|
73
|
+
o2 auth test-login # 登录(token 自动保存)
|
|
74
|
+
|
|
75
|
+
# 市场数据(公开)
|
|
76
|
+
o2 --json markets list # 市场列表
|
|
77
|
+
o2 --json markets orderbook --market-id 1 # BTC 订单簿
|
|
78
|
+
o2 --json markets candles --market-id 1 --interval 1h
|
|
79
|
+
|
|
80
|
+
# 余额
|
|
81
|
+
o2 --json balance show # 余额(现金+赠金)
|
|
82
|
+
o2 --json balance history
|
|
83
|
+
|
|
84
|
+
# 订单
|
|
85
|
+
o2 --json orders create -m 1 -s long -t market -a 0.001 # 市价做多
|
|
86
|
+
o2 --json orders create -m 1 -s short -t limit -a 0.001 -p 85000 # 限价做空
|
|
87
|
+
o2 --json orders list --status open
|
|
88
|
+
o2 --json orders cancel --order-id <ID>
|
|
89
|
+
o2 --json orders cancel-all
|
|
90
|
+
|
|
91
|
+
# 持仓
|
|
92
|
+
o2 --json positions list
|
|
93
|
+
o2 --json positions close --position-id <ID>
|
|
94
|
+
o2 --json positions risk --market-id 1
|
|
95
|
+
|
|
96
|
+
# 设置
|
|
97
|
+
o2 --json settings leverage --market-id 1 --leverage 10
|
|
98
|
+
o2 --json settings margin-mode --mode cross
|
|
99
|
+
|
|
100
|
+
# 充提
|
|
101
|
+
o2 --json deposits address --chain base
|
|
102
|
+
o2 --json withdrawals create --amount 500 --address 0x... --chain ethereum
|
|
103
|
+
|
|
104
|
+
# 其他
|
|
105
|
+
o2 --json trades list
|
|
106
|
+
o2 --json fees rates
|
|
107
|
+
o2 --json account overview
|
|
108
|
+
o2 --json notifications list
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 参数说明
|
|
112
|
+
|
|
113
|
+
| 参数 | 含义 | 值 |
|
|
114
|
+
|------|------|-----|
|
|
115
|
+
| `--market-id` / `-m` | 市场 | 1=BTC, 2=ETH |
|
|
116
|
+
| `--side` / `-s` | 方向 | `long` / `short` |
|
|
117
|
+
| `--order-type` / `-t` | 类型 | `market` / `limit` |
|
|
118
|
+
| `--base-amount` / `-a` | 数量 | 实际数量如 0.001 |
|
|
119
|
+
| `--price` / `-p` | 价格 | USDC |
|
|
120
|
+
| `--leverage` / `-l` | 杠杆 | 1-50 |
|
|
121
|
+
|
|
122
|
+
### 故障排查
|
|
123
|
+
|
|
124
|
+
- `Cannot connect` → 启动 O2 Backend
|
|
125
|
+
- `Not authenticated` → `o2 auth test-login`
|
|
126
|
+
- `o2: command not found` → `pip install o2-cli`
|
|
127
|
+
|
|
128
|
+
### 更新
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
o2 setup --update # 更新所有已安装工具的 skill 文件
|
|
132
|
+
```
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
# Cursor MDC 格式(带 frontmatter)
|
|
136
|
+
CURSOR_MDC_CONTENT = """\
|
|
137
|
+
---
|
|
138
|
+
description: O2 CLI Trading Tool - 使用命令行操作 O2 交易平台
|
|
139
|
+
globs:
|
|
140
|
+
alwaysApply: true
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
""" + SKILL_CONTENT
|
|
144
|
+
|
|
145
|
+
# Codex AGENTS.md 区块
|
|
146
|
+
CODEX_SECTION = """
|
|
147
|
+
|
|
148
|
+
## O2 CLI Trading Tool
|
|
149
|
+
|
|
150
|
+
使用 O2 CLI 操作 O2 交易平台。安装: `pip install o2-cli`
|
|
151
|
+
|
|
152
|
+
### 核心规则
|
|
153
|
+
1. `--json` 放在命令前面: `o2 --json balance show`
|
|
154
|
+
2. 公开命令无需登录: `markets list`, `fees rates`
|
|
155
|
+
3. 需要登录的命令先执行: `o2 auth test-login`
|
|
156
|
+
|
|
157
|
+
### 常用命令
|
|
158
|
+
```bash
|
|
159
|
+
o2 auth test-login # 登录
|
|
160
|
+
o2 --json markets list # 市场列表
|
|
161
|
+
o2 --json balance show # 余额
|
|
162
|
+
o2 --json orders create -m 1 -s long -t market -a 0.001 # 市价做多
|
|
163
|
+
o2 --json orders list --status open # 查询订单
|
|
164
|
+
o2 --json positions list # 持仓
|
|
165
|
+
o2 --json settings leverage --market-id 1 --leverage 10 # 杠杆
|
|
166
|
+
o2 --json deposits address --chain base # 充值地址
|
|
167
|
+
o2 --json account overview # 账户总览
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
详细文档: `o2 setup --show-skill`
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
# Windsurf / Cline 规则格式(追加到文件末尾)
|
|
174
|
+
RULES_SECTION = """
|
|
175
|
+
|
|
176
|
+
# O2 CLI Trading Tool
|
|
177
|
+
|
|
178
|
+
使用 O2 CLI 操作 O2 交易平台。安装: `pip install o2-cli`
|
|
179
|
+
|
|
180
|
+
## 规则
|
|
181
|
+
1. `--json` 放命令前面: `o2 --json balance show` (不是 `o2 balance show --json`)
|
|
182
|
+
2. 公开命令无需登录: `markets list`, `fees rates`
|
|
183
|
+
3. 需要登录先执行: `o2 auth test-login`
|
|
184
|
+
|
|
185
|
+
## 常用命令
|
|
186
|
+
- `o2 --json markets list` - 市场列表
|
|
187
|
+
- `o2 --json balance show` - 余额
|
|
188
|
+
- `o2 --json orders create -m 1 -s long -t market -a 0.001` - 市价做多
|
|
189
|
+
- `o2 --json orders list --status open` - 查询订单
|
|
190
|
+
- `o2 --json positions list` - 持仓
|
|
191
|
+
- `o2 --json account overview` - 账户总览
|
|
192
|
+
|
|
193
|
+
详细文档: `o2 setup --show-skill`
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ── Tool definitions ────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
class ToolConfig:
|
|
200
|
+
"""Defines how to install skill for a specific vibe coding tool."""
|
|
201
|
+
|
|
202
|
+
def __init__(
|
|
203
|
+
self,
|
|
204
|
+
name: str,
|
|
205
|
+
display_name: str,
|
|
206
|
+
install_scopes: list[str], # ["global", "project"] or ["project"]
|
|
207
|
+
):
|
|
208
|
+
self.name = name
|
|
209
|
+
self.display_name = display_name
|
|
210
|
+
self.install_scopes = install_scopes
|
|
211
|
+
|
|
212
|
+
def get_install_path(self, scope: str, project_dir: Path) -> Path:
|
|
213
|
+
raise NotImplementedError
|
|
214
|
+
|
|
215
|
+
def get_content(self) -> str:
|
|
216
|
+
raise NotImplementedError
|
|
217
|
+
|
|
218
|
+
def is_installed(self, scope: str, project_dir: Path) -> bool:
|
|
219
|
+
return self.get_install_path(scope, project_dir).exists()
|
|
220
|
+
|
|
221
|
+
def install(self, scope: str, project_dir: Path) -> Path:
|
|
222
|
+
path = self.get_install_path(scope, project_dir)
|
|
223
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
224
|
+
content = self.get_content()
|
|
225
|
+
|
|
226
|
+
if self.name == "codex":
|
|
227
|
+
# Append to AGENTS.md, don't overwrite
|
|
228
|
+
if path.exists():
|
|
229
|
+
existing = path.read_text(encoding="utf-8")
|
|
230
|
+
if "O2 CLI Trading Tool" in existing:
|
|
231
|
+
# Replace existing section
|
|
232
|
+
import re
|
|
233
|
+
pattern = r"\n## O2 CLI Trading Tool\n.*"
|
|
234
|
+
existing = re.sub(pattern, "", existing, flags=re.DOTALL)
|
|
235
|
+
path.write_text(existing.rstrip() + "\n" + content, encoding="utf-8")
|
|
236
|
+
else:
|
|
237
|
+
path.write_text(content.lstrip(), encoding="utf-8")
|
|
238
|
+
elif self.name in ("windsurf", "cline"):
|
|
239
|
+
# Append to rules file
|
|
240
|
+
if path.exists():
|
|
241
|
+
existing = path.read_text(encoding="utf-8")
|
|
242
|
+
if "O2 CLI Trading Tool" in existing:
|
|
243
|
+
import re
|
|
244
|
+
pattern = r"\n# O2 CLI Trading Tool\n.*"
|
|
245
|
+
existing = re.sub(pattern, "", existing, flags=re.DOTALL)
|
|
246
|
+
path.write_text(existing.rstrip() + "\n" + content, encoding="utf-8")
|
|
247
|
+
else:
|
|
248
|
+
path.write_text(content.lstrip(), encoding="utf-8")
|
|
249
|
+
else:
|
|
250
|
+
path.write_text(content, encoding="utf-8")
|
|
251
|
+
|
|
252
|
+
return path
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class ClaudeCodeTool(ToolConfig):
|
|
256
|
+
def __init__(self):
|
|
257
|
+
super().__init__("claude-code", "Claude Code", ["global", "project"])
|
|
258
|
+
|
|
259
|
+
def get_install_path(self, scope: str, project_dir: Path) -> Path:
|
|
260
|
+
if scope == "global":
|
|
261
|
+
return Path.home() / ".claude" / "skills" / "o2-cli" / "SKILL.md"
|
|
262
|
+
return project_dir / ".claude" / "skills" / "o2-cli" / "SKILL.md"
|
|
263
|
+
|
|
264
|
+
def get_content(self) -> str:
|
|
265
|
+
return SKILL_CONTENT
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class CursorTool(ToolConfig):
|
|
269
|
+
def __init__(self):
|
|
270
|
+
super().__init__("cursor", "Cursor", ["project"])
|
|
271
|
+
|
|
272
|
+
def get_install_path(self, scope: str, project_dir: Path) -> Path:
|
|
273
|
+
return project_dir / ".cursor" / "rules" / "o2-cli.mdc"
|
|
274
|
+
|
|
275
|
+
def get_content(self) -> str:
|
|
276
|
+
return CURSOR_MDC_CONTENT
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class CodexTool(ToolConfig):
|
|
280
|
+
def __init__(self):
|
|
281
|
+
super().__init__("codex", "Codex (OpenAI)", ["project"])
|
|
282
|
+
|
|
283
|
+
def get_install_path(self, scope: str, project_dir: Path) -> Path:
|
|
284
|
+
return project_dir / "AGENTS.md"
|
|
285
|
+
|
|
286
|
+
def get_content(self) -> str:
|
|
287
|
+
return CODEX_SECTION
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class WindsurfTool(ToolConfig):
|
|
291
|
+
def __init__(self):
|
|
292
|
+
super().__init__("windsurf", "Windsurf", ["project"])
|
|
293
|
+
|
|
294
|
+
def get_install_path(self, scope: str, project_dir: Path) -> Path:
|
|
295
|
+
return project_dir / ".windsurfrules"
|
|
296
|
+
|
|
297
|
+
def get_content(self) -> str:
|
|
298
|
+
return RULES_SECTION
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class ClineTool(ToolConfig):
|
|
302
|
+
def __init__(self):
|
|
303
|
+
super().__init__("cline", "Cline (VS Code)", ["project"])
|
|
304
|
+
|
|
305
|
+
def get_install_path(self, scope: str, project_dir: Path) -> Path:
|
|
306
|
+
return project_dir / ".clinerules"
|
|
307
|
+
|
|
308
|
+
def get_content(self) -> str:
|
|
309
|
+
return RULES_SECTION
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class TraeTool(ToolConfig):
|
|
313
|
+
def __init__(self):
|
|
314
|
+
super().__init__("trae", "Trae", ["project"])
|
|
315
|
+
|
|
316
|
+
def get_install_path(self, scope: str, project_dir: Path) -> Path:
|
|
317
|
+
return project_dir / ".trae" / "rules" / "o2-cli.md"
|
|
318
|
+
|
|
319
|
+
def get_content(self) -> str:
|
|
320
|
+
return SKILL_CONTENT
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
ALL_TOOLS: list[ToolConfig] = [
|
|
324
|
+
ClaudeCodeTool(),
|
|
325
|
+
CursorTool(),
|
|
326
|
+
CodexTool(),
|
|
327
|
+
WindsurfTool(),
|
|
328
|
+
ClineTool(),
|
|
329
|
+
TraeTool(),
|
|
330
|
+
]
|
|
331
|
+
|
|
332
|
+
TOOL_BY_NAME: dict[str, ToolConfig] = {t.name: t for t in ALL_TOOLS}
|
|
333
|
+
|
|
334
|
+
# ── Install state tracking ──────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
STATE_FILE = Path.home() / ".o2" / "setup-state.json"
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def load_state() -> dict:
|
|
340
|
+
"""Load setup state (which tools are installed, version, etc.)."""
|
|
341
|
+
if STATE_FILE.exists():
|
|
342
|
+
try:
|
|
343
|
+
return json.loads(STATE_FILE.read_text(encoding="utf-8"))
|
|
344
|
+
except (json.JSONDecodeError, OSError):
|
|
345
|
+
pass
|
|
346
|
+
return {"installed_tools": [], "version": None}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def save_state(state: dict) -> None:
|
|
350
|
+
"""Save setup state."""
|
|
351
|
+
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
352
|
+
state["version"] = __version__
|
|
353
|
+
STATE_FILE.write_text(json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# ── CLI check ───────────────────────────────────────────────────────────────
|
|
357
|
+
|
|
358
|
+
def is_cli_installed() -> bool:
|
|
359
|
+
"""Check if o2 CLI is in PATH."""
|
|
360
|
+
try:
|
|
361
|
+
result = subprocess.run(
|
|
362
|
+
["which", "o2"], capture_output=True, text=True, timeout=5
|
|
363
|
+
)
|
|
364
|
+
return result.returncode == 0
|
|
365
|
+
except Exception:
|
|
366
|
+
return False
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# ── Interactive setup ───────────────────────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
def interactive_setup(project_dir: Path | None = None) -> None:
|
|
372
|
+
"""Run interactive setup wizard."""
|
|
373
|
+
if project_dir is None:
|
|
374
|
+
project_dir = Path.cwd()
|
|
375
|
+
|
|
376
|
+
console.print(Panel(
|
|
377
|
+
"[bold]O2 CLI Setup Wizard[/bold]\n\n"
|
|
378
|
+
"Select your vibe coding tool(s) to install the skill file.\n"
|
|
379
|
+
"Run again anytime to add more tools or switch tools.",
|
|
380
|
+
title="o2 setup",
|
|
381
|
+
border_style="cyan",
|
|
382
|
+
))
|
|
383
|
+
|
|
384
|
+
# Step 1: Check CLI
|
|
385
|
+
if is_cli_installed():
|
|
386
|
+
console.print("[green]✓[/green] o2 CLI is installed")
|
|
387
|
+
else:
|
|
388
|
+
console.print("[yellow]⚠[/yellow] o2 CLI not in PATH")
|
|
389
|
+
console.print(" Install with: [bold]pip install -e .[/bold]")
|
|
390
|
+
if not confirm("Continue setup anyway?"):
|
|
391
|
+
return
|
|
392
|
+
|
|
393
|
+
# Step 2: Show installed tools
|
|
394
|
+
state = load_state()
|
|
395
|
+
if state.get("installed_tools"):
|
|
396
|
+
console.print(f"\n[dim]Previously installed: {', '.join(state['installed_tools'])}[/dim]")
|
|
397
|
+
|
|
398
|
+
# Step 3: Select tools
|
|
399
|
+
console.print("\n[bold]Available tools:[/bold]")
|
|
400
|
+
for i, tool in enumerate(ALL_TOOLS, 1):
|
|
401
|
+
scopes = "/".join(tool.install_scopes)
|
|
402
|
+
console.print(f" {i}. {tool.display_name} ({scopes})")
|
|
403
|
+
console.print(f" 0. [bold]All tools[/bold]")
|
|
404
|
+
|
|
405
|
+
choice = input("\nSelect tool number (or comma-separated, e.g. 1,3): ").strip()
|
|
406
|
+
|
|
407
|
+
if choice == "0":
|
|
408
|
+
selected_tools = ALL_TOOLS
|
|
409
|
+
else:
|
|
410
|
+
selected_tools = []
|
|
411
|
+
for num in choice.split(","):
|
|
412
|
+
try:
|
|
413
|
+
idx = int(num.strip()) - 1
|
|
414
|
+
if 0 <= idx < len(ALL_TOOLS):
|
|
415
|
+
selected_tools.append(ALL_TOOLS[idx])
|
|
416
|
+
except ValueError:
|
|
417
|
+
console.print(f"[red]Invalid: {num}[/red]")
|
|
418
|
+
|
|
419
|
+
if not selected_tools:
|
|
420
|
+
console.print("[red]No tools selected.[/red]")
|
|
421
|
+
return
|
|
422
|
+
|
|
423
|
+
# Step 4: For each tool, choose scope and install
|
|
424
|
+
installed = []
|
|
425
|
+
for tool in selected_tools:
|
|
426
|
+
console.print(f"\n[bold cyan]→ {tool.display_name}[/bold cyan]")
|
|
427
|
+
|
|
428
|
+
# Choose scope
|
|
429
|
+
if len(tool.install_scopes) > 1:
|
|
430
|
+
scope = choose_scope(tool.install_scopes)
|
|
431
|
+
else:
|
|
432
|
+
scope = tool.install_scopes[0]
|
|
433
|
+
console.print(f" Scope: {scope}")
|
|
434
|
+
|
|
435
|
+
# Install
|
|
436
|
+
path = tool.install(scope, project_dir)
|
|
437
|
+
console.print(f" [green]✓[/green] Installed to: [dim]{path}[/dim]")
|
|
438
|
+
installed.append({"tool": tool.name, "scope": scope, "path": str(path)})
|
|
439
|
+
|
|
440
|
+
# Step 5: Save state
|
|
441
|
+
state["installed_tools"] = [item["tool"] for item in installed]
|
|
442
|
+
state["install_details"] = installed
|
|
443
|
+
save_state(state)
|
|
444
|
+
|
|
445
|
+
console.print(Panel(
|
|
446
|
+
f"[bold green]Setup complete![/bold green]\n\n"
|
|
447
|
+
f"Installed {len(installed)} tool(s):\n"
|
|
448
|
+
+ "\n".join(f" • {item['tool']} ({item['scope']}) → {item['path']}" for item in installed)
|
|
449
|
+
+ f"\n\nTo update skills later: [bold]o2 setup --update[/bold]"
|
|
450
|
+
+ f"\nTo change tools: [bold]o2 setup[/bold] (re-run)",
|
|
451
|
+
border_style="green",
|
|
452
|
+
))
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def choose_scope(scopes: list[str]) -> str:
|
|
456
|
+
"""Let user choose install scope."""
|
|
457
|
+
if len(scopes) == 1:
|
|
458
|
+
return scopes[0]
|
|
459
|
+
|
|
460
|
+
console.print(" Choose scope:")
|
|
461
|
+
for i, s in enumerate(scopes, 1):
|
|
462
|
+
desc = "system-wide (~/.claude/skills/)" if s == "global" else "project-level (./.claude/skills/)"
|
|
463
|
+
console.print(f" {i}. {s} ({desc})")
|
|
464
|
+
|
|
465
|
+
while True:
|
|
466
|
+
try:
|
|
467
|
+
choice = int(input(" Scope [1]: ").strip() or "1")
|
|
468
|
+
if 1 <= choice <= len(scopes):
|
|
469
|
+
return scopes[choice - 1]
|
|
470
|
+
except ValueError:
|
|
471
|
+
pass
|
|
472
|
+
console.print(" [red]Invalid choice[/red]")
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def confirm(message: str) -> bool:
|
|
476
|
+
return input(f"{message} [y/N]: ").strip().lower() in ("y", "yes")
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
# ── Non-interactive setup ───────────────────────────────────────────────────
|
|
480
|
+
|
|
481
|
+
def setup_tool(tool_name: str, scope: str = "project", project_dir: Path | None = None) -> None:
|
|
482
|
+
"""Non-interactive setup for CI/agent use."""
|
|
483
|
+
if project_dir is None:
|
|
484
|
+
project_dir = Path.cwd()
|
|
485
|
+
|
|
486
|
+
tool = TOOL_BY_NAME.get(tool_name)
|
|
487
|
+
if not tool:
|
|
488
|
+
console.print(f"[red]Unknown tool: {tool_name}[/red]")
|
|
489
|
+
console.print(f"Available: {', '.join(TOOL_BY_NAME.keys())}")
|
|
490
|
+
return
|
|
491
|
+
|
|
492
|
+
path = tool.install(scope, project_dir)
|
|
493
|
+
state = load_state()
|
|
494
|
+
if tool_name not in state.get("installed_tools", []):
|
|
495
|
+
state.setdefault("installed_tools", []).append(tool_name)
|
|
496
|
+
save_state(state)
|
|
497
|
+
console.print(f"[green]✓[/green] {tool.display_name} installed to: {path}")
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
# ── Update ──────────────────────────────────────────────────────────────────
|
|
501
|
+
|
|
502
|
+
def update_skills(project_dir: Path | None = None) -> None:
|
|
503
|
+
"""Re-install skill files for all previously configured tools."""
|
|
504
|
+
if project_dir is None:
|
|
505
|
+
project_dir = Path.cwd()
|
|
506
|
+
|
|
507
|
+
state = load_state()
|
|
508
|
+
installed = state.get("installed_tools", [])
|
|
509
|
+
|
|
510
|
+
if not installed:
|
|
511
|
+
console.print("[yellow]No tools configured. Run [bold]o2 setup[/bold] first.[/yellow]")
|
|
512
|
+
return
|
|
513
|
+
|
|
514
|
+
console.print(f"[bold]Updating skills for: {', '.join(installed)}[/bold]")
|
|
515
|
+
for tool_name in installed:
|
|
516
|
+
tool = TOOL_BY_NAME.get(tool_name)
|
|
517
|
+
if not tool:
|
|
518
|
+
continue
|
|
519
|
+
# Find the scope from install details
|
|
520
|
+
details = state.get("install_details", [])
|
|
521
|
+
scope = "project"
|
|
522
|
+
for d in details:
|
|
523
|
+
if d.get("tool") == tool_name:
|
|
524
|
+
scope = d.get("scope", "project")
|
|
525
|
+
break
|
|
526
|
+
path = tool.install(scope, project_dir)
|
|
527
|
+
console.print(f" [green]✓[/green] {tool.display_name} → {path}")
|
|
528
|
+
|
|
529
|
+
state["version"] = __version__
|
|
530
|
+
save_state(state)
|
|
531
|
+
console.print("[green]All skills updated![/green]")
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def show_skill() -> None:
|
|
535
|
+
"""Print the full skill content to stdout."""
|
|
536
|
+
print(SKILL_CONTENT)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def show_status() -> None:
|
|
540
|
+
"""Show current setup status."""
|
|
541
|
+
state = load_state()
|
|
542
|
+
installed = state.get("installed_tools", [])
|
|
543
|
+
version = state.get("version", "unknown")
|
|
544
|
+
|
|
545
|
+
table = Table(title="O2 CLI Setup Status")
|
|
546
|
+
table.add_column("Item", style="bold")
|
|
547
|
+
table.add_column("Value")
|
|
548
|
+
|
|
549
|
+
table.add_row("CLI Version", __version__)
|
|
550
|
+
table.add_row("Installed in PATH", "Yes" if is_cli_installed() else "No")
|
|
551
|
+
table.add_row("Skills Version", version)
|
|
552
|
+
table.add_row("Configured Tools", ", ".join(installed) if installed else "None")
|
|
553
|
+
|
|
554
|
+
console.print(table)
|
|
555
|
+
|
|
556
|
+
if installed:
|
|
557
|
+
console.print("\n[bold]Install details:[/bold]")
|
|
558
|
+
for d in state.get("install_details", []):
|
|
559
|
+
tool = TOOL_BY_NAME.get(d.get("tool", ""))
|
|
560
|
+
name = tool.display_name if tool else d["tool"]
|
|
561
|
+
console.print(f" • {name} ({d.get('scope', '?')}) → {d.get('path', '?')}")
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: o2-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for O2 DEX Trading Platform
|
|
5
|
+
Author: Dylan Wu
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: cli,trading,dex,lighter,o2
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: typer[all]>=0.9.0
|
|
20
|
+
Requires-Dist: rich>=13.0.0
|
|
21
|
+
Requires-Dist: httpx>=0.25.0
|
|
22
|
+
Requires-Dist: pyyaml>=6.0
|
|
23
|
+
Requires-Dist: prompt-toolkit>=3.0.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
27
|
+
Requires-Dist: respx>=0.20.0; extra == "dev"
|
|
28
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# O2 CLI
|
|
32
|
+
|
|
33
|
+
Command-line interface for [O2 DEX](https://github.com/dylanwu19850222/lighter-dex) Trading Platform.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install o2-cli
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Login (dev environment)
|
|
45
|
+
o2 auth test-login
|
|
46
|
+
|
|
47
|
+
# View markets (public, no login needed)
|
|
48
|
+
o2 --json markets list
|
|
49
|
+
|
|
50
|
+
# Check balance
|
|
51
|
+
o2 --json balance show
|
|
52
|
+
|
|
53
|
+
# Place a market order (long 0.001 BTC)
|
|
54
|
+
o2 --json orders create -m 1 -s long -t market -a 0.001
|
|
55
|
+
|
|
56
|
+
# View positions
|
|
57
|
+
o2 --json positions list
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Vibe Coding Tool Setup
|
|
61
|
+
|
|
62
|
+
After installing, run the setup wizard to install skill files for your AI coding tool:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Interactive wizard
|
|
66
|
+
o2 setup
|
|
67
|
+
|
|
68
|
+
# Non-interactive
|
|
69
|
+
o2 setup --tool claude-code --scope global
|
|
70
|
+
o2 setup --tool cursor --scope project
|
|
71
|
+
|
|
72
|
+
# Update all installed tools
|
|
73
|
+
o2 setup --update
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Supported tools: **Claude Code**, **Cursor**, **Codex (OpenAI)**, **Windsurf**, **Cline**, **Trae**
|
|
77
|
+
|
|
78
|
+
## Key Rules
|
|
79
|
+
|
|
80
|
+
1. `--json` must come before the subcommand: `o2 --json balance show` (not `o2 balance show --json`)
|
|
81
|
+
2. Public commands (no login): `markets list`, `fees rates`
|
|
82
|
+
3. Other commands require `o2 auth test-login` first
|
|
83
|
+
4. Exit codes: 0 = success, 1 = error
|
|
84
|
+
|
|
85
|
+
## Commands
|
|
86
|
+
|
|
87
|
+
| Group | Commands | Auth Required |
|
|
88
|
+
|-------|----------|---------------|
|
|
89
|
+
| `auth` | `test-login`, `me`, `session` | No (login) |
|
|
90
|
+
| `markets` | `list`, `orderbook`, `candles`, `trades` | No |
|
|
91
|
+
| `fees` | `rates`, `estimate` | No |
|
|
92
|
+
| `balance` | `show`, `history` | Yes |
|
|
93
|
+
| `orders` | `create`, `list`, `cancel`, `cancel-all`, `modify`, `batch` | Yes |
|
|
94
|
+
| `positions` | `list`, `market`, `close`, `risk` | Yes |
|
|
95
|
+
| `trades` | `list`, `summary` | Yes |
|
|
96
|
+
| `deposits` | `address`, `history` | Yes |
|
|
97
|
+
| `withdrawals` | `create`, `status`, `cancel`, `list` | Yes |
|
|
98
|
+
| `settings` | `get`, `leverage`, `margin-mode` | Yes |
|
|
99
|
+
| `notifications` | `list`, `unread`, `read` | Yes |
|
|
100
|
+
| `account` | `overview` | Yes |
|
|
101
|
+
| `mm` | `status`, `start`, `stop`, `stats`, `orders` | API Key |
|
|
102
|
+
| `admin` | `gas-status`, `proxy-list`, `api-keys`, `reconcile` | Admin JWT |
|
|
103
|
+
| `setup` | wizard, `--tool`, `--update`, `--status` | No |
|
|
104
|
+
|
|
105
|
+
## Configuration
|
|
106
|
+
|
|
107
|
+
Config file: `~/.o2/config.yaml`
|
|
108
|
+
|
|
109
|
+
```yaml
|
|
110
|
+
active_profile: default
|
|
111
|
+
profiles:
|
|
112
|
+
default:
|
|
113
|
+
api_url: http://localhost:8000/api/v1
|
|
114
|
+
timeout: 30
|
|
115
|
+
auth_type: jwt
|
|
116
|
+
token: eyJ... # auto-saved
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Override at runtime:
|
|
120
|
+
```bash
|
|
121
|
+
o2 --profile production --json balance show
|
|
122
|
+
o2 --api-url https://api.example.com/api/v1 --json markets list
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Development
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
git clone https://github.com/dylanwu19850222/o2-cli.git
|
|
129
|
+
cd o2-cli
|
|
130
|
+
pip install -e ".[dev]"
|
|
131
|
+
|
|
132
|
+
# Run tests
|
|
133
|
+
pytest
|
|
134
|
+
|
|
135
|
+
# Lint
|
|
136
|
+
ruff check o2_cli/
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT
|