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.
Files changed (56) hide show
  1. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/PKG-INFO +16 -3
  2. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/README.md +15 -2
  3. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/pyproject.toml +1 -1
  4. tarnished_cli-0.1.4/skills/tarnished-cli/SKILL.md +171 -0
  5. tarnished_cli-0.1.4/skills/tarnished-cli/evals/trigger-eval.json +82 -0
  6. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/auth_storage.py +3 -13
  7. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/client.py +16 -105
  8. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/admin.py +10 -1
  9. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/analytics.py +10 -1
  10. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/applications.py +25 -14
  11. tarnished_cli-0.1.4/src/tarnished_cli/commands/auth.py +56 -0
  12. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/exports.py +1 -1
  13. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/imports.py +4 -4
  14. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/job_leads.py +17 -8
  15. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/profile.py +4 -2
  16. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/round_types.py +4 -4
  17. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/rounds.py +13 -11
  18. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/statuses.py +6 -4
  19. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/user_settings.py +2 -2
  20. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/main.py +14 -1
  21. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/state.py +1 -17
  22. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_admin_commands.py +5 -5
  23. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_analytics_commands.py +2 -2
  24. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_applications_commands.py +15 -15
  25. tarnished_cli-0.1.4/tests/test_auth_commands.py +67 -0
  26. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_auth_storage.py +10 -13
  27. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_client.py +15 -36
  28. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_dashboard_commands.py +1 -1
  29. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_export_commands.py +2 -2
  30. tarnished_cli-0.1.4/tests/test_help_surfaces.py +69 -0
  31. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_import_commands.py +4 -4
  32. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_job_leads_commands.py +8 -8
  33. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_profile_commands.py +4 -4
  34. tarnished_cli-0.1.4/tests/test_repo_skill.py +76 -0
  35. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_round_types_commands.py +4 -4
  36. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_rounds_commands.py +10 -10
  37. tarnished_cli-0.1.4/tests/test_skill_eval_fixture.py +31 -0
  38. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_statuses_commands.py +8 -8
  39. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_user_settings_commands.py +3 -3
  40. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/uv.lock +1 -1
  41. tarnished_cli-0.1.2/src/tarnished_cli/commands/auth.py +0 -152
  42. tarnished_cli-0.1.2/tests/test_auth_commands.py +0 -220
  43. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/.gitignore +0 -0
  44. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/__init__.py +0 -0
  45. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/__init__.py +0 -0
  46. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/dashboard.py +0 -0
  47. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/commands/preferences.py +0 -0
  48. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/config.py +0 -0
  49. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/input.py +0 -0
  50. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/models/__init__.py +0 -0
  51. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/models/requests.py +0 -0
  52. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/src/tarnished_cli/output.py +0 -0
  53. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/conftest.py +0 -0
  54. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_cli_config.py +0 -0
  55. {tarnished_cli-0.1.2 → tarnished_cli-0.1.4}/tests/test_main.py +0 -0
  56. {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.2
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.2-py3-none-any.whl
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.2'
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.2-py3-none-any.whl
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.2'
49
+ uv tool install 'tarnished-cli==0.1.4'
37
50
  ```
38
51
 
39
52
  Then run:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tarnished-cli"
3
- version = "0.1.2"
3
+ version = "0.1.4"
4
4
  description = "Agent-first CLI for Tarnished"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -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 not any([self.access_token, self.refresh_token, self.api_key])
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 any([access_token, refresh_token, api_key]):
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", "jwt", "api_key", "flexible"]
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 = "jwt",
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 = "jwt",
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 = "jwt",
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 = "jwt",
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 = "jwt",
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 = "jwt") -> None:
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 = "jwt") -> Any:
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 = "jwt",
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 = "jwt",
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 headers
255
-
256
- if auth == "jwt":
257
- if not self.access_token:
258
- raise CLIError(
259
- "This command requires login. Run `tarnished auth login`."
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
- app = typer.Typer(help="Admin-only commands.")
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
- app = typer.Typer(help="Read analytics data.")
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")