talis-cli 0.2.0__tar.gz → 0.2.1__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.
- {talis_cli-0.2.0 → talis_cli-0.2.1}/PKG-INFO +46 -9
- {talis_cli-0.2.0 → talis_cli-0.2.1}/README.md +45 -8
- {talis_cli-0.2.0 → talis_cli-0.2.1}/pyproject.toml +1 -1
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/cli.py +66 -1
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/mcp_server.py +63 -15
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis_cli.egg-info/PKG-INFO +46 -9
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_mcp_docstring_contract.py +45 -0
- talis_cli-0.2.1/tests/test_mcp_session_expiry.py +276 -0
- talis_cli-0.2.1/tests/test_session_autorenew.py +286 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_version_sync.py +6 -1
- talis_cli-0.2.0/tests/test_mcp_session_expiry.py +0 -124
- talis_cli-0.2.0/tests/test_session_autorenew.py +0 -141
- {talis_cli-0.2.0 → talis_cli-0.2.1}/setup.cfg +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/__init__.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/_eip55.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/api.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/__init__.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/_shared.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/auth.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/brief.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/chat.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/fund.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/guide.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/history.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/notifications.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/outcome.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/portfolio.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/research.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/snapshot.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/strategies.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/test_tenant.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/trade.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/wait.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/withdraw.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/config.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/output.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/paper.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/spot.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/symbols.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis_cli.egg-info/SOURCES.txt +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis_cli.egg-info/dependency_links.txt +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis_cli.egg-info/entry_points.txt +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis_cli.egg-info/requires.txt +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/talis_cli.egg-info/top_level.txt +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_api.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_api_key_auth.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_brief.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_chat.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_commands.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_config.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_confirm_prompt.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_eip55.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_guide.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_history.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_login_flow.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_loopback_login.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_mcp.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_mcp_balance_contract.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_mcp_mode_contract.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_mcp_sdk_compat.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_mcp_strategy_tools.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_non_interactive_auth.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_notifications.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_outcome.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_paper_mode.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_research.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_signup.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_snapshot_diff.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_spot.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_spot_gateway_first.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_strategies_create.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_test_tenant.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_wait.py +0 -0
- {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_withdraw.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: talis-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Command-line client for the Talis trading platform. Sign in via device flow, manage sessions, view portfolio, place orders.
|
|
5
5
|
Author: Talis
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -114,6 +114,27 @@ suitable for parsing from scripts or LLM agents. Force JSON anywhere with
|
|
|
114
114
|
The CLI stores its session token at `~/.talis/credentials` with `chmod 600`
|
|
115
115
|
on POSIX systems. The token is a JWT; treat it like any other credential.
|
|
116
116
|
|
|
117
|
+
### Sessions renew themselves
|
|
118
|
+
|
|
119
|
+
You should not have to think about session expiry. When the CLI or the MCP
|
|
120
|
+
server runs with under 48 hours left on the clock, it renews in place against
|
|
121
|
+
`POST /auth/session/refresh` before doing its work — at most once every 15
|
|
122
|
+
minutes, so a burst of commands costs one renewal, not one each. Renewal
|
|
123
|
+
preserves the
|
|
124
|
+
session's **original grant**, not a fresh maximum — a token issued with a 24h
|
|
125
|
+
life renews to another 24h, never to something longer. That is deliberate: an
|
|
126
|
+
auto-renewing session must not be able to ratchet itself into a longer-lived
|
|
127
|
+
credential than the one you approved.
|
|
128
|
+
|
|
129
|
+
Renewal is best-effort and silent: if it cannot happen, your existing
|
|
130
|
+
credential is left untouched and the command still runs. What you WILL see is a
|
|
131
|
+
`session_warning` on MCP tool responses once renewal stops keeping up, or when
|
|
132
|
+
under six hours remain. If that appears and keeps counting down, renewal is
|
|
133
|
+
failing — run `talis login` to sign in again.
|
|
134
|
+
|
|
135
|
+
A steady `session_hours_remaining` with no `session_warning` is the healthy
|
|
136
|
+
state, not a countdown to worry about.
|
|
137
|
+
|
|
117
138
|
## Relationship to `cli/jarvis_cli.sh`
|
|
118
139
|
|
|
119
140
|
The bash script at [`cli/jarvis_cli.sh`](../../cli/jarvis_cli.sh) is the legacy
|
|
@@ -142,12 +163,16 @@ your portfolio, pull Talis Research, place paper trades, create strategies,
|
|
|
142
163
|
set price alerts, and talk to the Talis chat agent. Log in first
|
|
143
164
|
(`talis login`); the server authenticates as the logged-in user.
|
|
144
165
|
|
|
145
|
-
|
|
166
|
+
Install and add it to **Claude Code**:
|
|
146
167
|
|
|
147
168
|
```bash
|
|
169
|
+
pip install talis-cli # or: uv tool install talis-cli
|
|
170
|
+
talis login # device-flow browser approval, once
|
|
148
171
|
claude mcp add talis -- talis mcp
|
|
149
172
|
```
|
|
150
173
|
|
|
174
|
+
No local checkout required — `talis` is on PyPI.
|
|
175
|
+
|
|
151
176
|
Or to **Claude Desktop** (and other clients using the `mcpServers` config
|
|
152
177
|
shape) — `claude_desktop_config.json`:
|
|
153
178
|
|
|
@@ -162,14 +187,26 @@ shape) — `claude_desktop_config.json`:
|
|
|
162
187
|
}
|
|
163
188
|
```
|
|
164
189
|
|
|
165
|
-
Tools:
|
|
166
|
-
|
|
167
|
-
`
|
|
190
|
+
Tools (25):
|
|
191
|
+
|
|
192
|
+
`amend_strategy`, `balance`, `brief_latest`, `buy`, `cancel_all_orders`,
|
|
193
|
+
`cancel_order`, `chat`, `close`, `create_strategy`, `history`,
|
|
194
|
+
`notifications_create`, `open_orders`, `outcomes`, `packets`, `place_order`,
|
|
195
|
+
`portfolio`, `research`, `sell`, `start_strategy`, `stop_strategy`,
|
|
196
|
+
`strategies`, `strategy`, `strategy_activity`, `validate_strategy`, `whoami`.
|
|
197
|
+
|
|
198
|
+
**Safety — read this before pointing an agent at it.** The trading tools
|
|
199
|
+
(`buy` / `sell` / `close` / `create_strategy`) execute with **REAL MONEY by
|
|
200
|
+
default** (`paper=False`). Pass `paper=True` to simulate instead.
|
|
201
|
+
|
|
202
|
+
`buy`, `sell` and `create_strategy` additionally refuse to spend real money
|
|
203
|
+
unless the caller passes `confirm_real_money=True`. `close` deliberately has no
|
|
204
|
+
such gate: a protective exit must never be blocked by a missing flag.
|
|
205
|
+
`place_order` (limit orders, spot, HIP-4 outcome markets) has **no paper mode
|
|
206
|
+
at all** and refuses without `confirm_real_money=True`.
|
|
168
207
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
`paper=False` explicitly to trade real money. Set `TALIS_PAPER=1` in the
|
|
172
|
-
server's environment to force paper mode regardless of tool arguments.
|
|
208
|
+
Set `TALIS_PAPER=1` in the server's environment to force paper mode regardless
|
|
209
|
+
of tool arguments.
|
|
173
210
|
Withdrawals and wallet creation are not exposed over MCP. For a strictly
|
|
174
211
|
read-only agent, log in with `talis login --read-only` — the server rejects
|
|
175
212
|
every mutation at the API layer.
|
|
@@ -86,6 +86,27 @@ suitable for parsing from scripts or LLM agents. Force JSON anywhere with
|
|
|
86
86
|
The CLI stores its session token at `~/.talis/credentials` with `chmod 600`
|
|
87
87
|
on POSIX systems. The token is a JWT; treat it like any other credential.
|
|
88
88
|
|
|
89
|
+
### Sessions renew themselves
|
|
90
|
+
|
|
91
|
+
You should not have to think about session expiry. When the CLI or the MCP
|
|
92
|
+
server runs with under 48 hours left on the clock, it renews in place against
|
|
93
|
+
`POST /auth/session/refresh` before doing its work — at most once every 15
|
|
94
|
+
minutes, so a burst of commands costs one renewal, not one each. Renewal
|
|
95
|
+
preserves the
|
|
96
|
+
session's **original grant**, not a fresh maximum — a token issued with a 24h
|
|
97
|
+
life renews to another 24h, never to something longer. That is deliberate: an
|
|
98
|
+
auto-renewing session must not be able to ratchet itself into a longer-lived
|
|
99
|
+
credential than the one you approved.
|
|
100
|
+
|
|
101
|
+
Renewal is best-effort and silent: if it cannot happen, your existing
|
|
102
|
+
credential is left untouched and the command still runs. What you WILL see is a
|
|
103
|
+
`session_warning` on MCP tool responses once renewal stops keeping up, or when
|
|
104
|
+
under six hours remain. If that appears and keeps counting down, renewal is
|
|
105
|
+
failing — run `talis login` to sign in again.
|
|
106
|
+
|
|
107
|
+
A steady `session_hours_remaining` with no `session_warning` is the healthy
|
|
108
|
+
state, not a countdown to worry about.
|
|
109
|
+
|
|
89
110
|
## Relationship to `cli/jarvis_cli.sh`
|
|
90
111
|
|
|
91
112
|
The bash script at [`cli/jarvis_cli.sh`](../../cli/jarvis_cli.sh) is the legacy
|
|
@@ -114,12 +135,16 @@ your portfolio, pull Talis Research, place paper trades, create strategies,
|
|
|
114
135
|
set price alerts, and talk to the Talis chat agent. Log in first
|
|
115
136
|
(`talis login`); the server authenticates as the logged-in user.
|
|
116
137
|
|
|
117
|
-
|
|
138
|
+
Install and add it to **Claude Code**:
|
|
118
139
|
|
|
119
140
|
```bash
|
|
141
|
+
pip install talis-cli # or: uv tool install talis-cli
|
|
142
|
+
talis login # device-flow browser approval, once
|
|
120
143
|
claude mcp add talis -- talis mcp
|
|
121
144
|
```
|
|
122
145
|
|
|
146
|
+
No local checkout required — `talis` is on PyPI.
|
|
147
|
+
|
|
123
148
|
Or to **Claude Desktop** (and other clients using the `mcpServers` config
|
|
124
149
|
shape) — `claude_desktop_config.json`:
|
|
125
150
|
|
|
@@ -134,14 +159,26 @@ shape) — `claude_desktop_config.json`:
|
|
|
134
159
|
}
|
|
135
160
|
```
|
|
136
161
|
|
|
137
|
-
Tools:
|
|
138
|
-
|
|
139
|
-
`
|
|
162
|
+
Tools (25):
|
|
163
|
+
|
|
164
|
+
`amend_strategy`, `balance`, `brief_latest`, `buy`, `cancel_all_orders`,
|
|
165
|
+
`cancel_order`, `chat`, `close`, `create_strategy`, `history`,
|
|
166
|
+
`notifications_create`, `open_orders`, `outcomes`, `packets`, `place_order`,
|
|
167
|
+
`portfolio`, `research`, `sell`, `start_strategy`, `stop_strategy`,
|
|
168
|
+
`strategies`, `strategy`, `strategy_activity`, `validate_strategy`, `whoami`.
|
|
169
|
+
|
|
170
|
+
**Safety — read this before pointing an agent at it.** The trading tools
|
|
171
|
+
(`buy` / `sell` / `close` / `create_strategy`) execute with **REAL MONEY by
|
|
172
|
+
default** (`paper=False`). Pass `paper=True` to simulate instead.
|
|
173
|
+
|
|
174
|
+
`buy`, `sell` and `create_strategy` additionally refuse to spend real money
|
|
175
|
+
unless the caller passes `confirm_real_money=True`. `close` deliberately has no
|
|
176
|
+
such gate: a protective exit must never be blocked by a missing flag.
|
|
177
|
+
`place_order` (limit orders, spot, HIP-4 outcome markets) has **no paper mode
|
|
178
|
+
at all** and refuses without `confirm_real_money=True`.
|
|
140
179
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
`paper=False` explicitly to trade real money. Set `TALIS_PAPER=1` in the
|
|
144
|
-
server's environment to force paper mode regardless of tool arguments.
|
|
180
|
+
Set `TALIS_PAPER=1` in the server's environment to force paper mode regardless
|
|
181
|
+
of tool arguments.
|
|
145
182
|
Withdrawals and wallet creation are not exposed over MCP. For a strictly
|
|
146
183
|
read-only agent, log in with `talis login --read-only` — the server rejects
|
|
147
184
|
every mutation at the API layer.
|
|
@@ -13,7 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
import typer
|
|
15
15
|
|
|
16
|
-
from . import __version__, output
|
|
16
|
+
from . import __version__, config, output
|
|
17
17
|
from .commands import auth, mcp_server, trade
|
|
18
18
|
from .commands import chat as chat_cmd
|
|
19
19
|
from .commands import guide as guide_cmd
|
|
@@ -83,6 +83,71 @@ app.add_typer(fund_app, name="fund")
|
|
|
83
83
|
app.command("deposit")(__import__("talis.commands.fund",fromlist=["deposit"]).deposit)
|
|
84
84
|
|
|
85
85
|
|
|
86
|
+
# Commands that must NEVER trigger a renewal.
|
|
87
|
+
# login/signup/approve — there is no session yet to renew.
|
|
88
|
+
# logout — we are about to revoke the session we hold; renewing it first is
|
|
89
|
+
# wasted work and muddies the audit trail.
|
|
90
|
+
# version — must stay answerable with no credentials and no network at all.
|
|
91
|
+
_NO_RENEW = frozenset({"login", "signup", "approve", "logout", "version"})
|
|
92
|
+
|
|
93
|
+
# Minimum gap between renewal attempts across separate CLI invocations.
|
|
94
|
+
_RENEW_COOLDOWN_SECONDS = 900.0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _renewed_recently() -> bool:
|
|
98
|
+
"""True when the stored credential was rewritten inside the cooldown.
|
|
99
|
+
|
|
100
|
+
WHY A COOLDOWN AT ALL. ensure_fresh fires whenever under 48h remain, but a
|
|
101
|
+
session's grant is 24h — permanently under that threshold. Without a gate,
|
|
102
|
+
wiring renewal into the CLI would mint a new JWT and pay a network
|
|
103
|
+
round-trip on EVERY command, forever.
|
|
104
|
+
|
|
105
|
+
WHY MTIME rather than a new state file. The MCP server uses an in-memory
|
|
106
|
+
cooldown, which cannot work here: a CLI process is born and dies per
|
|
107
|
+
command. The credentials file's mtime is ALREADY an exact record of the
|
|
108
|
+
last SUCCESSFUL renewal (renewal rewrites it via atomic replace), so use
|
|
109
|
+
that instead of inventing a second source of truth that could disagree
|
|
110
|
+
with the first.
|
|
111
|
+
|
|
112
|
+
A FAILED renewal leaves mtime untouched, so a broken renewal keeps being
|
|
113
|
+
retried rather than being silenced by its own failure — the behaviour we
|
|
114
|
+
want, obtained for free.
|
|
115
|
+
"""
|
|
116
|
+
import os
|
|
117
|
+
import time as _t
|
|
118
|
+
try:
|
|
119
|
+
age = _t.time() - os.stat(str(config.credentials_path())).st_mtime
|
|
120
|
+
except OSError:
|
|
121
|
+
return False # unreadable/absent: let ensure_fresh decide
|
|
122
|
+
# A negative age means the clock moved; treat it as recent so a skewed
|
|
123
|
+
# machine backs off rather than renewing in a tight loop.
|
|
124
|
+
return age < _RENEW_COOLDOWN_SECONDS
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@app.callback()
|
|
128
|
+
def _keep_session_alive(ctx: typer.Context) -> None:
|
|
129
|
+
"""Renew a short-lived session before running the command.
|
|
130
|
+
|
|
131
|
+
WHY THIS EXISTS: renewal was wired into the MCP server only, so a human
|
|
132
|
+
running `talis portfolio` every day still watched their session run out —
|
|
133
|
+
the CLI shipped the renewal code and never called it. The README promised
|
|
134
|
+
otherwise. This is the code catching up to the claim.
|
|
135
|
+
|
|
136
|
+
Best-effort BY CONTRACT: any failure leaves the stored credential exactly
|
|
137
|
+
as it was and the command runs anyway. config.ensure_fresh also no-ops
|
|
138
|
+
without touching the network unless the session is genuinely short, so this
|
|
139
|
+
costs nothing on the overwhelming majority of invocations.
|
|
140
|
+
"""
|
|
141
|
+
if ctx.invoked_subcommand in _NO_RENEW or _renewed_recently():
|
|
142
|
+
return
|
|
143
|
+
try:
|
|
144
|
+
creds = config.load()
|
|
145
|
+
if creds:
|
|
146
|
+
config.ensure_fresh(creds)
|
|
147
|
+
except Exception:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
|
|
86
151
|
@app.command("version")
|
|
87
152
|
def version(json_: bool = typer.Option(False, "--json", help="JSON output.")) -> None:
|
|
88
153
|
"""Print the CLI version."""
|
|
@@ -64,9 +64,31 @@ class NotLoggedInError(RuntimeError):
|
|
|
64
64
|
# warning, because the tool being used is the only one guaranteed to be seen.
|
|
65
65
|
_SESSION_WARN_HOURS = 48.0
|
|
66
66
|
|
|
67
|
+
# Below this the session is genuinely at risk and we warn however healthy
|
|
68
|
+
# renewal looks. Above it, a session that is renewing on schedule is NOT news.
|
|
69
|
+
#
|
|
70
|
+
# WHY THIS SECOND TIER EXISTS: /auth/session/refresh deliberately carries the
|
|
71
|
+
# ORIGINAL grant rather than issuing a fresh maximum-length one — a token minted
|
|
72
|
+
# with a 24h life renews to 24h, never to 7d. That is the token-lifetime ceiling
|
|
73
|
+
# working as designed. But it means a perfectly self-renewing session sits
|
|
74
|
+
# permanently at ~24h, which is permanently under a 48h warn floor. Warning on
|
|
75
|
+
# that alone emitted SESSION EXPIRES on every single tool response forever — one
|
|
76
|
+
# second after a successful renewal included. And because the warning is
|
|
77
|
+
# deliberately hoisted to the FRONT of the dict, it was the first thing the model
|
|
78
|
+
# read on every call. A warning that always fires is one the reader learns to
|
|
79
|
+
# skip, including on the day renewal actually breaks.
|
|
80
|
+
_SESSION_CRITICAL_HOURS = 6.0
|
|
81
|
+
|
|
82
|
+
# What the last renewal attempt actually ACHIEVED. Written by _maybe_renew, read
|
|
83
|
+
# below to tell "short grant, renewing fine" apart from "renewal is broken".
|
|
84
|
+
# Starts "unknown" and "unknown" warns: absence of evidence that renewal works
|
|
85
|
+
# is not evidence that it does.
|
|
86
|
+
_RENEW_OUTCOME = ["unknown"] # unknown | renewed | not_needed | failed
|
|
87
|
+
_RENEW_HEALTHY = ("renewed", "not_needed")
|
|
88
|
+
|
|
67
89
|
|
|
68
90
|
def _session_expiry_warning(creds) -> dict:
|
|
69
|
-
"""{} when the session is
|
|
91
|
+
"""{} when the session is alive AND renewing, else a loud actionable dict."""
|
|
70
92
|
raw = getattr(creds, "expires_at", None)
|
|
71
93
|
if not raw:
|
|
72
94
|
return {}
|
|
@@ -84,17 +106,21 @@ def _session_expiry_warning(creds) -> dict:
|
|
|
84
106
|
if hours <= 0:
|
|
85
107
|
return {"session_expired": True,
|
|
86
108
|
"session_warning": (
|
|
87
|
-
f"SESSION EXPIRED at {raw}. Every call will now fail.
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
|
|
109
|
+
f"SESSION EXPIRED at {raw}. Every call will now fail. "
|
|
110
|
+
"Automatic renewal did not save it — run `talis login` to "
|
|
111
|
+
"sign in again.")}
|
|
112
|
+
# Always REPORT the number; raise the ALARM only when renewal is not holding
|
|
113
|
+
# the line. Readers key off `session_warning`, never off this field.
|
|
114
|
+
remaining = {"session_hours_remaining": round(hours, 1)}
|
|
115
|
+
if hours > _SESSION_CRITICAL_HOURS and _RENEW_OUTCOME[0] in _RENEW_HEALTHY:
|
|
116
|
+
return remaining
|
|
117
|
+
return {**remaining,
|
|
91
118
|
"session_warning": (
|
|
92
|
-
f"SESSION EXPIRES IN {hours:.1f} HOURS ({raw}).
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"that job is NOT running — check ~/.talis/token-refresh.log.")}
|
|
119
|
+
f"SESSION EXPIRES IN {hours:.1f} HOURS ({raw}). This client "
|
|
120
|
+
"renews itself against POST /auth/session/refresh as you use "
|
|
121
|
+
"it, but the last attempt did not extend the session. If this "
|
|
122
|
+
"keeps counting down, renewal is failing — run `talis login` "
|
|
123
|
+
"to sign in again before it expires.")}
|
|
98
124
|
|
|
99
125
|
|
|
100
126
|
_LAST_RENEW_ATTEMPT = [0.0]
|
|
@@ -111,7 +137,9 @@ def _maybe_renew() -> None:
|
|
|
111
137
|
and config.ensure_fresh itself no-ops unless the session is actually short.
|
|
112
138
|
|
|
113
139
|
Silent by contract: a renewal that cannot happen must never turn into a
|
|
114
|
-
tool error. The session_warning that follows is what tells the reader
|
|
140
|
+
tool error. The session_warning that follows is what tells the reader — and
|
|
141
|
+
it can only tell the truth if this records what actually happened, so every
|
|
142
|
+
exit path here writes _RENEW_OUTCOME.
|
|
115
143
|
"""
|
|
116
144
|
import time as _t
|
|
117
145
|
|
|
@@ -121,10 +149,30 @@ def _maybe_renew() -> None:
|
|
|
121
149
|
_LAST_RENEW_ATTEMPT[0] = now
|
|
122
150
|
try:
|
|
123
151
|
creds = config.load()
|
|
124
|
-
if creds:
|
|
125
|
-
|
|
152
|
+
if not creds:
|
|
153
|
+
_RENEW_OUTCOME[0] = "unknown"
|
|
154
|
+
return
|
|
155
|
+
before_token = getattr(creds, "token", None)
|
|
156
|
+
before_hours = config.hours_remaining(creds)
|
|
157
|
+
fresh = config.ensure_fresh(creds)
|
|
158
|
+
after_token = getattr(fresh, "token", None)
|
|
159
|
+
if after_token and after_token != before_token:
|
|
160
|
+
# A DIFFERENT token came back — renewal demonstrably worked. Keyed
|
|
161
|
+
# on token identity, not on elapsed hours: comparing expiries means
|
|
162
|
+
# comparing floats across a wall-clock read, and a no-op would drift
|
|
163
|
+
# below its own start value on any slow call.
|
|
164
|
+
_RENEW_OUTCOME[0] = "renewed"
|
|
165
|
+
elif before_hours is None or before_hours > config.REFRESH_UNDER_HOURS:
|
|
166
|
+
# ensure_fresh no-ops above the threshold, and for API-key creds
|
|
167
|
+
# which carry no expiry at all. Nothing was attempted, so nothing
|
|
168
|
+
# failed — do not cry wolf.
|
|
169
|
+
_RENEW_OUTCOME[0] = "not_needed"
|
|
170
|
+
else:
|
|
171
|
+
# Under the threshold and the token did not change: renewal was
|
|
172
|
+
# attempted and did not take. THIS is the state worth shouting at.
|
|
173
|
+
_RENEW_OUTCOME[0] = "failed"
|
|
126
174
|
except Exception:
|
|
127
|
-
|
|
175
|
+
_RENEW_OUTCOME[0] = "failed"
|
|
128
176
|
|
|
129
177
|
|
|
130
178
|
def _with_session_warning(fn):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: talis-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Command-line client for the Talis trading platform. Sign in via device flow, manage sessions, view portfolio, place orders.
|
|
5
5
|
Author: Talis
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -114,6 +114,27 @@ suitable for parsing from scripts or LLM agents. Force JSON anywhere with
|
|
|
114
114
|
The CLI stores its session token at `~/.talis/credentials` with `chmod 600`
|
|
115
115
|
on POSIX systems. The token is a JWT; treat it like any other credential.
|
|
116
116
|
|
|
117
|
+
### Sessions renew themselves
|
|
118
|
+
|
|
119
|
+
You should not have to think about session expiry. When the CLI or the MCP
|
|
120
|
+
server runs with under 48 hours left on the clock, it renews in place against
|
|
121
|
+
`POST /auth/session/refresh` before doing its work — at most once every 15
|
|
122
|
+
minutes, so a burst of commands costs one renewal, not one each. Renewal
|
|
123
|
+
preserves the
|
|
124
|
+
session's **original grant**, not a fresh maximum — a token issued with a 24h
|
|
125
|
+
life renews to another 24h, never to something longer. That is deliberate: an
|
|
126
|
+
auto-renewing session must not be able to ratchet itself into a longer-lived
|
|
127
|
+
credential than the one you approved.
|
|
128
|
+
|
|
129
|
+
Renewal is best-effort and silent: if it cannot happen, your existing
|
|
130
|
+
credential is left untouched and the command still runs. What you WILL see is a
|
|
131
|
+
`session_warning` on MCP tool responses once renewal stops keeping up, or when
|
|
132
|
+
under six hours remain. If that appears and keeps counting down, renewal is
|
|
133
|
+
failing — run `talis login` to sign in again.
|
|
134
|
+
|
|
135
|
+
A steady `session_hours_remaining` with no `session_warning` is the healthy
|
|
136
|
+
state, not a countdown to worry about.
|
|
137
|
+
|
|
117
138
|
## Relationship to `cli/jarvis_cli.sh`
|
|
118
139
|
|
|
119
140
|
The bash script at [`cli/jarvis_cli.sh`](../../cli/jarvis_cli.sh) is the legacy
|
|
@@ -142,12 +163,16 @@ your portfolio, pull Talis Research, place paper trades, create strategies,
|
|
|
142
163
|
set price alerts, and talk to the Talis chat agent. Log in first
|
|
143
164
|
(`talis login`); the server authenticates as the logged-in user.
|
|
144
165
|
|
|
145
|
-
|
|
166
|
+
Install and add it to **Claude Code**:
|
|
146
167
|
|
|
147
168
|
```bash
|
|
169
|
+
pip install talis-cli # or: uv tool install talis-cli
|
|
170
|
+
talis login # device-flow browser approval, once
|
|
148
171
|
claude mcp add talis -- talis mcp
|
|
149
172
|
```
|
|
150
173
|
|
|
174
|
+
No local checkout required — `talis` is on PyPI.
|
|
175
|
+
|
|
151
176
|
Or to **Claude Desktop** (and other clients using the `mcpServers` config
|
|
152
177
|
shape) — `claude_desktop_config.json`:
|
|
153
178
|
|
|
@@ -162,14 +187,26 @@ shape) — `claude_desktop_config.json`:
|
|
|
162
187
|
}
|
|
163
188
|
```
|
|
164
189
|
|
|
165
|
-
Tools:
|
|
166
|
-
|
|
167
|
-
`
|
|
190
|
+
Tools (25):
|
|
191
|
+
|
|
192
|
+
`amend_strategy`, `balance`, `brief_latest`, `buy`, `cancel_all_orders`,
|
|
193
|
+
`cancel_order`, `chat`, `close`, `create_strategy`, `history`,
|
|
194
|
+
`notifications_create`, `open_orders`, `outcomes`, `packets`, `place_order`,
|
|
195
|
+
`portfolio`, `research`, `sell`, `start_strategy`, `stop_strategy`,
|
|
196
|
+
`strategies`, `strategy`, `strategy_activity`, `validate_strategy`, `whoami`.
|
|
197
|
+
|
|
198
|
+
**Safety — read this before pointing an agent at it.** The trading tools
|
|
199
|
+
(`buy` / `sell` / `close` / `create_strategy`) execute with **REAL MONEY by
|
|
200
|
+
default** (`paper=False`). Pass `paper=True` to simulate instead.
|
|
201
|
+
|
|
202
|
+
`buy`, `sell` and `create_strategy` additionally refuse to spend real money
|
|
203
|
+
unless the caller passes `confirm_real_money=True`. `close` deliberately has no
|
|
204
|
+
such gate: a protective exit must never be blocked by a missing flag.
|
|
205
|
+
`place_order` (limit orders, spot, HIP-4 outcome markets) has **no paper mode
|
|
206
|
+
at all** and refuses without `confirm_real_money=True`.
|
|
168
207
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
`paper=False` explicitly to trade real money. Set `TALIS_PAPER=1` in the
|
|
172
|
-
server's environment to force paper mode regardless of tool arguments.
|
|
208
|
+
Set `TALIS_PAPER=1` in the server's environment to force paper mode regardless
|
|
209
|
+
of tool arguments.
|
|
173
210
|
Withdrawals and wallet creation are not exposed over MCP. For a strictly
|
|
174
211
|
read-only agent, log in with `talis login --read-only` — the server rejects
|
|
175
212
|
every mutation at the API layer.
|
|
@@ -103,3 +103,48 @@ class TestInstructionsMatchRealDefaults:
|
|
|
103
103
|
assert "real money" in low and "paper=false" in low, (
|
|
104
104
|
"the instructions must say plainly that the default spends real "
|
|
105
105
|
"money — an agent reads this before it acts")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class TestTheREADMEMatchesTheCode:
|
|
109
|
+
"""The README is what a user reads BEFORE installing, which makes a wrong
|
|
110
|
+
safety claim there worse than one in a docstring.
|
|
111
|
+
|
|
112
|
+
It said "the trading tools default to paper=True — an isolated simulated
|
|
113
|
+
book" while every signature read `paper: bool = False`. That was the SAME
|
|
114
|
+
inverted claim already fixed in the server instructions and the tool
|
|
115
|
+
docstrings — a third copy nobody thought to grep. Asserted against the
|
|
116
|
+
signatures, so prose and code cannot drift apart again.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def _readme(self):
|
|
120
|
+
import pathlib
|
|
121
|
+
return (pathlib.Path(__file__).resolve().parent.parent
|
|
122
|
+
/ "README.md").read_text()
|
|
123
|
+
|
|
124
|
+
def test_readme_does_not_claim_paper_by_default(self):
|
|
125
|
+
low = self._readme().lower()
|
|
126
|
+
assert "default to `paper=true`" not in low
|
|
127
|
+
assert "default to paper=true" not in low
|
|
128
|
+
|
|
129
|
+
def test_readme_states_real_money_is_the_default(self):
|
|
130
|
+
low = self._readme().lower()
|
|
131
|
+
assert "real money" in low and "paper=false" in low
|
|
132
|
+
|
|
133
|
+
def test_readme_matches_the_actual_signature(self):
|
|
134
|
+
import inspect
|
|
135
|
+
|
|
136
|
+
from talis.commands import mcp_server as m
|
|
137
|
+
for tool in ("buy", "sell", "close", "create_strategy"):
|
|
138
|
+
default = inspect.signature(getattr(m, tool)).parameters["paper"].default
|
|
139
|
+
assert default is False, (
|
|
140
|
+
f"{tool} no longer defaults to real money — update README.md "
|
|
141
|
+
"in the same commit")
|
|
142
|
+
|
|
143
|
+
def test_readme_lists_every_tool_the_server_registers(self):
|
|
144
|
+
"""A stale list (13 of 25) hides half the surface from anyone
|
|
145
|
+
evaluating what an agent can reach."""
|
|
146
|
+
from talis.commands.mcp_server import TOOL_FUNCTIONS
|
|
147
|
+
readme = self._readme()
|
|
148
|
+
missing = [f.__name__ for f in TOOL_FUNCTIONS
|
|
149
|
+
if f"`{f.__name__}`" not in readme]
|
|
150
|
+
assert not missing, f"README omits registered tools: {missing}"
|