tarnished-cli 0.1.2__tar.gz → 0.1.4__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.
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/PKG-INFO +16 -3
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/README.md +15 -2
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/pyproject.toml +1 -1
- tarnished_cli-0.1.4/skills/tarnished-cli/SKILL.md +171 -0
- tarnished_cli-0.1.4/skills/tarnished-cli/evals/trigger-eval.json +82 -0
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/auth_storage.py +3 -13
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/client.py +16 -105
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/admin.py +10 -1
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/analytics.py +10 -1
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/applications.py +25 -14
- tarnished_cli-0.1.4/src/tarnished_cli/commands/auth.py +56 -0
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/exports.py +1 -1
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/imports.py +4 -4
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/job_leads.py +17 -8
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/profile.py +4 -2
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/round_types.py +4 -4
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/rounds.py +13 -11
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/statuses.py +6 -4
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/user_settings.py +2 -2
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/main.py +14 -1
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/state.py +1 -17
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_admin_commands.py +5 -5
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_analytics_commands.py +2 -2
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_applications_commands.py +15 -15
- tarnished_cli-0.1.4/tests/test_auth_commands.py +67 -0
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_auth_storage.py +10 -13
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_client.py +15 -36
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_dashboard_commands.py +1 -1
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_export_commands.py +2 -2
- tarnished_cli-0.1.4/tests/test_help_surfaces.py +69 -0
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_import_commands.py +4 -4
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_job_leads_commands.py +8 -8
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_profile_commands.py +4 -4
- tarnished_cli-0.1.4/tests/test_repo_skill.py +76 -0
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_round_types_commands.py +4 -4
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_rounds_commands.py +10 -10
- tarnished_cli-0.1.4/tests/test_skill_eval_fixture.py +31 -0
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_statuses_commands.py +8 -8
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_user_settings_commands.py +3 -3
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/uv.lock +1 -1
- tarnished_cli-0.1.2/src/tarnished_cli/commands/auth.py +0 -152
- tarnished_cli-0.1.2/tests/test_auth_commands.py +0 -220
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/.gitignore +0 -0
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/__init__.py +0 -0
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/__init__.py +0 -0
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/dashboard.py +0 -0
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/preferences.py +0 -0
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/config.py +0 -0
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/input.py +0 -0
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/models/__init__.py +0 -0
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/models/requests.py +0 -0
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/output.py +0 -0
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/conftest.py +0 -0
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_cli_config.py +0 -0
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_main.py +0 -0
- {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_output_contract.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tarnished-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Agent-first CLI for Tarnished
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Requires-Dist: email-validator>=2.0.0
|
|
@@ -28,10 +28,23 @@ Alternative:
|
|
|
28
28
|
pipx install tarnished-cli
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
Homebrew convenience path:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
brew tap markoonakic/tap
|
|
35
|
+
brew install tarnished-cli
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
For agents and automation, keep using:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv tool install tarnished-cli
|
|
42
|
+
```
|
|
43
|
+
|
|
31
44
|
Install from a built wheel before PyPI publication:
|
|
32
45
|
|
|
33
46
|
```bash
|
|
34
|
-
uv tool install ./dist/tarnished_cli-0.1.
|
|
47
|
+
uv tool install ./dist/tarnished_cli-0.1.4-py3-none-any.whl
|
|
35
48
|
```
|
|
36
49
|
|
|
37
50
|
## OpenClaw / Agent Install
|
|
@@ -45,7 +58,7 @@ uv tool install tarnished-cli
|
|
|
45
58
|
Version-pinned:
|
|
46
59
|
|
|
47
60
|
```bash
|
|
48
|
-
uv tool install 'tarnished-cli==0.1.
|
|
61
|
+
uv tool install 'tarnished-cli==0.1.4'
|
|
49
62
|
```
|
|
50
63
|
|
|
51
64
|
Then run:
|
|
@@ -16,10 +16,23 @@ Alternative:
|
|
|
16
16
|
pipx install tarnished-cli
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
Homebrew convenience path:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
brew tap markoonakic/tap
|
|
23
|
+
brew install tarnished-cli
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
For agents and automation, keep using:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uv tool install tarnished-cli
|
|
30
|
+
```
|
|
31
|
+
|
|
19
32
|
Install from a built wheel before PyPI publication:
|
|
20
33
|
|
|
21
34
|
```bash
|
|
22
|
-
uv tool install ./dist/tarnished_cli-0.1.
|
|
35
|
+
uv tool install ./dist/tarnished_cli-0.1.4-py3-none-any.whl
|
|
23
36
|
```
|
|
24
37
|
|
|
25
38
|
## OpenClaw / Agent Install
|
|
@@ -33,7 +46,7 @@ uv tool install tarnished-cli
|
|
|
33
46
|
Version-pinned:
|
|
34
47
|
|
|
35
48
|
```bash
|
|
36
|
-
uv tool install 'tarnished-cli==0.1.
|
|
49
|
+
uv tool install 'tarnished-cli==0.1.4'
|
|
37
50
|
```
|
|
38
51
|
|
|
39
52
|
Then run:
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tarnished-cli
|
|
3
|
+
description: Use this skill whenever the user wants to operate Tarnished through the CLI, inspect or change applications, job leads, rounds, profile, settings, analytics, import/export, or admin data, or automate Tarnished from an agent. Prefer this skill even for general Tarnished operational requests if the work should happen through the `tarnished` command instead of the browser UI.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Tarnished CLI
|
|
7
|
+
|
|
8
|
+
Use the Tarnished CLI as the primary interface to Tarnished's server-backed product surface.
|
|
9
|
+
|
|
10
|
+
The CLI covers the full server-backed Tarnished surface:
|
|
11
|
+
|
|
12
|
+
- `auth`
|
|
13
|
+
- `applications`
|
|
14
|
+
- `job-leads`
|
|
15
|
+
- `dashboard`
|
|
16
|
+
- `analytics`
|
|
17
|
+
- `statuses`
|
|
18
|
+
- `round-types`
|
|
19
|
+
- `rounds`
|
|
20
|
+
- `profile`
|
|
21
|
+
- `preferences`
|
|
22
|
+
- `user-settings`
|
|
23
|
+
- `export`
|
|
24
|
+
- `import`
|
|
25
|
+
- `admin`
|
|
26
|
+
|
|
27
|
+
The CLI covers server-backed data/actions from the product. It does not reproduce browser-rendered presentation or browser-only behavior such as charts, modals, page navigation, extension popups, or DOM automation.
|
|
28
|
+
|
|
29
|
+
## Preflight
|
|
30
|
+
|
|
31
|
+
1. Check the CLI version:
|
|
32
|
+
- `tarnished --version`
|
|
33
|
+
2. Check session state:
|
|
34
|
+
- `tarnished auth status --json`
|
|
35
|
+
- if not authenticated, use `tarnished auth --help` and run the appropriate `login` or `register` flow
|
|
36
|
+
3. If the target environment is ambiguous, prefer explicit targeting:
|
|
37
|
+
- `--profile`
|
|
38
|
+
- `--base-url`
|
|
39
|
+
|
|
40
|
+
## Operating Defaults
|
|
41
|
+
|
|
42
|
+
- prefer `--json` for agent use.
|
|
43
|
+
- prefer explicit `--profile` when more than one Tarnished environment may exist.
|
|
44
|
+
- Prefer explicit `--base-url` when operating against a one-off instance.
|
|
45
|
+
- Prefer structured `--body-file` JSON for writes instead of constructing many ad hoc flags.
|
|
46
|
+
- Add `--yes` for destructive commands.
|
|
47
|
+
- Use only when you need privileged access.
|
|
48
|
+
This applies to `tarnished admin ...`.
|
|
49
|
+
|
|
50
|
+
## Core Workflows
|
|
51
|
+
|
|
52
|
+
### Read Data
|
|
53
|
+
|
|
54
|
+
Use read commands with `--json` whenever the output will be consumed by an agent.
|
|
55
|
+
|
|
56
|
+
Common starting points:
|
|
57
|
+
|
|
58
|
+
- Applications:
|
|
59
|
+
- `tarnished applications list --json`
|
|
60
|
+
- `tarnished statuses list --json`
|
|
61
|
+
- use `tarnished statuses list --json` before filtering applications by `status_id`
|
|
62
|
+
- `tarnished applications get <application-id> --json`
|
|
63
|
+
- Job leads:
|
|
64
|
+
- `tarnished job-leads list --json`
|
|
65
|
+
- `tarnished job-leads get <job-lead-id> --json`
|
|
66
|
+
- Dashboard and analytics:
|
|
67
|
+
- `tarnished dashboard summary --json`
|
|
68
|
+
- `tarnished analytics kpis --period 30d --json`
|
|
69
|
+
- `tarnished analytics weekly --period 30d --json`
|
|
70
|
+
- `tarnished analytics sankey --json`
|
|
71
|
+
- Profile and settings:
|
|
72
|
+
- `tarnished auth whoami --json`
|
|
73
|
+
- `tarnished auth api-key show --json`
|
|
74
|
+
- `tarnished profile get --json`
|
|
75
|
+
- `tarnished dashboard summary --json`
|
|
76
|
+
- `tarnished user-settings get --json`
|
|
77
|
+
- `tarnished preferences get --json`
|
|
78
|
+
|
|
79
|
+
If you need exact flags for a command group, use:
|
|
80
|
+
|
|
81
|
+
- `tarnished <command> --help`
|
|
82
|
+
|
|
83
|
+
### Write Data
|
|
84
|
+
|
|
85
|
+
For create and update flows, prefer checked-in or generated JSON body files.
|
|
86
|
+
|
|
87
|
+
Examples:
|
|
88
|
+
|
|
89
|
+
- Create an application:
|
|
90
|
+
- `tarnished applications create --body-file create-application.json`
|
|
91
|
+
- Extract and create an application from source text or URL:
|
|
92
|
+
- `tarnished applications extract --body-file extract-application.json`
|
|
93
|
+
- Create a job lead:
|
|
94
|
+
- `tarnished job-leads create --body-file create-job-lead.json`
|
|
95
|
+
- Convert a lead into an application:
|
|
96
|
+
- `tarnished job-leads convert <job-lead-id>`
|
|
97
|
+
- Update a profile:
|
|
98
|
+
- `tarnished profile update --body-file profile-update.json`
|
|
99
|
+
|
|
100
|
+
When changing data, keep commands explicit and avoid relying on interactive behavior.
|
|
101
|
+
|
|
102
|
+
### Destructive Actions
|
|
103
|
+
|
|
104
|
+
Destructive commands require deliberate confirmation.
|
|
105
|
+
|
|
106
|
+
Examples:
|
|
107
|
+
|
|
108
|
+
- `tarnished applications delete <application-id> --yes`
|
|
109
|
+
- `tarnished job-leads delete <job-lead-id> --yes`
|
|
110
|
+
- `tarnished admin users delete <user-id> --yes`
|
|
111
|
+
|
|
112
|
+
Do not omit `--yes` in automated flows.
|
|
113
|
+
|
|
114
|
+
### Admin Surface
|
|
115
|
+
|
|
116
|
+
Treat `admin` as privileged.
|
|
117
|
+
|
|
118
|
+
Use it only when the task genuinely requires administrative access.
|
|
119
|
+
|
|
120
|
+
Common admin reads:
|
|
121
|
+
|
|
122
|
+
- `tarnished admin stats --json`
|
|
123
|
+
- `tarnished admin users list --json`
|
|
124
|
+
- `tarnished admin applications list --json`
|
|
125
|
+
- `tarnished admin ai-settings get --json`
|
|
126
|
+
|
|
127
|
+
Common admin writes:
|
|
128
|
+
|
|
129
|
+
- `tarnished admin users create --body-file admin-user.json`
|
|
130
|
+
- `tarnished admin users update <user-id> --body-file admin-user-update.json`
|
|
131
|
+
- `tarnished admin ai-settings update --body-file ai-settings.json`
|
|
132
|
+
|
|
133
|
+
## Additional Surfaces
|
|
134
|
+
|
|
135
|
+
The CLI also covers:
|
|
136
|
+
|
|
137
|
+
- `rounds`
|
|
138
|
+
- `round-types`
|
|
139
|
+
- application CV / cover-letter flows
|
|
140
|
+
- transcript and media upload/download flows
|
|
141
|
+
- `export`
|
|
142
|
+
- `import`
|
|
143
|
+
- API key management via `tarnished auth api-key ...`
|
|
144
|
+
|
|
145
|
+
Useful operator recipes:
|
|
146
|
+
|
|
147
|
+
- Signed application document URLs:
|
|
148
|
+
- `tarnished applications cv url <application-id>`
|
|
149
|
+
- `tarnished applications cover-letter url <application-id>`
|
|
150
|
+
- Round transcript and media flows:
|
|
151
|
+
- `tarnished rounds transcript upload <round-id> --file-path transcript.txt`
|
|
152
|
+
- `tarnished rounds media upload <round-id> --file-path interview.wav`
|
|
153
|
+
- `tarnished rounds transcript url <round-id>`
|
|
154
|
+
- `tarnished rounds media url <round-id>`
|
|
155
|
+
- Import safety:
|
|
156
|
+
- `tarnished import validate <export-file>`
|
|
157
|
+
- `tarnished import run <export-file> --wait`
|
|
158
|
+
|
|
159
|
+
Use command-group help for exact syntax:
|
|
160
|
+
|
|
161
|
+
- `tarnished rounds --help`
|
|
162
|
+
- `tarnished export --help`
|
|
163
|
+
- `tarnished import --help`
|
|
164
|
+
|
|
165
|
+
## Safety Rules
|
|
166
|
+
|
|
167
|
+
- Prefer read commands first when you need to inspect state before mutating it.
|
|
168
|
+
- Prefer `--json` whenever a downstream agent or tool will parse the output.
|
|
169
|
+
- Prefer explicit environment targeting with `--profile` or `--base-url` rather than assuming the default target.
|
|
170
|
+
- Do not use `admin` unless the task requires privileged operations.
|
|
171
|
+
- Do not describe the CLI as reproducing browser UI behavior; it exposes server-backed data/actions, not browser-rendered presentation.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"query": "i need to see every application in my tarnished instance that is still in the applied stage and dump it as json so another agent can sort them by age",
|
|
4
|
+
"should_trigger": true
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
"query": "can you use tarnished to create a new application for ExampleCo from this body file i already prepared in /tmp/create-application.json and keep the output machine readable",
|
|
8
|
+
"should_trigger": true
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"query": "my local job tracker on http://127.0.0.1:5577 has a lead i want converted into an application, please do it through the tarnished cli not the browser",
|
|
12
|
+
"should_trigger": true
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"query": "check whether im logged into tarnished on the prod profile and if not tell me what auth command i should run next",
|
|
16
|
+
"should_trigger": true
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"query": "pull the 30 day kpis and weekly analytics from tarnished as json because i want another script to compare my response rate week over week",
|
|
20
|
+
"should_trigger": true
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"query": "i want to export my tarnished data before i mess with imports, use the cli and give me the safest command flow",
|
|
24
|
+
"should_trigger": true
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"query": "use tarnished admin commands to list users on my self hosted instance and show me the output as json, dont click through the UI",
|
|
28
|
+
"should_trigger": true
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"query": "there is a cv attached to application app-123 in tarnished and i need the signed download url through the cli",
|
|
32
|
+
"should_trigger": true
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"query": "for my job tracker instance, add a new lead from /tmp/lead.json and then list the latest job leads back to me in json",
|
|
36
|
+
"should_trigger": true
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"query": "i recorded an interview round and need to manage the round data, transcript, and media through tarnished commands instead of the site",
|
|
40
|
+
"should_trigger": true
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"query": "can you inspect the Tarnished analytics experience and tell me whether the sankey chart is visually broken on mobile, even if you need to look at the browser rather than raw json",
|
|
44
|
+
"should_trigger": false
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"query": "my Tarnished pipeline looks bad but i dont actually want commands run, i just want strategic advice on whether i should keep applying or pause for interview prep",
|
|
48
|
+
"should_trigger": false
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"query": "use kubectl to inspect why the tarnished deployment in my homelab namespace keeps restarting and check pod logs, this is cluster debugging not cli usage",
|
|
52
|
+
"should_trigger": false
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"query": "write release notes for Tarnished CLI 0.1.3 and mention the new analytics and import work, but do not run the tarnished command at all",
|
|
56
|
+
"should_trigger": false
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"query": "i have a csv exported from Tarnished but i want you to analyze it with python and charts directly, not through the tarnished command",
|
|
60
|
+
"should_trigger": false
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"query": "fix the React settings page in Tarnished where the API key form jumps around during loading, and only use the cli if you need sample data, the main task is frontend code",
|
|
64
|
+
"should_trigger": false
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"query": "open the Tarnished browser extension popup and check whether autofill is working on this greenhouse form, so this is extension behavior not cli automation",
|
|
68
|
+
"should_trigger": false
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"query": "show me how to install tarnished-cli with homebrew on a new machine and explain the setup, i dont need you to operate the CLI after that",
|
|
72
|
+
"should_trigger": false
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"query": "i want a shell alias so zsh autocomplete works better for tarnished, this is terminal config work around the cli rather than actual Tarnished operations",
|
|
76
|
+
"should_trigger": false
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"query": "please design a prettier dashboard for my job tracker with better spacing and color hierarchy, but do not use the CLI because this is a product design task",
|
|
80
|
+
"should_trigger": false
|
|
81
|
+
}
|
|
82
|
+
]
|
|
@@ -11,19 +11,15 @@ from pydantic import BaseModel
|
|
|
11
11
|
|
|
12
12
|
from tarnished_cli.config import resolve_config_dir
|
|
13
13
|
|
|
14
|
-
ACCESS_TOKEN_ENV = "TARNISHED_ACCESS_TOKEN"
|
|
15
|
-
REFRESH_TOKEN_ENV = "TARNISHED_REFRESH_TOKEN"
|
|
16
14
|
API_KEY_ENV = "TARNISHED_API_KEY"
|
|
17
15
|
KEYRING_SERVICE = "tarnished-cli"
|
|
18
16
|
|
|
19
17
|
|
|
20
18
|
class StoredAuth(BaseModel):
|
|
21
|
-
access_token: str | None = None
|
|
22
|
-
refresh_token: str | None = None
|
|
23
19
|
api_key: str | None = None
|
|
24
20
|
|
|
25
21
|
def is_empty(self) -> bool:
|
|
26
|
-
return
|
|
22
|
+
return self.api_key is None
|
|
27
23
|
|
|
28
24
|
|
|
29
25
|
def resolve_auth_path(profile: str = "default", config_dir: Path | None = None) -> Path:
|
|
@@ -36,16 +32,10 @@ def _keyring_account(profile: str = "default", config_dir: Path | None = None) -
|
|
|
36
32
|
|
|
37
33
|
|
|
38
34
|
def _env_auth() -> StoredAuth | None:
|
|
39
|
-
access_token = os.getenv(ACCESS_TOKEN_ENV)
|
|
40
|
-
refresh_token = os.getenv(REFRESH_TOKEN_ENV)
|
|
41
35
|
api_key = os.getenv(API_KEY_ENV)
|
|
42
|
-
if not
|
|
36
|
+
if not api_key:
|
|
43
37
|
return None
|
|
44
|
-
return StoredAuth(
|
|
45
|
-
access_token=access_token,
|
|
46
|
-
refresh_token=refresh_token,
|
|
47
|
-
api_key=api_key,
|
|
48
|
-
)
|
|
38
|
+
return StoredAuth(api_key=api_key)
|
|
49
39
|
|
|
50
40
|
|
|
51
41
|
def _read_auth_file(path: Path) -> StoredAuth:
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
from collections.abc import Callable
|
|
5
4
|
from dataclasses import dataclass
|
|
6
5
|
from importlib.metadata import PackageNotFoundError, version
|
|
7
6
|
from pathlib import Path
|
|
@@ -9,8 +8,7 @@ from typing import Any, Literal
|
|
|
9
8
|
|
|
10
9
|
import httpx
|
|
11
10
|
|
|
12
|
-
AuthMode = Literal["none", "
|
|
13
|
-
TokenUpdateHook = Callable[[str, str], None]
|
|
11
|
+
AuthMode = Literal["none", "api_key"]
|
|
14
12
|
|
|
15
13
|
|
|
16
14
|
def _resolve_cli_version() -> str:
|
|
@@ -42,17 +40,11 @@ class TarnishedClient:
|
|
|
42
40
|
self,
|
|
43
41
|
*,
|
|
44
42
|
base_url: str,
|
|
45
|
-
access_token: str | None = None,
|
|
46
|
-
refresh_token: str | None = None,
|
|
47
43
|
api_key: str | None = None,
|
|
48
|
-
on_token_refresh: TokenUpdateHook | None = None,
|
|
49
44
|
transport: httpx.BaseTransport | None = None,
|
|
50
45
|
) -> None:
|
|
51
46
|
self.base_url = base_url.rstrip("/")
|
|
52
|
-
self.access_token = access_token
|
|
53
|
-
self.refresh_token = refresh_token
|
|
54
47
|
self.api_key = api_key
|
|
55
|
-
self._on_token_refresh = on_token_refresh
|
|
56
48
|
self._client = httpx.Client(
|
|
57
49
|
base_url=self.base_url,
|
|
58
50
|
follow_redirects=True,
|
|
@@ -82,8 +74,7 @@ class TarnishedClient:
|
|
|
82
74
|
params: dict[str, Any] | None = None,
|
|
83
75
|
json_body: dict[str, Any] | None = None,
|
|
84
76
|
files: dict[str, Any] | None = None,
|
|
85
|
-
auth: AuthMode = "
|
|
86
|
-
allow_refresh: bool = True,
|
|
77
|
+
auth: AuthMode = "api_key",
|
|
87
78
|
) -> httpx.Response:
|
|
88
79
|
response = self._client.request(
|
|
89
80
|
method,
|
|
@@ -94,24 +85,6 @@ class TarnishedClient:
|
|
|
94
85
|
headers=self._build_auth_headers(auth),
|
|
95
86
|
)
|
|
96
87
|
|
|
97
|
-
if (
|
|
98
|
-
response.status_code == 401
|
|
99
|
-
and allow_refresh
|
|
100
|
-
and auth in {"jwt", "flexible"}
|
|
101
|
-
and self.access_token
|
|
102
|
-
and self.refresh_token
|
|
103
|
-
):
|
|
104
|
-
self.refresh_session()
|
|
105
|
-
return self.request(
|
|
106
|
-
method,
|
|
107
|
-
path,
|
|
108
|
-
params=params,
|
|
109
|
-
json_body=json_body,
|
|
110
|
-
files=files,
|
|
111
|
-
auth=auth,
|
|
112
|
-
allow_refresh=False,
|
|
113
|
-
)
|
|
114
|
-
|
|
115
88
|
if response.is_error:
|
|
116
89
|
raise self._to_api_error(response)
|
|
117
90
|
|
|
@@ -122,7 +95,7 @@ class TarnishedClient:
|
|
|
122
95
|
path: str,
|
|
123
96
|
*,
|
|
124
97
|
params: dict[str, Any] | None = None,
|
|
125
|
-
auth: AuthMode = "
|
|
98
|
+
auth: AuthMode = "api_key",
|
|
126
99
|
) -> Any:
|
|
127
100
|
return self._decode_response(
|
|
128
101
|
self.request("GET", path, params=params, auth=auth)
|
|
@@ -133,7 +106,7 @@ class TarnishedClient:
|
|
|
133
106
|
path: str,
|
|
134
107
|
*,
|
|
135
108
|
body: dict[str, Any],
|
|
136
|
-
auth: AuthMode = "
|
|
109
|
+
auth: AuthMode = "api_key",
|
|
137
110
|
) -> Any:
|
|
138
111
|
return self._decode_response(
|
|
139
112
|
self.request("POST", path, json_body=body, auth=auth)
|
|
@@ -144,7 +117,7 @@ class TarnishedClient:
|
|
|
144
117
|
path: str,
|
|
145
118
|
*,
|
|
146
119
|
body: dict[str, Any],
|
|
147
|
-
auth: AuthMode = "
|
|
120
|
+
auth: AuthMode = "api_key",
|
|
148
121
|
) -> Any:
|
|
149
122
|
return self._decode_response(
|
|
150
123
|
self.request("PATCH", path, json_body=body, auth=auth)
|
|
@@ -155,16 +128,16 @@ class TarnishedClient:
|
|
|
155
128
|
path: str,
|
|
156
129
|
*,
|
|
157
130
|
body: dict[str, Any],
|
|
158
|
-
auth: AuthMode = "
|
|
131
|
+
auth: AuthMode = "api_key",
|
|
159
132
|
) -> Any:
|
|
160
133
|
return self._decode_response(
|
|
161
134
|
self.request("PUT", path, json_body=body, auth=auth)
|
|
162
135
|
)
|
|
163
136
|
|
|
164
|
-
def delete(self, path: str, *, auth: AuthMode = "
|
|
137
|
+
def delete(self, path: str, *, auth: AuthMode = "api_key") -> None:
|
|
165
138
|
self.request("DELETE", path, auth=auth)
|
|
166
139
|
|
|
167
|
-
def delete_json(self, path: str, *, auth: AuthMode = "
|
|
140
|
+
def delete_json(self, path: str, *, auth: AuthMode = "api_key") -> Any:
|
|
168
141
|
return self._decode_response(self.request("DELETE", path, auth=auth))
|
|
169
142
|
|
|
170
143
|
def post_file_json(
|
|
@@ -174,9 +147,8 @@ class TarnishedClient:
|
|
|
174
147
|
file_path: Path,
|
|
175
148
|
field_name: str = "file",
|
|
176
149
|
data: dict[str, Any] | None = None,
|
|
177
|
-
auth: AuthMode = "
|
|
150
|
+
auth: AuthMode = "api_key",
|
|
178
151
|
content_type: str | None = None,
|
|
179
|
-
allow_refresh: bool = True,
|
|
180
152
|
) -> Any:
|
|
181
153
|
with file_path.open("rb") as handle:
|
|
182
154
|
file_tuple: tuple[str, Any] | tuple[str, Any, str]
|
|
@@ -192,24 +164,6 @@ class TarnishedClient:
|
|
|
192
164
|
headers=self._build_auth_headers(auth),
|
|
193
165
|
)
|
|
194
166
|
|
|
195
|
-
if (
|
|
196
|
-
response.status_code == 401
|
|
197
|
-
and allow_refresh
|
|
198
|
-
and auth in {"jwt", "flexible"}
|
|
199
|
-
and self.access_token
|
|
200
|
-
and self.refresh_token
|
|
201
|
-
):
|
|
202
|
-
self.refresh_session()
|
|
203
|
-
return self.post_file_json(
|
|
204
|
-
path,
|
|
205
|
-
file_path=file_path,
|
|
206
|
-
field_name=field_name,
|
|
207
|
-
data=data,
|
|
208
|
-
auth=auth,
|
|
209
|
-
content_type=content_type,
|
|
210
|
-
allow_refresh=False,
|
|
211
|
-
)
|
|
212
|
-
|
|
213
167
|
if response.is_error:
|
|
214
168
|
raise self._to_api_error(response)
|
|
215
169
|
|
|
@@ -220,62 +174,19 @@ class TarnishedClient:
|
|
|
220
174
|
path: str,
|
|
221
175
|
*,
|
|
222
176
|
params: dict[str, Any] | None = None,
|
|
223
|
-
auth: AuthMode = "
|
|
177
|
+
auth: AuthMode = "api_key",
|
|
224
178
|
) -> tuple[bytes, httpx.Headers]:
|
|
225
179
|
response = self.request("GET", path, params=params, auth=auth)
|
|
226
180
|
return response.content, response.headers
|
|
227
181
|
|
|
228
|
-
def refresh_session(self) -> None:
|
|
229
|
-
if not self.refresh_token:
|
|
230
|
-
raise CLIError("This command requires login.")
|
|
231
|
-
|
|
232
|
-
response = self._client.post(
|
|
233
|
-
"/api/auth/refresh",
|
|
234
|
-
json={"refresh_token": self.refresh_token},
|
|
235
|
-
)
|
|
236
|
-
if response.is_error:
|
|
237
|
-
raise CLIError("Stored session expired. Run `tarnished auth login` again.")
|
|
238
|
-
|
|
239
|
-
payload = response.json()
|
|
240
|
-
access_token = payload.get("access_token")
|
|
241
|
-
refresh_token = payload.get("refresh_token")
|
|
242
|
-
if not access_token or not refresh_token:
|
|
243
|
-
raise CLIError("Refresh response did not include a full token pair.")
|
|
244
|
-
|
|
245
|
-
self.access_token = access_token
|
|
246
|
-
self.refresh_token = refresh_token
|
|
247
|
-
if self._on_token_refresh is not None:
|
|
248
|
-
self._on_token_refresh(access_token, refresh_token)
|
|
249
|
-
|
|
250
182
|
def _build_auth_headers(self, auth: AuthMode) -> dict[str, str]:
|
|
251
|
-
headers: dict[str, str] = {}
|
|
252
|
-
|
|
253
183
|
if auth == "none":
|
|
254
|
-
return
|
|
255
|
-
|
|
256
|
-
if
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
)
|
|
261
|
-
headers["Authorization"] = f"Bearer {self.access_token}"
|
|
262
|
-
return headers
|
|
263
|
-
|
|
264
|
-
if auth == "api_key":
|
|
265
|
-
if not self.api_key:
|
|
266
|
-
raise CLIError("This command requires an API key.")
|
|
267
|
-
headers["X-API-Key"] = self.api_key
|
|
268
|
-
return headers
|
|
269
|
-
|
|
270
|
-
if self.access_token:
|
|
271
|
-
headers["Authorization"] = f"Bearer {self.access_token}"
|
|
272
|
-
return headers
|
|
273
|
-
|
|
274
|
-
if self.api_key:
|
|
275
|
-
headers["X-API-Key"] = self.api_key
|
|
276
|
-
return headers
|
|
277
|
-
|
|
278
|
-
raise CLIError("This command requires authentication.")
|
|
184
|
+
return {}
|
|
185
|
+
|
|
186
|
+
if not self.api_key:
|
|
187
|
+
raise CLIError("This command requires an API key.")
|
|
188
|
+
|
|
189
|
+
return {"X-API-Key": self.api_key}
|
|
279
190
|
|
|
280
191
|
def _decode_response(self, response: httpx.Response) -> Any:
|
|
281
192
|
if response.status_code == 204 or not response.content:
|
|
@@ -14,7 +14,16 @@ from tarnished_cli.models import (
|
|
|
14
14
|
from tarnished_cli.output import emit_result, exit_for_error
|
|
15
15
|
from tarnished_cli.state import get_state
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
ADMIN_HELP = """Admin-only commands. Use only when you need privileged access.
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
tarnished admin stats --json
|
|
21
|
+
tarnished admin users list --json
|
|
22
|
+
tarnished admin ai-settings get --json
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
app = typer.Typer(help=ADMIN_HELP)
|
|
18
27
|
users_app = typer.Typer(help="Manage users.")
|
|
19
28
|
applications_app = typer.Typer(help="Inspect all applications.")
|
|
20
29
|
statuses_app = typer.Typer(help="Manage default statuses.")
|
|
@@ -8,7 +8,16 @@ from tarnished_cli.models import InsightsRequest
|
|
|
8
8
|
from tarnished_cli.output import emit_result, exit_for_error
|
|
9
9
|
from tarnished_cli.state import get_state
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
ANALYTICS_HELP = """Read analytics data.
|
|
12
|
+
|
|
13
|
+
Examples:
|
|
14
|
+
tarnished analytics kpis --period 30d --json
|
|
15
|
+
tarnished analytics heatmap --year 2026 --json
|
|
16
|
+
tarnished analytics insights --body-file insights.json
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
app = typer.Typer(help=ANALYTICS_HELP)
|
|
12
21
|
|
|
13
22
|
|
|
14
23
|
@app.command("kpis")
|