fere-cli 0.1.0.dev6__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.
fere_cli/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """FereAI CLI — terminal interface for crypto trading and research."""
2
+
3
+ __version__ = "0.1.0"
fere_cli/async_util.py ADDED
@@ -0,0 +1,63 @@
1
+ """Async bridge utilities for the CLI.
2
+
3
+ Click commands are synchronous; the SDK is async. This module
4
+ bridges the two with a thin ``run_async`` wrapper and a
5
+ ``get_client`` factory.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import sys
12
+ from functools import wraps
13
+ from typing import Any, Callable
14
+
15
+ import click
16
+ from fere_sdk import FereClient
17
+
18
+ from .config import load_config
19
+
20
+
21
+ def run_async(coro):
22
+ """Run an async coroutine from synchronous context."""
23
+ return asyncio.run(coro)
24
+
25
+
26
+ def async_command(f: Callable) -> Callable:
27
+ """Decorator: wraps an async function for use as a Click command."""
28
+
29
+ @wraps(f)
30
+ def wrapper(*args, **kwargs):
31
+ return run_async(f(*args, **kwargs))
32
+
33
+ return wrapper
34
+
35
+
36
+ async def get_client(ctx: click.Context) -> FereClient:
37
+ """Create an authenticated FereClient from config + CLI overrides.
38
+
39
+ Raises click.ClickException if no agent name is configured.
40
+ """
41
+ cfg = load_config()
42
+
43
+ # CLI flag overrides
44
+ agent_name = ctx.obj.get("agent_override") or cfg.get("agent_name")
45
+ base_url = ctx.obj.get("base_url_override") or cfg.get("base_url")
46
+ key_path = cfg.get("key_path")
47
+
48
+ if not agent_name:
49
+ raise click.ClickException(
50
+ "No agent configured. Run 'fere auth' first "
51
+ "or pass --agent NAME."
52
+ )
53
+
54
+ return await FereClient.create(
55
+ agent_name=agent_name,
56
+ base_url=base_url,
57
+ key_path=key_path,
58
+ )
59
+
60
+
61
+ def is_tty() -> bool:
62
+ """Check if stdin is a TTY (interactive terminal)."""
63
+ return sys.stdin.isatty()
fere_cli/banner.py ADDED
@@ -0,0 +1,23 @@
1
+ """ASCII art banner for the Fere CLI."""
2
+
3
+ from . import __version__
4
+
5
+ # Simplified spiral motif + stylized "FERE AI" text
6
+ # The spiral evokes the Fere AI logo's concentric swirl pattern
7
+ # Kept under 70 chars wide for comfortable 80-col terminal display
8
+ LOGO = r"""
9
+ . · .
10
+ · · _____ _____ ____ _____ _ ___
11
+ · . · . · | ___| ___| _ \| ____| / \ |_ _|
12
+ · · · · | |_ | |_ | |_) | _| / _ \ | |
13
+ · · . · · | _| | _| | _ <| |___ / ___ \ | |
14
+ · · · · |_| |___|_|_| \_\_____| /_/ \_\___|
15
+ · · . · ·
16
+ · ·
17
+ · . ·
18
+ """
19
+
20
+
21
+ def get_banner() -> str:
22
+ """Return the full banner string with version."""
23
+ return f"{LOGO}\n v{__version__} — crypto trading & research from your terminal\n"
File without changes
@@ -0,0 +1,108 @@
1
+ """Auth commands: fere auth, fere whoami, fere credits."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+ from fere_sdk import FereClient
7
+ from rich.console import Console
8
+
9
+ from ..async_util import async_command, get_client, is_tty, run_async
10
+ from ..config import load_config, save_config
11
+ from ..output import print_error, print_success
12
+
13
+ console = Console()
14
+
15
+
16
+ @click.command()
17
+ @click.option("--name", prompt=False, default=None, help="Agent name.")
18
+ @click.pass_context
19
+ @async_command
20
+ async def auth(ctx, name: str | None):
21
+ """Authenticate and register a new agent (first-run setup)."""
22
+ cfg = load_config()
23
+
24
+ agent_name = (
25
+ ctx.obj.get("agent_override")
26
+ or name
27
+ or cfg.get("agent_name")
28
+ )
29
+ if not agent_name:
30
+ if is_tty():
31
+ agent_name = click.prompt("Agent name")
32
+ else:
33
+ raise click.ClickException(
34
+ "Agent name required. Pass --name or --agent."
35
+ )
36
+
37
+ base_url = ctx.obj.get("base_url_override") or cfg.get("base_url")
38
+ key_path = cfg.get("key_path")
39
+
40
+ try:
41
+ client = await FereClient.create(
42
+ agent_name=agent_name,
43
+ base_url=base_url,
44
+ key_path=key_path,
45
+ )
46
+
47
+ # Persist agent name to config
48
+ cfg["agent_name"] = agent_name
49
+ save_config(cfg)
50
+
51
+ user = await client.get_user()
52
+ await client.close()
53
+
54
+ print_success(
55
+ {
56
+ "status": "authenticated",
57
+ "agent_name": agent_name,
58
+ "user": user,
59
+ },
60
+ quiet_value=agent_name,
61
+ )
62
+ except Exception as e:
63
+ print_error(f"Authentication failed: {e}")
64
+ raise SystemExit(1)
65
+
66
+
67
+ @click.command()
68
+ @click.pass_context
69
+ @async_command
70
+ async def whoami(ctx):
71
+ """Show current agent identity and wallet addresses."""
72
+ try:
73
+ client = await get_client(ctx)
74
+ user = await client.get_user()
75
+ wallets_data = await client.get_wallets()
76
+ await client.close()
77
+
78
+ result = {**user}
79
+ if wallets_data:
80
+ result["wallets"] = wallets_data
81
+
82
+ print_success(result)
83
+ except click.ClickException:
84
+ raise
85
+ except Exception as e:
86
+ print_error(str(e))
87
+ raise SystemExit(1)
88
+
89
+
90
+ @click.command()
91
+ @click.pass_context
92
+ @async_command
93
+ async def credits(ctx):
94
+ """Show available credit balance."""
95
+ try:
96
+ client = await get_client(ctx)
97
+ balance = await client.get_credits()
98
+ await client.close()
99
+
100
+ print_success(
101
+ {"credits_available": balance},
102
+ quiet_value=str(balance),
103
+ )
104
+ except click.ClickException:
105
+ raise
106
+ except Exception as e:
107
+ print_error(str(e))
108
+ raise SystemExit(1)
@@ -0,0 +1,198 @@
1
+ """Chat commands: fere chat, fere threads."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+
8
+ import click
9
+ from rich.console import Console
10
+
11
+ from ..async_util import async_command, get_client, is_tty
12
+ from ..output import (
13
+ is_json_mode,
14
+ print_error,
15
+ print_streaming_text,
16
+ print_success,
17
+ print_table_from_dicts,
18
+ )
19
+
20
+ console = Console()
21
+
22
+
23
+ @click.command()
24
+ @click.argument("query", required=False, default=None)
25
+ @click.option(
26
+ "--stream",
27
+ "stream_mode",
28
+ is_flag=True,
29
+ default=False,
30
+ help="Stream response events as they arrive.",
31
+ )
32
+ @click.option(
33
+ "--thread",
34
+ "thread_id",
35
+ default=None,
36
+ help="Continue an existing chat thread.",
37
+ )
38
+ @click.option(
39
+ "--agent",
40
+ "agent_type",
41
+ default="ProAgent",
42
+ help="Agent type to use (default: ProAgent).",
43
+ )
44
+ @click.pass_context
45
+ @async_command
46
+ async def chat(ctx, query, stream_mode, thread_id, agent_type):
47
+ """Chat with FereAI's crypto AI agent.
48
+
49
+ Pass a query as an argument for one-shot mode, or run without
50
+ arguments to enter interactive REPL mode.
51
+ """
52
+ try:
53
+ client = await get_client(ctx)
54
+ except click.ClickException:
55
+ raise
56
+ except Exception as e:
57
+ print_error(str(e))
58
+ raise SystemExit(1)
59
+
60
+ try:
61
+ if query:
62
+ # One-shot mode
63
+ if stream_mode:
64
+ await _stream_chat(
65
+ client, query, thread_id, agent_type
66
+ )
67
+ else:
68
+ await _oneshot_chat(
69
+ client, query, thread_id, agent_type
70
+ )
71
+ elif is_tty():
72
+ # Interactive REPL
73
+ await _repl_chat(client, thread_id, agent_type)
74
+ else:
75
+ raise click.ClickException(
76
+ "Query required in non-interactive mode. "
77
+ "Usage: fere chat \"your question\""
78
+ )
79
+ except click.ClickException:
80
+ raise
81
+ except KeyboardInterrupt:
82
+ pass
83
+ except Exception as e:
84
+ print_error(str(e))
85
+ raise SystemExit(1)
86
+ finally:
87
+ await client.close()
88
+
89
+
90
+ async def _oneshot_chat(client, query, thread_id, agent_type):
91
+ """Send a query and print the final answer."""
92
+ result = await client.chat(
93
+ query, thread_id=thread_id, agent=agent_type
94
+ )
95
+ print_success(
96
+ result,
97
+ quiet_value=result.get("answer", ""),
98
+ )
99
+
100
+
101
+ async def _stream_chat(client, query, thread_id, agent_type):
102
+ """Stream chat events, printing each as it arrives."""
103
+ json_mode = is_json_mode()
104
+
105
+ async for event in client.chat_stream(
106
+ query, thread_id=thread_id, agent=agent_type
107
+ ):
108
+ if json_mode:
109
+ # NDJSON: one JSON object per line
110
+ click.echo(
111
+ json.dumps(
112
+ {"event": event.event, "data": event.data},
113
+ default=str,
114
+ )
115
+ )
116
+ else:
117
+ if event.event == "chunk":
118
+ text = event.data.get("text", "")
119
+ print_streaming_text(text)
120
+ elif event.event == "answer":
121
+ text = event.data.get("text", "")
122
+ if text:
123
+ click.echo() # newline after chunks
124
+ console.print(
125
+ f"\n[bold]Answer:[/bold] {text}"
126
+ )
127
+ elif event.event == "tool_response":
128
+ console.print(
129
+ f"[dim]Tool: {json.dumps(event.data, default=str)}[/dim]"
130
+ )
131
+ elif event.event == "error":
132
+ msg = event.data.get("message", str(event.data))
133
+ print_error(msg)
134
+ elif event.event == "meta":
135
+ chat_id = event.data.get("chat_id", "")
136
+ console.print(f"[dim]Chat: {chat_id}[/dim]")
137
+
138
+ if not json_mode:
139
+ click.echo() # final newline
140
+
141
+
142
+ async def _repl_chat(client, thread_id, agent_type):
143
+ """Interactive REPL loop."""
144
+ console.print(
145
+ "[bold]FereAI Chat[/bold] "
146
+ "[dim](type /exit or Ctrl+D to quit)[/dim]\n"
147
+ )
148
+
149
+ current_thread = thread_id
150
+
151
+ while True:
152
+ try:
153
+ query = input(">>> ")
154
+ except EOFError:
155
+ click.echo()
156
+ break
157
+
158
+ query = query.strip()
159
+ if not query:
160
+ continue
161
+ if query in ("/exit", "/quit"):
162
+ break
163
+
164
+ result = await client.chat(
165
+ query, thread_id=current_thread, agent=agent_type
166
+ )
167
+
168
+ # Track thread for continuity
169
+ if "chat_id" in result and not current_thread:
170
+ current_thread = result["chat_id"]
171
+
172
+ answer = result.get("answer", "")
173
+ if answer:
174
+ console.print(f"\n{answer}\n")
175
+
176
+
177
+ @click.command()
178
+ @click.option(
179
+ "--limit",
180
+ default=10,
181
+ help="Number of threads to show.",
182
+ )
183
+ @click.option("--skip", default=0, help="Offset.")
184
+ @click.pass_context
185
+ @async_command
186
+ async def threads(ctx, limit, skip):
187
+ """List recent chat threads."""
188
+ try:
189
+ client = await get_client(ctx)
190
+ data = await client.get_threads(skip=skip, limit=limit)
191
+ await client.close()
192
+
193
+ print_success(data)
194
+ except click.ClickException:
195
+ raise
196
+ except Exception as e:
197
+ print_error(str(e))
198
+ raise SystemExit(1)
@@ -0,0 +1,143 @@
1
+ """Earn commands: fere earn (info, deposit, withdraw, positions)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+ from rich.console import Console
7
+
8
+ from ..async_util import async_command, get_client, is_tty
9
+ from ..output import print_error, print_success
10
+
11
+ console = Console()
12
+
13
+
14
+ @click.group()
15
+ def earn():
16
+ """Manage yield earning (deposit, withdraw, view APY)."""
17
+ pass
18
+
19
+
20
+ @earn.command()
21
+ @click.pass_context
22
+ @async_command
23
+ async def info(ctx):
24
+ """Show current APY and vault details."""
25
+ try:
26
+ client = await get_client(ctx)
27
+ data = await client.get_earn_info()
28
+ await client.close()
29
+
30
+ print_success(data)
31
+ except click.ClickException:
32
+ raise
33
+ except Exception as e:
34
+ print_error(str(e))
35
+ raise SystemExit(1)
36
+
37
+
38
+ @earn.command()
39
+ @click.option(
40
+ "--amount",
41
+ required=True,
42
+ type=float,
43
+ help="USDC amount to deposit.",
44
+ )
45
+ @click.option(
46
+ "--position-id",
47
+ default=None,
48
+ help="Existing position ID to add to.",
49
+ )
50
+ @click.option(
51
+ "--timeout", default=120, type=int, help="Wait timeout (seconds)."
52
+ )
53
+ @click.option("-y", "--yes", is_flag=True, help="Skip confirmation.")
54
+ @click.pass_context
55
+ @async_command
56
+ async def deposit(ctx, amount, position_id, timeout, yes):
57
+ """Deposit USDC to earn yield."""
58
+ if not yes and is_tty():
59
+ click.echo(f"Deposit {amount} USDC into Fere Earn")
60
+ if not click.confirm("Proceed?"):
61
+ raise SystemExit(0)
62
+
63
+ try:
64
+ client = await get_client(ctx)
65
+
66
+ with console.status("Processing deposit..."):
67
+ result = await client.deposit(
68
+ amount_usdc=amount,
69
+ position_id=position_id,
70
+ timeout=timeout,
71
+ )
72
+
73
+ await client.close()
74
+ print_success(result)
75
+ except click.ClickException:
76
+ raise
77
+ except Exception as e:
78
+ print_error(str(e))
79
+ raise SystemExit(1)
80
+
81
+
82
+ @earn.command()
83
+ @click.option(
84
+ "--position-id",
85
+ required=True,
86
+ help="Position ID to withdraw from.",
87
+ )
88
+ @click.option(
89
+ "--amount",
90
+ required=True,
91
+ type=float,
92
+ help="USDC amount to withdraw.",
93
+ )
94
+ @click.option(
95
+ "--timeout", default=120, type=int, help="Wait timeout (seconds)."
96
+ )
97
+ @click.option("-y", "--yes", is_flag=True, help="Skip confirmation.")
98
+ @click.pass_context
99
+ @async_command
100
+ async def withdraw(ctx, position_id, amount, timeout, yes):
101
+ """Withdraw USDC from a yield position."""
102
+ if not yes and is_tty():
103
+ click.echo(
104
+ f"Withdraw {amount} USDC from position {position_id}"
105
+ )
106
+ if not click.confirm("Proceed?"):
107
+ raise SystemExit(0)
108
+
109
+ try:
110
+ client = await get_client(ctx)
111
+
112
+ with console.status("Processing withdrawal..."):
113
+ result = await client.withdraw(
114
+ position_id=position_id,
115
+ amount_usdc=amount,
116
+ timeout=timeout,
117
+ )
118
+
119
+ await client.close()
120
+ print_success(result)
121
+ except click.ClickException:
122
+ raise
123
+ except Exception as e:
124
+ print_error(str(e))
125
+ raise SystemExit(1)
126
+
127
+
128
+ @earn.command()
129
+ @click.pass_context
130
+ @async_command
131
+ async def positions(ctx):
132
+ """Show active yield positions."""
133
+ try:
134
+ client = await get_client(ctx)
135
+ data = await client.get_positions()
136
+ await client.close()
137
+
138
+ print_success(data)
139
+ except click.ClickException:
140
+ raise
141
+ except Exception as e:
142
+ print_error(str(e))
143
+ raise SystemExit(1)
@@ -0,0 +1,74 @@
1
+ """Hooks commands: fere hooks set."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ import click
8
+
9
+ from ..async_util import async_command, get_client
10
+ from ..output import print_error, print_success
11
+
12
+
13
+ @click.group()
14
+ def hooks():
15
+ """Manage stop-loss and take-profit hooks."""
16
+ pass
17
+
18
+
19
+ @hooks.command()
20
+ @click.option(
21
+ "--chain-id", required=True, type=int, help="Chain ID."
22
+ )
23
+ @click.option(
24
+ "--token", required=True, help="Token contract address."
25
+ )
26
+ @click.option(
27
+ "--stop-loss",
28
+ default=None,
29
+ help="Stop-loss config as JSON (e.g. '{\"price\": 1.5}').",
30
+ )
31
+ @click.option(
32
+ "--take-profit",
33
+ default=None,
34
+ help="Take-profit config as JSON (e.g. '{\"price\": 3.0}').",
35
+ )
36
+ @click.pass_context
37
+ @async_command
38
+ async def set(ctx, chain_id, token, stop_loss, take_profit):
39
+ """Set stop-loss and/or take-profit hooks on a token."""
40
+ try:
41
+ sl = json.loads(stop_loss) if stop_loss else None
42
+ except json.JSONDecodeError as exc:
43
+ raise click.BadParameter(
44
+ f"Invalid JSON for --stop-loss: {exc}", param_hint="'--stop-loss'"
45
+ )
46
+ try:
47
+ tp = json.loads(take_profit) if take_profit else None
48
+ except json.JSONDecodeError as exc:
49
+ raise click.BadParameter(
50
+ f"Invalid JSON for --take-profit: {exc}",
51
+ param_hint="'--take-profit'",
52
+ )
53
+
54
+ if not sl and not tp:
55
+ raise click.ClickException(
56
+ "At least one of --stop-loss or --take-profit is required."
57
+ )
58
+
59
+ try:
60
+ client = await get_client(ctx)
61
+ result = await client.set_hooks(
62
+ chain_id=chain_id,
63
+ token_address=token,
64
+ stop_loss=sl,
65
+ take_profit=tp,
66
+ )
67
+ await client.close()
68
+
69
+ print_success(result)
70
+ except click.ClickException:
71
+ raise
72
+ except Exception as e:
73
+ print_error(str(e))
74
+ raise SystemExit(1)