tetra-cli 0.2.0__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.
- tetra_cli-0.2.0/PKG-INFO +140 -0
- tetra_cli-0.2.0/README.md +125 -0
- tetra_cli-0.2.0/pyproject.toml +44 -0
- tetra_cli-0.2.0/setup.cfg +4 -0
- tetra_cli-0.2.0/tests/test_no_backend_leak.py +52 -0
- tetra_cli-0.2.0/tests/test_smoke.py +6 -0
- tetra_cli-0.2.0/tetra_cli/__init__.py +6 -0
- tetra_cli-0.2.0/tetra_cli/api_client/__init__.py +10 -0
- tetra_cli-0.2.0/tetra_cli/api_client/client.py +173 -0
- tetra_cli-0.2.0/tetra_cli/api_client/config.py +125 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/__init__.py +9 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/accounts.py +303 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/ai.py +278 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/analysis.py +190 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/api_keys.py +145 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/archive.py +114 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/awards.py +123 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/capacity.py +84 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/conversations.py +447 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/conversations_2.py +262 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/cosmetics.py +148 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/dashboard.py +282 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/data.py +250 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/events.py +734 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/gamification.py +470 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/goals.py +1144 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/groups.py +647 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/issues.py +198 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/offset.py +61 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/onboarding.py +284 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/outcome_schemas.py +292 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/peer_connections.py +243 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/plaid.py +329 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/reminders.py +273 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/scratches.py +280 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/skill_trees.py +160 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/social_2.py +560 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/social_3.py +618 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/social_4.py +527 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/strava.py +215 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/stripe.py +113 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/tags.py +488 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/values.py +867 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/values_2.py +584 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/watch.py +105 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/webhooks.py +50 -0
- tetra_cli-0.2.0/tetra_cli/api_client/operations/xp.py +27 -0
- tetra_cli-0.2.0/tetra_cli/cli/__init__.py +5 -0
- tetra_cli-0.2.0/tetra_cli/cli/__main__.py +5 -0
- tetra_cli-0.2.0/tetra_cli/cli/app.py +86 -0
- tetra_cli-0.2.0/tetra_cli/cli/commands/__init__.py +1 -0
- tetra_cli-0.2.0/tetra_cli/cli/commands/auth.py +201 -0
- tetra_cli-0.2.0/tetra_cli/cli/commands/guide.py +8 -0
- tetra_cli-0.2.0/tetra_cli/cli/commands/messages.py +161 -0
- tetra_cli-0.2.0/tetra_cli/cli/commands/skill.py +71 -0
- tetra_cli-0.2.0/tetra_cli/cli/context.py +13 -0
- tetra_cli-0.2.0/tetra_cli/cli/generate.py +282 -0
- tetra_cli-0.2.0/tetra_cli/cli/output.py +58 -0
- tetra_cli-0.2.0/tetra_cli/mcp_gen.py +137 -0
- tetra_cli-0.2.0/tetra_cli/ontology.py +70 -0
- tetra_cli-0.2.0/tetra_cli/registry.py +118 -0
- tetra_cli-0.2.0/tetra_cli/skill/SKILL.md +69 -0
- tetra_cli-0.2.0/tetra_cli/skill/__init__.py +1 -0
- tetra_cli-0.2.0/tetra_cli.egg-info/PKG-INFO +140 -0
- tetra_cli-0.2.0/tetra_cli.egg-info/SOURCES.txt +67 -0
- tetra_cli-0.2.0/tetra_cli.egg-info/dependency_links.txt +1 -0
- tetra_cli-0.2.0/tetra_cli.egg-info/entry_points.txt +2 -0
- tetra_cli-0.2.0/tetra_cli.egg-info/requires.txt +8 -0
- tetra_cli-0.2.0/tetra_cli.egg-info/top_level.txt +1 -0
tetra_cli-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tetra-cli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Standalone CLI/agent client for the Tetra API (no backend).
|
|
5
|
+
Project-URL: Homepage, https://tetraoptum.com
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: httpx>=0.27
|
|
9
|
+
Requires-Dist: typer>=0.12.0
|
|
10
|
+
Requires-Dist: click>=8.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
13
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
14
|
+
Requires-Dist: respx>=0.21; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# Tetra CLI
|
|
17
|
+
|
|
18
|
+
[**Tetra**](https://tetraoptum.com) is an intelligent life organizer — "Tetris
|
|
19
|
+
for time." It helps you align your time and money with what you actually value,
|
|
20
|
+
through hierarchical **values**, **goals**, and **events**, plus the analytics
|
|
21
|
+
to see where it all goes. Learn more or create a free account at
|
|
22
|
+
**[tetraoptum.com](https://tetraoptum.com)**.
|
|
23
|
+
|
|
24
|
+
This package is the standalone command-line client for Tetra — manage your
|
|
25
|
+
values, goals, events, and time from your terminal or from any LLM agent
|
|
26
|
+
(Claude Code, OpenClaw, and other tool-using assistants).
|
|
27
|
+
|
|
28
|
+
It talks to the Tetra API over HTTPS using **your** API key. It has no access
|
|
29
|
+
to the Tetra backend internals — it only does what your account can do.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install tetra-cli
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This installs the `tetra-cli` command. It depends only on public packages
|
|
38
|
+
(`httpx`, `typer`, `click`).
|
|
39
|
+
|
|
40
|
+
## 1. Get an API key
|
|
41
|
+
|
|
42
|
+
In the Tetra web app, go to **Settings → API Keys → Create key**. Choose the
|
|
43
|
+
scopes your agent needs (you can create a read-only key, or grant specific
|
|
44
|
+
writes):
|
|
45
|
+
|
|
46
|
+
| Scope | Lets the agent… |
|
|
47
|
+
|-------|-----------------|
|
|
48
|
+
| `read` | read everything in your account |
|
|
49
|
+
| `events:write` | record / edit / delete events |
|
|
50
|
+
| `goals:write` | create / edit / delete goals |
|
|
51
|
+
| `values:write` | create / edit / delete values |
|
|
52
|
+
| `tags:write` | create / edit / merge tags |
|
|
53
|
+
|
|
54
|
+
The key (`tetra_…`) is shown **once**. Copy it then.
|
|
55
|
+
|
|
56
|
+
> **Keep your API key secret.** It grants the same access as logging in —
|
|
57
|
+
> anyone who has it can act as you, just like your password. Store it in your
|
|
58
|
+
> agent's secrets/credential store, never paste it into shared chats or commit
|
|
59
|
+
> it to git, and revoke it from **Settings → API Keys** if it may have leaked.
|
|
60
|
+
|
|
61
|
+
## 2. Configure the CLI
|
|
62
|
+
|
|
63
|
+
Save the key once (it's stored in `~/.tetra/cli_config.json`):
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
tetra-cli auth login tetra_your_key_here
|
|
67
|
+
# defaults to https://api.tetraoptum.com; pass --url to target another server
|
|
68
|
+
tetra-cli auth status # confirm who you're authenticated as
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Or skip the saved config and use environment variables (handy for agents /
|
|
72
|
+
CI), which always take precedence:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
export TETRA_API_KEY=tetra_your_key_here
|
|
76
|
+
export TETRA_API_URL=http://localhost:8000 # optional; only to target a local backend (defaults to prod)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 3. Install the agent skill (optional)
|
|
80
|
+
|
|
81
|
+
To let an AI coding agent drive Tetra for you, install the bundled skill into
|
|
82
|
+
its skills directory (defaults to Claude Code's `~/.claude/skills`):
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
tetra-cli skill install # -> ~/.claude/skills/tetra-cli/SKILL.md
|
|
86
|
+
tetra-cli skill install --dir /path/to/other/agent/skills # another agent
|
|
87
|
+
tetra-cli skill show # print the skill without installing
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## 4. Use it
|
|
91
|
+
|
|
92
|
+
Goals and values are addressed by their hierarchical **name** — no UID hunting:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
tetra-cli values list
|
|
96
|
+
tetra-cli goals list
|
|
97
|
+
tetra-cli goals get "Exercise > Weight Training"
|
|
98
|
+
tetra-cli events create 2026-06-10 --goal "Exercise > Weight Training" \
|
|
99
|
+
--duration-minutes 45 --description "Bench + accessories"
|
|
100
|
+
tetra-cli events list --on-date 2026-06-10
|
|
101
|
+
tetra-cli dashboard week-snapshot
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Every command supports `--help`. Output is JSON by default (ideal for agents);
|
|
105
|
+
add `--pretty` for human-readable tables.
|
|
106
|
+
|
|
107
|
+
## Full command surface
|
|
108
|
+
|
|
109
|
+
The CLI mirrors **everything you can do in the Tetra web app** — your agent can
|
|
110
|
+
act on your behalf anywhere you can act yourself. Commands are generated from
|
|
111
|
+
the same operation registry the app uses, so the surface never drifts behind
|
|
112
|
+
the product (a parity check in CI fails if a user-facing endpoint has no
|
|
113
|
+
command). Run `tetra-cli --help` to browse the groups:
|
|
114
|
+
|
|
115
|
+
| Group | What it covers |
|
|
116
|
+
|-------|----------------|
|
|
117
|
+
| `values`, `goals`, `events`, `tags` | the core model: create/read/update/delete, hierarchy moves, metrics, bulk edits |
|
|
118
|
+
| `dashboard`, `analysis`, `data` | week/day analytics, time & spending breakdowns, full export & CSV import |
|
|
119
|
+
| `scratches`, `notifications` | quick capture, one-shot reminders |
|
|
120
|
+
| `gamification`, `awards`, `xp`, `capacity` | XP/level/streak, credits, gems, item shop, slot capacity |
|
|
121
|
+
| `social`, `peers`, `conversations` | groups, sharing, Arena cohorts, connections, DMs & the AI thread |
|
|
122
|
+
| `outcome-schemas`, `skill-trees` | reusable metric & tree templates |
|
|
123
|
+
| `accounts`, `api-keys`, `onboarding` | your profile, key management, onboarding |
|
|
124
|
+
| `archive`, `stripe` | restore soft-deleted items, manage your subscription |
|
|
125
|
+
| `strava`, `plaid`, `issues` | activity & bank integrations, bug reports |
|
|
126
|
+
|
|
127
|
+
Admin/superuser endpoints are intentionally excluded — a key only ever reaches
|
|
128
|
+
your own account's data.
|
|
129
|
+
|
|
130
|
+
## Using it from an LLM agent
|
|
131
|
+
|
|
132
|
+
Point your agent's shell at the `tetra-cli` binary with `TETRA_API_KEY` set in
|
|
133
|
+
its environment. The agent can then drive data entry, edits, and analysis on
|
|
134
|
+
your account. For a Model-Context-Protocol integration (Claude Desktop, etc.),
|
|
135
|
+
see the companion **tetra-mcp** package, which reads the same
|
|
136
|
+
`~/.tetra/cli_config.json`.
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Tetra CLI
|
|
2
|
+
|
|
3
|
+
[**Tetra**](https://tetraoptum.com) is an intelligent life organizer — "Tetris
|
|
4
|
+
for time." It helps you align your time and money with what you actually value,
|
|
5
|
+
through hierarchical **values**, **goals**, and **events**, plus the analytics
|
|
6
|
+
to see where it all goes. Learn more or create a free account at
|
|
7
|
+
**[tetraoptum.com](https://tetraoptum.com)**.
|
|
8
|
+
|
|
9
|
+
This package is the standalone command-line client for Tetra — manage your
|
|
10
|
+
values, goals, events, and time from your terminal or from any LLM agent
|
|
11
|
+
(Claude Code, OpenClaw, and other tool-using assistants).
|
|
12
|
+
|
|
13
|
+
It talks to the Tetra API over HTTPS using **your** API key. It has no access
|
|
14
|
+
to the Tetra backend internals — it only does what your account can do.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install tetra-cli
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This installs the `tetra-cli` command. It depends only on public packages
|
|
23
|
+
(`httpx`, `typer`, `click`).
|
|
24
|
+
|
|
25
|
+
## 1. Get an API key
|
|
26
|
+
|
|
27
|
+
In the Tetra web app, go to **Settings → API Keys → Create key**. Choose the
|
|
28
|
+
scopes your agent needs (you can create a read-only key, or grant specific
|
|
29
|
+
writes):
|
|
30
|
+
|
|
31
|
+
| Scope | Lets the agent… |
|
|
32
|
+
|-------|-----------------|
|
|
33
|
+
| `read` | read everything in your account |
|
|
34
|
+
| `events:write` | record / edit / delete events |
|
|
35
|
+
| `goals:write` | create / edit / delete goals |
|
|
36
|
+
| `values:write` | create / edit / delete values |
|
|
37
|
+
| `tags:write` | create / edit / merge tags |
|
|
38
|
+
|
|
39
|
+
The key (`tetra_…`) is shown **once**. Copy it then.
|
|
40
|
+
|
|
41
|
+
> **Keep your API key secret.** It grants the same access as logging in —
|
|
42
|
+
> anyone who has it can act as you, just like your password. Store it in your
|
|
43
|
+
> agent's secrets/credential store, never paste it into shared chats or commit
|
|
44
|
+
> it to git, and revoke it from **Settings → API Keys** if it may have leaked.
|
|
45
|
+
|
|
46
|
+
## 2. Configure the CLI
|
|
47
|
+
|
|
48
|
+
Save the key once (it's stored in `~/.tetra/cli_config.json`):
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
tetra-cli auth login tetra_your_key_here
|
|
52
|
+
# defaults to https://api.tetraoptum.com; pass --url to target another server
|
|
53
|
+
tetra-cli auth status # confirm who you're authenticated as
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Or skip the saved config and use environment variables (handy for agents /
|
|
57
|
+
CI), which always take precedence:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
export TETRA_API_KEY=tetra_your_key_here
|
|
61
|
+
export TETRA_API_URL=http://localhost:8000 # optional; only to target a local backend (defaults to prod)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 3. Install the agent skill (optional)
|
|
65
|
+
|
|
66
|
+
To let an AI coding agent drive Tetra for you, install the bundled skill into
|
|
67
|
+
its skills directory (defaults to Claude Code's `~/.claude/skills`):
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
tetra-cli skill install # -> ~/.claude/skills/tetra-cli/SKILL.md
|
|
71
|
+
tetra-cli skill install --dir /path/to/other/agent/skills # another agent
|
|
72
|
+
tetra-cli skill show # print the skill without installing
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 4. Use it
|
|
76
|
+
|
|
77
|
+
Goals and values are addressed by their hierarchical **name** — no UID hunting:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
tetra-cli values list
|
|
81
|
+
tetra-cli goals list
|
|
82
|
+
tetra-cli goals get "Exercise > Weight Training"
|
|
83
|
+
tetra-cli events create 2026-06-10 --goal "Exercise > Weight Training" \
|
|
84
|
+
--duration-minutes 45 --description "Bench + accessories"
|
|
85
|
+
tetra-cli events list --on-date 2026-06-10
|
|
86
|
+
tetra-cli dashboard week-snapshot
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Every command supports `--help`. Output is JSON by default (ideal for agents);
|
|
90
|
+
add `--pretty` for human-readable tables.
|
|
91
|
+
|
|
92
|
+
## Full command surface
|
|
93
|
+
|
|
94
|
+
The CLI mirrors **everything you can do in the Tetra web app** — your agent can
|
|
95
|
+
act on your behalf anywhere you can act yourself. Commands are generated from
|
|
96
|
+
the same operation registry the app uses, so the surface never drifts behind
|
|
97
|
+
the product (a parity check in CI fails if a user-facing endpoint has no
|
|
98
|
+
command). Run `tetra-cli --help` to browse the groups:
|
|
99
|
+
|
|
100
|
+
| Group | What it covers |
|
|
101
|
+
|-------|----------------|
|
|
102
|
+
| `values`, `goals`, `events`, `tags` | the core model: create/read/update/delete, hierarchy moves, metrics, bulk edits |
|
|
103
|
+
| `dashboard`, `analysis`, `data` | week/day analytics, time & spending breakdowns, full export & CSV import |
|
|
104
|
+
| `scratches`, `notifications` | quick capture, one-shot reminders |
|
|
105
|
+
| `gamification`, `awards`, `xp`, `capacity` | XP/level/streak, credits, gems, item shop, slot capacity |
|
|
106
|
+
| `social`, `peers`, `conversations` | groups, sharing, Arena cohorts, connections, DMs & the AI thread |
|
|
107
|
+
| `outcome-schemas`, `skill-trees` | reusable metric & tree templates |
|
|
108
|
+
| `accounts`, `api-keys`, `onboarding` | your profile, key management, onboarding |
|
|
109
|
+
| `archive`, `stripe` | restore soft-deleted items, manage your subscription |
|
|
110
|
+
| `strava`, `plaid`, `issues` | activity & bank integrations, bug reports |
|
|
111
|
+
|
|
112
|
+
Admin/superuser endpoints are intentionally excluded — a key only ever reaches
|
|
113
|
+
your own account's data.
|
|
114
|
+
|
|
115
|
+
## Using it from an LLM agent
|
|
116
|
+
|
|
117
|
+
Point your agent's shell at the `tetra-cli` binary with `TETRA_API_KEY` set in
|
|
118
|
+
its environment. The agent can then drive data entry, edits, and analysis on
|
|
119
|
+
your account. For a Model-Context-Protocol integration (Claude Desktop, etc.),
|
|
120
|
+
see the companion **tetra-mcp** package, which reads the same
|
|
121
|
+
`~/.tetra/cli_config.json`.
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools >= 77.0.3"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tetra-cli"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Standalone CLI/agent client for the Tetra API (no backend)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"httpx>=0.27",
|
|
13
|
+
"typer>=0.12.0",
|
|
14
|
+
# Imported directly (click.Choice in cli/generate.py). typer is built on
|
|
15
|
+
# click but newer typer releases no longer re-export it as a hard dep, so
|
|
16
|
+
# declare it explicitly — otherwise a clean install fails at import time.
|
|
17
|
+
"click>=8.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://tetraoptum.com"
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = [
|
|
25
|
+
"pytest>=8.0.0",
|
|
26
|
+
"pytest-asyncio>=0.23.0",
|
|
27
|
+
"respx>=0.21",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
tetra-cli = "tetra_cli.cli.app:main"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
where = ["."]
|
|
35
|
+
include = ["tetra_cli*"]
|
|
36
|
+
|
|
37
|
+
# Ship the bundled agent skill (SKILL.md) in the wheel so `pip install tetra-cli`
|
|
38
|
+
# delivers the client AND the instructions that teach an AI to drive it.
|
|
39
|
+
[tool.setuptools.package-data]
|
|
40
|
+
"tetra_cli.skill" = ["*.md"]
|
|
41
|
+
|
|
42
|
+
[tool.pytest.ini_options]
|
|
43
|
+
pythonpath = ["."]
|
|
44
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""tetra_cli must never import the backend or heavy server deps.
|
|
2
|
+
|
|
3
|
+
This is the distribution invariant: the slim package is shipped to agents
|
|
4
|
+
and must contain/pull in NO backend (models, routes, services, ORM). The
|
|
5
|
+
guard imports every tetra_cli submodule, then asserts no forbidden module
|
|
6
|
+
got loaded into sys.modules.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib
|
|
11
|
+
import pkgutil
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
import tetra_cli
|
|
15
|
+
|
|
16
|
+
FORBIDDEN_PREFIXES = (
|
|
17
|
+
"tetra.api",
|
|
18
|
+
"tetra.core",
|
|
19
|
+
"tetra.services",
|
|
20
|
+
"tetra.db",
|
|
21
|
+
"sqlalchemy",
|
|
22
|
+
"fastapi",
|
|
23
|
+
"psycopg2",
|
|
24
|
+
"alembic",
|
|
25
|
+
"stripe",
|
|
26
|
+
"redis",
|
|
27
|
+
"llama_index",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _all_tetra_cli_modules() -> list[str]:
|
|
32
|
+
mods = ["tetra_cli"]
|
|
33
|
+
for info in pkgutil.walk_packages(tetra_cli.__path__, prefix="tetra_cli."):
|
|
34
|
+
mods.append(info.name)
|
|
35
|
+
return mods
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_importing_tetra_cli_pulls_in_no_backend():
|
|
39
|
+
# Drop anything forbidden already loaded by the test session, so we measure
|
|
40
|
+
# only what tetra_cli imports trigger.
|
|
41
|
+
for name in list(sys.modules):
|
|
42
|
+
if name.startswith(FORBIDDEN_PREFIXES):
|
|
43
|
+
del sys.modules[name]
|
|
44
|
+
|
|
45
|
+
for mod in _all_tetra_cli_modules():
|
|
46
|
+
importlib.import_module(mod)
|
|
47
|
+
|
|
48
|
+
leaked = sorted(
|
|
49
|
+
name for name in sys.modules
|
|
50
|
+
if name.startswith(FORBIDDEN_PREFIXES)
|
|
51
|
+
)
|
|
52
|
+
assert leaked == [], f"tetra_cli leaked backend/heavy imports: {leaked}"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Shared HTTP client and operations for the Tetra API.
|
|
2
|
+
|
|
3
|
+
TetraClient/TetraConfig live in .client/.config (this package — no backend).
|
|
4
|
+
The operations package is the single source of truth for HTTP path, body
|
|
5
|
+
shape, and parameter conversions (e.g. duration_minutes -> seconds).
|
|
6
|
+
"""
|
|
7
|
+
from tetra_cli.api_client.client import TetraClient, TetraClientError
|
|
8
|
+
from tetra_cli.api_client.config import TetraConfig
|
|
9
|
+
|
|
10
|
+
__all__ = ["TetraClient", "TetraClientError", "TetraConfig"]
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""HTTP client wrapper for the Tetra API.
|
|
2
|
+
|
|
3
|
+
Handles authentication headers, error normalization, and response parsing.
|
|
4
|
+
All MCP tools use this client instead of making raw httpx calls.
|
|
5
|
+
"""
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from tetra_cli.api_client.config import TetraConfig
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TetraClientError(Exception):
|
|
17
|
+
"""Error from the Tetra API, formatted for MCP tool output."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, message: str, status_code: int = 0):
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.status_code = status_code
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TetraClient:
|
|
25
|
+
"""Async HTTP client for the Tetra REST API."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, config: TetraConfig):
|
|
28
|
+
self._config = config
|
|
29
|
+
self._client = httpx.AsyncClient(
|
|
30
|
+
base_url=config.api_url,
|
|
31
|
+
headers={
|
|
32
|
+
"Authorization": f"Bearer {config.token}",
|
|
33
|
+
# The CLI/MCP surface is agent-driven by definition. The backend
|
|
34
|
+
# honors this header in ``is_agent_request`` so created/updated
|
|
35
|
+
# entities get ``agent_touched=True`` regardless of whether the
|
|
36
|
+
# token is a real API key or a JWT minted via ``dev_token.py``.
|
|
37
|
+
"X-Tetra-Agent": "1",
|
|
38
|
+
},
|
|
39
|
+
timeout=30.0,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
async def close(self) -> None:
|
|
43
|
+
"""Close the underlying HTTP client."""
|
|
44
|
+
await self._client.aclose()
|
|
45
|
+
|
|
46
|
+
async def get(self, path: str, params: dict[str, Any] | None = None) -> Any:
|
|
47
|
+
"""GET request with error handling."""
|
|
48
|
+
response = await self._client.get(path, params=_clean_params(params))
|
|
49
|
+
return _handle_response(response)
|
|
50
|
+
|
|
51
|
+
async def post(self, path: str, json: dict[str, Any] | None = None) -> Any:
|
|
52
|
+
"""POST request with error handling."""
|
|
53
|
+
response = await self._client.post(path, json=json)
|
|
54
|
+
return _handle_response(response)
|
|
55
|
+
|
|
56
|
+
async def put(self, path: str, json: dict[str, Any] | None = None) -> Any:
|
|
57
|
+
"""PUT request with error handling."""
|
|
58
|
+
response = await self._client.put(path, json=json)
|
|
59
|
+
return _handle_response(response)
|
|
60
|
+
|
|
61
|
+
async def patch(self, path: str, json: dict[str, Any] | None = None) -> Any:
|
|
62
|
+
"""PATCH request with error handling."""
|
|
63
|
+
response = await self._client.patch(path, json=json)
|
|
64
|
+
return _handle_response(response)
|
|
65
|
+
|
|
66
|
+
async def delete(self, path: str) -> Any:
|
|
67
|
+
"""DELETE request with error handling."""
|
|
68
|
+
response = await self._client.delete(path)
|
|
69
|
+
return _handle_response(response)
|
|
70
|
+
|
|
71
|
+
async def download(
|
|
72
|
+
self, path: str, params: dict[str, Any] | None = None
|
|
73
|
+
) -> tuple[bytes, str]:
|
|
74
|
+
"""GET a binary/file response. Returns ``(content_bytes, filename)``.
|
|
75
|
+
|
|
76
|
+
Used by ``output="file"`` ops whose endpoints return binary or
|
|
77
|
+
non-JSON bodies (zip, CSV, iCal). Unlike :meth:`get`, this never
|
|
78
|
+
JSON-decodes the response. On a non-2xx status it delegates to
|
|
79
|
+
:func:`_handle_response`, which raises :class:`TetraClientError`
|
|
80
|
+
(and is guarded against binary error bodies).
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
path: API path to GET.
|
|
84
|
+
params: Optional query params (None values are dropped).
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
A tuple of the raw response bytes and a best-effort filename
|
|
88
|
+
derived from the ``Content-Disposition`` header, the last path
|
|
89
|
+
segment, or ``"download"``.
|
|
90
|
+
"""
|
|
91
|
+
response = await self._client.get(path, params=_clean_params(params))
|
|
92
|
+
if not (200 <= response.status_code < 300):
|
|
93
|
+
# Reuse the non-2xx error path; it always raises TetraClientError.
|
|
94
|
+
_handle_response(response)
|
|
95
|
+
content = response.content
|
|
96
|
+
filename = (
|
|
97
|
+
_filename_from_disposition(response.headers.get("content-disposition"))
|
|
98
|
+
or path.rstrip("/").split("/")[-1]
|
|
99
|
+
or "download"
|
|
100
|
+
)
|
|
101
|
+
return content, filename
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _filename_from_disposition(value: str | None) -> str | None:
|
|
105
|
+
"""Extract the filename from a ``Content-Disposition`` header value.
|
|
106
|
+
|
|
107
|
+
Parses ``attachment; filename=foo.zip`` (and the quoted
|
|
108
|
+
``filename="foo.zip"`` variant). Returns None when the header is absent
|
|
109
|
+
or carries no ``filename`` directive.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
value: The raw ``Content-Disposition`` header value, or None.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
The unquoted filename, or None.
|
|
116
|
+
"""
|
|
117
|
+
if not value:
|
|
118
|
+
return None
|
|
119
|
+
for part in value.split(";"):
|
|
120
|
+
part = part.strip()
|
|
121
|
+
if part.lower().startswith("filename="):
|
|
122
|
+
name = part[len("filename="):].strip().strip('"')
|
|
123
|
+
return name or None
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _clean_params(params: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
128
|
+
"""Remove None values from query parameters."""
|
|
129
|
+
if params is None:
|
|
130
|
+
return None
|
|
131
|
+
return {k: v for k, v in params.items() if v is not None}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _handle_response(response: httpx.Response) -> Any:
|
|
135
|
+
"""Parse response and raise clear errors for non-2xx status codes."""
|
|
136
|
+
if response.status_code == 204:
|
|
137
|
+
return {"deleted": True}
|
|
138
|
+
|
|
139
|
+
if 200 <= response.status_code < 300:
|
|
140
|
+
return response.json()
|
|
141
|
+
|
|
142
|
+
# Extract error detail from response body. Guard every access: a binary
|
|
143
|
+
# error body (e.g. an upstream returning a zip on failure) must not crash
|
|
144
|
+
# the error path itself, so fall back to text, then to the status code.
|
|
145
|
+
try:
|
|
146
|
+
body = response.json()
|
|
147
|
+
detail = body.get("detail", str(body))
|
|
148
|
+
except Exception:
|
|
149
|
+
try:
|
|
150
|
+
detail = response.text or f"HTTP {response.status_code}"
|
|
151
|
+
except Exception:
|
|
152
|
+
detail = f"HTTP {response.status_code}"
|
|
153
|
+
|
|
154
|
+
status = response.status_code
|
|
155
|
+
|
|
156
|
+
if status == 401:
|
|
157
|
+
raise TetraClientError(
|
|
158
|
+
"Authentication failed. Check TETRA_API_KEY or TETRA_USERNAME.", status
|
|
159
|
+
)
|
|
160
|
+
if status == 403:
|
|
161
|
+
raise TetraClientError(f"Permission denied: {detail}", status)
|
|
162
|
+
if status == 404:
|
|
163
|
+
raise TetraClientError(f"Not found: {detail}", status)
|
|
164
|
+
if status == 400:
|
|
165
|
+
raise TetraClientError(f"Invalid request: {detail}", status)
|
|
166
|
+
if status == 422:
|
|
167
|
+
raise TetraClientError(f"Validation error: {detail}", status)
|
|
168
|
+
if status >= 500:
|
|
169
|
+
raise TetraClientError(
|
|
170
|
+
f"Server error (HTTP {status}). Is the Tetra backend running?", status
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
raise TetraClientError(f"Unexpected HTTP {status}: {detail}", status)
|