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.
Files changed (74) hide show
  1. {talis_cli-0.2.0 → talis_cli-0.2.1}/PKG-INFO +46 -9
  2. {talis_cli-0.2.0 → talis_cli-0.2.1}/README.md +45 -8
  3. {talis_cli-0.2.0 → talis_cli-0.2.1}/pyproject.toml +1 -1
  4. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/cli.py +66 -1
  5. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/mcp_server.py +63 -15
  6. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis_cli.egg-info/PKG-INFO +46 -9
  7. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_mcp_docstring_contract.py +45 -0
  8. talis_cli-0.2.1/tests/test_mcp_session_expiry.py +276 -0
  9. talis_cli-0.2.1/tests/test_session_autorenew.py +286 -0
  10. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_version_sync.py +6 -1
  11. talis_cli-0.2.0/tests/test_mcp_session_expiry.py +0 -124
  12. talis_cli-0.2.0/tests/test_session_autorenew.py +0 -141
  13. {talis_cli-0.2.0 → talis_cli-0.2.1}/setup.cfg +0 -0
  14. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/__init__.py +0 -0
  15. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/_eip55.py +0 -0
  16. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/api.py +0 -0
  17. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/__init__.py +0 -0
  18. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/_shared.py +0 -0
  19. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/auth.py +0 -0
  20. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/brief.py +0 -0
  21. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/chat.py +0 -0
  22. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/fund.py +0 -0
  23. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/guide.py +0 -0
  24. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/history.py +0 -0
  25. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/notifications.py +0 -0
  26. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/outcome.py +0 -0
  27. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/portfolio.py +0 -0
  28. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/research.py +0 -0
  29. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/snapshot.py +0 -0
  30. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/strategies.py +0 -0
  31. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/test_tenant.py +0 -0
  32. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/trade.py +0 -0
  33. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/wait.py +0 -0
  34. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/commands/withdraw.py +0 -0
  35. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/config.py +0 -0
  36. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/output.py +0 -0
  37. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/paper.py +0 -0
  38. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/spot.py +0 -0
  39. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis/symbols.py +0 -0
  40. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis_cli.egg-info/SOURCES.txt +0 -0
  41. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis_cli.egg-info/dependency_links.txt +0 -0
  42. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis_cli.egg-info/entry_points.txt +0 -0
  43. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis_cli.egg-info/requires.txt +0 -0
  44. {talis_cli-0.2.0 → talis_cli-0.2.1}/talis_cli.egg-info/top_level.txt +0 -0
  45. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_api.py +0 -0
  46. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_api_key_auth.py +0 -0
  47. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_brief.py +0 -0
  48. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_chat.py +0 -0
  49. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_commands.py +0 -0
  50. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_config.py +0 -0
  51. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_confirm_prompt.py +0 -0
  52. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_eip55.py +0 -0
  53. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_guide.py +0 -0
  54. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_history.py +0 -0
  55. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_login_flow.py +0 -0
  56. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_loopback_login.py +0 -0
  57. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_mcp.py +0 -0
  58. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_mcp_balance_contract.py +0 -0
  59. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_mcp_mode_contract.py +0 -0
  60. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_mcp_sdk_compat.py +0 -0
  61. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_mcp_strategy_tools.py +0 -0
  62. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_non_interactive_auth.py +0 -0
  63. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_notifications.py +0 -0
  64. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_outcome.py +0 -0
  65. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_paper_mode.py +0 -0
  66. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_research.py +0 -0
  67. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_signup.py +0 -0
  68. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_snapshot_diff.py +0 -0
  69. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_spot.py +0 -0
  70. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_spot_gateway_first.py +0 -0
  71. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_strategies_create.py +0 -0
  72. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_test_tenant.py +0 -0
  73. {talis_cli-0.2.0 → talis_cli-0.2.1}/tests/test_wait.py +0 -0
  74. {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.0
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
- Add it to **Claude Code**:
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: `whoami`, `portfolio`, `balance`, `research`, `packets`,
166
- `brief_latest`, `history`, `buy`, `sell`, `close`, `create_strategy`, `chat`,
167
- `notifications_create`.
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
- **Safety**: the trading tools (`buy` / `sell` / `close` / `create_strategy`)
170
- default to `paper=True` — an isolated simulated book. An agent must pass
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
- Add it to **Claude Code**:
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: `whoami`, `portfolio`, `balance`, `research`, `packets`,
138
- `brief_latest`, `history`, `buy`, `sell`, `close`, `create_strategy`, `chat`,
139
- `notifications_create`.
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
- **Safety**: the trading tools (`buy` / `sell` / `close` / `create_strategy`)
142
- default to `paper=True` — an isolated simulated book. An agent must pass
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.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "talis-cli"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "Command-line client for the Talis trading platform. Sign in via device flow, manage sessions, view portfolio, place orders."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -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 comfortably alive, else a loud actionable dict."""
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. Renew with "
88
- "`talis_token_refresh.py --force` (admin key), or a human "
89
- "can run `talis login`.")}
90
- return {"session_hours_remaining": round(hours, 1),
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}). The token itself carries no "
93
- "refresh token, but tools/talis-cli/talis_token_refresh.py "
94
- "re-mints it via POST /auth/tokens and is installed as a "
95
- "LaunchAgent (com.talis.token-refresh, every 6h, renews under "
96
- "8h remaining). If this warning keeps counting down below 8h, "
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
- config.ensure_fresh(creds)
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
- pass
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.0
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
- Add it to **Claude Code**:
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: `whoami`, `portfolio`, `balance`, `research`, `packets`,
166
- `brief_latest`, `history`, `buy`, `sell`, `close`, `create_strategy`, `chat`,
167
- `notifications_create`.
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
- **Safety**: the trading tools (`buy` / `sell` / `close` / `create_strategy`)
170
- default to `paper=True` — an isolated simulated book. An agent must pass
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}"