tarnished-cli 0.1.7__tar.gz → 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.
Files changed (96) hide show
  1. tarnished_cli-0.2.0/.gitignore +66 -0
  2. tarnished_cli-0.2.0/LICENSE +21 -0
  3. tarnished_cli-0.2.0/PKG-INFO +187 -0
  4. tarnished_cli-0.2.0/README.md +169 -0
  5. tarnished_cli-0.2.0/examples/evidence/current-meaning.json +1 -0
  6. tarnished_cli-0.2.0/examples/evidence/document-text.json +4 -0
  7. tarnished_cli-0.2.0/examples/evidence/history-correction.json +1 -0
  8. tarnished_cli-0.2.0/examples/evidence/insights.json +1 -0
  9. tarnished_cli-0.2.0/examples/evidence/report-intent-pipeline.json +5 -0
  10. tarnished_cli-0.2.0/examples/evidence/report-intent.json +5 -0
  11. tarnished_cli-0.2.0/examples/evidence/response-clear.json +1 -0
  12. tarnished_cli-0.2.0/examples/evidence/response-record.json +7 -0
  13. tarnished_cli-0.2.0/examples/evidence/transcript-edit.json +9 -0
  14. tarnished_cli-0.2.0/examples/evidence/transcript-paste.json +4 -0
  15. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/pyproject.toml +11 -3
  16. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/src/tarnished_cli/auth_diagnostics.py +4 -3
  17. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/src/tarnished_cli/auth_storage.py +19 -9
  18. tarnished_cli-0.2.0/src/tarnished_cli/client.py +355 -0
  19. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/src/tarnished_cli/commands/admin.py +0 -19
  20. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/src/tarnished_cli/commands/analytics.py +49 -5
  21. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/src/tarnished_cli/commands/applications.py +105 -4
  22. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/src/tarnished_cli/commands/auth.py +20 -14
  23. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/src/tarnished_cli/commands/dashboard.py +18 -4
  24. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/src/tarnished_cli/commands/exports.py +2 -3
  25. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/src/tarnished_cli/commands/imports.py +44 -7
  26. tarnished_cli-0.2.0/src/tarnished_cli/commands/job_leads.py +211 -0
  27. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/src/tarnished_cli/commands/preferences.py +3 -1
  28. tarnished_cli-0.2.0/src/tarnished_cli/commands/reports.py +230 -0
  29. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/src/tarnished_cli/commands/rounds.py +68 -6
  30. tarnished_cli-0.2.0/src/tarnished_cli/commands/transcriptions.py +197 -0
  31. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/src/tarnished_cli/config.py +1 -32
  32. tarnished_cli-0.2.0/src/tarnished_cli/files.py +12 -0
  33. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/src/tarnished_cli/input.py +5 -1
  34. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/src/tarnished_cli/main.py +31 -20
  35. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/src/tarnished_cli/models/__init__.py +10 -8
  36. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/src/tarnished_cli/models/requests.py +213 -4
  37. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/src/tarnished_cli/output.py +11 -0
  38. tarnished_cli-0.2.0/src/tarnished_cli/polling.py +12 -0
  39. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/src/tarnished_cli/state.py +19 -15
  40. tarnished_cli-0.2.0/tests/conftest.py +56 -0
  41. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/tests/test_admin_commands.py +83 -0
  42. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/tests/test_auth_commands.py +23 -124
  43. tarnished_cli-0.2.0/tests/test_auth_storage.py +117 -0
  44. tarnished_cli-0.2.0/tests/test_cli_config.py +180 -0
  45. tarnished_cli-0.2.0/tests/test_cli_connection_docs.py +92 -0
  46. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/tests/test_client.py +43 -0
  47. tarnished_cli-0.2.0/tests/test_date_requests.py +64 -0
  48. tarnished_cli-0.2.0/tests/test_documented_commands.py +49 -0
  49. tarnished_cli-0.2.0/tests/test_download_output.py +118 -0
  50. tarnished_cli-0.2.0/tests/test_evidence_commands.py +276 -0
  51. tarnished_cli-0.2.0/tests/test_evidence_requests.py +92 -0
  52. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/tests/test_import_commands.py +5 -2
  53. tarnished_cli-0.2.0/tests/test_import_wait.py +176 -0
  54. tarnished_cli-0.2.0/tests/test_lead_capture.py +264 -0
  55. tarnished_cli-0.2.0/tests/test_local_file_errors.py +43 -0
  56. tarnished_cli-0.2.0/tests/test_report_wait.py +115 -0
  57. tarnished_cli-0.2.0/tests/test_reports_commands.py +187 -0
  58. tarnished_cli-0.2.0/tests/test_shipped_promises.py +85 -0
  59. tarnished_cli-0.2.0/tests/test_transcript_document_commands.py +170 -0
  60. tarnished_cli-0.2.0/tests/test_transcriptions_commands.py +275 -0
  61. tarnished_cli-0.2.0/tests/test_transport_contract.py +368 -0
  62. tarnished_cli-0.2.0/tests/test_user_settings_commands.py +83 -0
  63. tarnished_cli-0.2.0/uv.lock +655 -0
  64. tarnished_cli-0.1.7/.gitignore +0 -181
  65. tarnished_cli-0.1.7/PKG-INFO +0 -103
  66. tarnished_cli-0.1.7/README.md +0 -91
  67. tarnished_cli-0.1.7/skills/tarnished-cli/SKILL.md +0 -173
  68. tarnished_cli-0.1.7/skills/tarnished-cli/evals/trigger-eval.json +0 -82
  69. tarnished_cli-0.1.7/src/tarnished_cli/client.py +0 -224
  70. tarnished_cli-0.1.7/src/tarnished_cli/commands/job_leads.py +0 -140
  71. tarnished_cli-0.1.7/tests/conftest.py +0 -15
  72. tarnished_cli-0.1.7/tests/test_auth_storage.py +0 -53
  73. tarnished_cli-0.1.7/tests/test_cli_config.py +0 -9
  74. tarnished_cli-0.1.7/tests/test_repo_skill.py +0 -76
  75. tarnished_cli-0.1.7/tests/test_skill_eval_fixture.py +0 -31
  76. tarnished_cli-0.1.7/tests/test_user_settings_commands.py +0 -53
  77. tarnished_cli-0.1.7/uv.lock +0 -614
  78. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/src/tarnished_cli/__init__.py +0 -0
  79. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/src/tarnished_cli/commands/__init__.py +0 -0
  80. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/src/tarnished_cli/commands/profile.py +0 -0
  81. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/src/tarnished_cli/commands/round_types.py +0 -0
  82. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/src/tarnished_cli/commands/statuses.py +0 -0
  83. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/src/tarnished_cli/commands/user_settings.py +0 -0
  84. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/src/tarnished_cli/models/auth.py +0 -0
  85. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/tests/test_analytics_commands.py +0 -0
  86. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/tests/test_applications_commands.py +0 -0
  87. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/tests/test_dashboard_commands.py +0 -0
  88. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/tests/test_export_commands.py +0 -0
  89. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/tests/test_help_surfaces.py +0 -0
  90. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/tests/test_job_leads_commands.py +0 -0
  91. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/tests/test_main.py +0 -0
  92. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/tests/test_output_contract.py +0 -0
  93. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/tests/test_profile_commands.py +0 -0
  94. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/tests/test_round_types_commands.py +0 -0
  95. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/tests/test_rounds_commands.py +0 -0
  96. {tarnished_cli-0.1.7 → tarnished_cli-0.2.0}/tests/test_statuses_commands.py +0 -0
@@ -0,0 +1,66 @@
1
+ # Dependencies and build output
2
+ .venv/
3
+ node_modules/
4
+ .yarn/
5
+ .pnp.*
6
+ .uv/
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+ *.egg
11
+ *.tsbuildinfo
12
+
13
+ # Local configuration and data
14
+ .env
15
+ .env.*
16
+ !.env.example
17
+ .envrc.local
18
+ .direnv/
19
+ data/
20
+ uploads/
21
+ postgres_data/
22
+ *.db
23
+ *.sqlite
24
+ *.sqlite3
25
+ docker-compose.override.yml
26
+ docker-compose.override.yaml
27
+ backend/requirements.txt
28
+
29
+ # Caches and test output
30
+ __pycache__/
31
+ *.py[cod]
32
+ .ruff_cache/
33
+ .pytest_cache/
34
+ .mypy_cache/
35
+ .dmypy.json
36
+ .tox/
37
+ .vite/
38
+ .eslintcache
39
+ .cache/
40
+ .npm/
41
+ .coverage
42
+ coverage/
43
+ htmlcov/
44
+ *.cover
45
+ *.lcov
46
+ *.log
47
+
48
+ # Editor and operating system files
49
+ .vscode/
50
+ .idea/
51
+ .DS_Store
52
+ Thumbs.db
53
+ *.swp
54
+ *.swo
55
+ *~
56
+ *.tmp
57
+ *.bak
58
+ /core
59
+ core.*
60
+ tmp/
61
+
62
+ # Browser packages
63
+ *.xpi
64
+ extension/tarnished-chrome.zip
65
+ extension/tarnished-firefox.zip
66
+ extension/package-lock.json
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Marko Onakic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,187 @@
1
+ Metadata-Version: 2.5
2
+ Name: tarnished-cli
3
+ Version: 0.2.0
4
+ Summary: Command-line interface for Tarnished
5
+ Project-URL: Homepage, https://markoonakic.github.io/tarnished/
6
+ Project-URL: Repository, https://github.com/markoonakic/tarnished
7
+ Project-URL: Documentation, https://markoonakic.github.io/tarnished/how-to/use-the-cli/
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Requires-Python: >=3.12
11
+ Requires-Dist: email-validator>=2.0.0
12
+ Requires-Dist: httpx>=0.28.1
13
+ Requires-Dist: keyring>=25.7.0
14
+ Requires-Dist: pydantic>=2.12.0
15
+ Requires-Dist: typer>=0.23.1
16
+ Requires-Dist: tzlocal>=5.3.1
17
+ Description-Content-Type: text/markdown
18
+
19
+ # Tarnished CLI
20
+
21
+ Command-line interface for Tarnished, version **0.2.0**. Use a matching Tarnished backend.
22
+
23
+ **Documentation:** https://markoonakic.github.io/tarnished/
24
+
25
+ For the user-facing CLI guide, see:
26
+ - https://markoonakic.github.io/tarnished/how-to/use-the-cli/
27
+
28
+ ## Install
29
+
30
+ Install version 0.2.0 from PyPI once that release is available:
31
+
32
+ ```bash
33
+ uv tool install tarnished-cli==0.2.0
34
+ ```
35
+
36
+ To install this checkout, including before a release is published, run from the repository root:
37
+
38
+ ```bash
39
+ uv tool install ./cli
40
+ ```
41
+
42
+ Homebrew convenience path (the tap can lag the PyPI release):
43
+
44
+ ```bash
45
+ brew tap markoonakic/tap
46
+ brew install tarnished-cli
47
+ ```
48
+
49
+ To install a downloaded wheel:
50
+
51
+ ```bash
52
+ uv tool install ./dist/tarnished_cli-<version>-py3-none-any.whl
53
+ ```
54
+
55
+ ## Authentication
56
+
57
+ Tarnished CLI is API-key-first.
58
+
59
+ 1. Create or rotate the API key in the Tarnished web app.
60
+ 2. Prefer the **CLI** preset so the key includes the scopes the CLI expects.
61
+ 3. Validate and store the key locally:
62
+
63
+ ```bash
64
+ tarnished auth init --api-key '...'
65
+ ```
66
+
67
+ 4. Run the auth doctor to confirm the stored key, live auth check, and required
68
+ CLI scopes all pass:
69
+
70
+ ```bash
71
+ tarnished auth doctor
72
+ tarnished auth whoami
73
+ ```
74
+
75
+ 5. Clear the locally stored key when needed:
76
+
77
+ ```bash
78
+ tarnished auth api-key clear
79
+ ```
80
+
81
+ The web app remains the source of truth for API keys. The CLI does **not**
82
+ create, rotate, revoke, or otherwise manage remote API keys.
83
+
84
+ The CLI uses the system keyring when available. Otherwise, it stores the key in
85
+ a private file in the CLI config directory. `TARNISHED_API_KEY` overrides stored
86
+ credentials. Clearing a stored key does not unset this environment variable.
87
+
88
+ ## Usage
89
+
90
+ Global options go before the command:
91
+
92
+ ```bash
93
+ tarnished --base-url https://tarnished.example.com auth status
94
+ tarnished --profile work --json applications list
95
+ tarnished applications --help
96
+ ```
97
+
98
+ Create `config.json` in `~/.config/tarnished` (or
99
+ `$XDG_CONFIG_HOME/tarnished`) to set profiles:
100
+
101
+ ```json
102
+ {
103
+ "default_profile": "default",
104
+ "profiles": {
105
+ "default": { "base_url": "http://127.0.0.1:5577", "output": "json" },
106
+ "work": { "base_url": "https://tarnished.example.com", "output": "json" }
107
+ }
108
+ }
109
+ ```
110
+
111
+ `--config-dir` and `TARNISHED_CONFIG_DIR` override this directory. Server URL
112
+ precedence is `--base-url`, then `TARNISHED_BASE_URL`, then the selected profile.
113
+ Use a trusted server: URL overrides send the selected profile's key to that server.
114
+ The CLI follows same-origin redirects but rejects cross-origin or
115
+ credential-bearing URLs. Use the final server URL where possible.
116
+
117
+ JSON is the default output. Set the profile's output to `text` or use
118
+ `TARNISHED_OUTPUT=text` for text output; `--json` overrides both. API and local
119
+ configuration errors exit with code 1. JSON errors go to stdout; text errors go
120
+ to stderr. Invalid command arguments use Typer's standard error output.
121
+
122
+ Imports, reports, and transcriptions support `--wait`, `--poll-interval`, and
123
+ `--timeout-seconds`. A local wait timeout does not cancel server work. Report
124
+ waits stop if another job replaces the submitted job. Review the saved state
125
+ before retrying an AI or speech request, which can incur provider charges.
126
+
127
+ ## Development
128
+
129
+ ```bash
130
+ cd cli
131
+ uv sync
132
+ uv run tarnished --help
133
+ uv run pytest -q
134
+ uv run ruff check .
135
+ uv run ruff format --check .
136
+ uv run pyright
137
+ uv build
138
+ ```
139
+
140
+ ## Release
141
+
142
+ The repository release workflow builds CLI distributions, uploads them to the
143
+ GitHub release, and publishes the CLI to PyPI by default. Homebrew tap updates
144
+ run in the separate `homebrew-tap.yml` workflow after Homebrew's 24-hour PyPI
145
+ resource-resolution cooldown window has elapsed.
146
+
147
+ ### One-Time PyPI Trusted Publishing Setup
148
+
149
+ 1. Create the `tarnished-cli` project on PyPI.
150
+ 2. In the PyPI project settings, add a Trusted Publisher for this GitHub repository.
151
+ 3. Use these values:
152
+ - Owner: `markoonakic`
153
+ - Repository: `tarnished`
154
+ - Workflow name: `release.yml`
155
+ - Environment name: `pypi`
156
+
157
+ ### One-Time Homebrew Tap Automation Setup
158
+
159
+ 1. Create a write-enabled deploy key for `markoonakic/homebrew-tap`.
160
+ 2. Add the private key to this repository as the `HOMEBREW_TAP_DEPLOY_KEY` secret.
161
+ 3. The dedicated `homebrew-tap.yml` workflow will:
162
+ - wait until the target PyPI release is older than Homebrew's 24-hour Python resource cooldown window
163
+ - update `Formula/tarnished-cli.rb` from PyPI metadata
164
+ - refresh Python resource blocks with `brew update-python-resources`
165
+ - build the formula from source and run `brew test tarnished-cli`
166
+ - push the tap update to `markoonakic/homebrew-tap`
167
+
168
+ Why separate from the main release workflow?
169
+
170
+ - Homebrew's Python dependency resolver intentionally excludes PyPI uploads from the last 24 hours when refreshing resource blocks.
171
+ - Keeping tap sync separate avoids false-negative release failures while still following Homebrew's documented Python-formula workflow.
172
+
173
+ ### Release Outputs
174
+
175
+ `release.yml` publishes:
176
+
177
+ - `cli/dist/*.whl`
178
+ - `cli/dist/*.tar.gz`
179
+
180
+ to the GitHub release and publishes the package to PyPI.
181
+
182
+ `homebrew-tap.yml` then updates the Homebrew tap separately once the published
183
+ sdist is old enough for `brew update-python-resources` to resolve safely.
184
+
185
+ ## License
186
+
187
+ MIT. The wheel and source distribution include the LICENSE file.
@@ -0,0 +1,169 @@
1
+ # Tarnished CLI
2
+
3
+ Command-line interface for Tarnished, version **0.2.0**. Use a matching Tarnished backend.
4
+
5
+ **Documentation:** https://markoonakic.github.io/tarnished/
6
+
7
+ For the user-facing CLI guide, see:
8
+ - https://markoonakic.github.io/tarnished/how-to/use-the-cli/
9
+
10
+ ## Install
11
+
12
+ Install version 0.2.0 from PyPI once that release is available:
13
+
14
+ ```bash
15
+ uv tool install tarnished-cli==0.2.0
16
+ ```
17
+
18
+ To install this checkout, including before a release is published, run from the repository root:
19
+
20
+ ```bash
21
+ uv tool install ./cli
22
+ ```
23
+
24
+ Homebrew convenience path (the tap can lag the PyPI release):
25
+
26
+ ```bash
27
+ brew tap markoonakic/tap
28
+ brew install tarnished-cli
29
+ ```
30
+
31
+ To install a downloaded wheel:
32
+
33
+ ```bash
34
+ uv tool install ./dist/tarnished_cli-<version>-py3-none-any.whl
35
+ ```
36
+
37
+ ## Authentication
38
+
39
+ Tarnished CLI is API-key-first.
40
+
41
+ 1. Create or rotate the API key in the Tarnished web app.
42
+ 2. Prefer the **CLI** preset so the key includes the scopes the CLI expects.
43
+ 3. Validate and store the key locally:
44
+
45
+ ```bash
46
+ tarnished auth init --api-key '...'
47
+ ```
48
+
49
+ 4. Run the auth doctor to confirm the stored key, live auth check, and required
50
+ CLI scopes all pass:
51
+
52
+ ```bash
53
+ tarnished auth doctor
54
+ tarnished auth whoami
55
+ ```
56
+
57
+ 5. Clear the locally stored key when needed:
58
+
59
+ ```bash
60
+ tarnished auth api-key clear
61
+ ```
62
+
63
+ The web app remains the source of truth for API keys. The CLI does **not**
64
+ create, rotate, revoke, or otherwise manage remote API keys.
65
+
66
+ The CLI uses the system keyring when available. Otherwise, it stores the key in
67
+ a private file in the CLI config directory. `TARNISHED_API_KEY` overrides stored
68
+ credentials. Clearing a stored key does not unset this environment variable.
69
+
70
+ ## Usage
71
+
72
+ Global options go before the command:
73
+
74
+ ```bash
75
+ tarnished --base-url https://tarnished.example.com auth status
76
+ tarnished --profile work --json applications list
77
+ tarnished applications --help
78
+ ```
79
+
80
+ Create `config.json` in `~/.config/tarnished` (or
81
+ `$XDG_CONFIG_HOME/tarnished`) to set profiles:
82
+
83
+ ```json
84
+ {
85
+ "default_profile": "default",
86
+ "profiles": {
87
+ "default": { "base_url": "http://127.0.0.1:5577", "output": "json" },
88
+ "work": { "base_url": "https://tarnished.example.com", "output": "json" }
89
+ }
90
+ }
91
+ ```
92
+
93
+ `--config-dir` and `TARNISHED_CONFIG_DIR` override this directory. Server URL
94
+ precedence is `--base-url`, then `TARNISHED_BASE_URL`, then the selected profile.
95
+ Use a trusted server: URL overrides send the selected profile's key to that server.
96
+ The CLI follows same-origin redirects but rejects cross-origin or
97
+ credential-bearing URLs. Use the final server URL where possible.
98
+
99
+ JSON is the default output. Set the profile's output to `text` or use
100
+ `TARNISHED_OUTPUT=text` for text output; `--json` overrides both. API and local
101
+ configuration errors exit with code 1. JSON errors go to stdout; text errors go
102
+ to stderr. Invalid command arguments use Typer's standard error output.
103
+
104
+ Imports, reports, and transcriptions support `--wait`, `--poll-interval`, and
105
+ `--timeout-seconds`. A local wait timeout does not cancel server work. Report
106
+ waits stop if another job replaces the submitted job. Review the saved state
107
+ before retrying an AI or speech request, which can incur provider charges.
108
+
109
+ ## Development
110
+
111
+ ```bash
112
+ cd cli
113
+ uv sync
114
+ uv run tarnished --help
115
+ uv run pytest -q
116
+ uv run ruff check .
117
+ uv run ruff format --check .
118
+ uv run pyright
119
+ uv build
120
+ ```
121
+
122
+ ## Release
123
+
124
+ The repository release workflow builds CLI distributions, uploads them to the
125
+ GitHub release, and publishes the CLI to PyPI by default. Homebrew tap updates
126
+ run in the separate `homebrew-tap.yml` workflow after Homebrew's 24-hour PyPI
127
+ resource-resolution cooldown window has elapsed.
128
+
129
+ ### One-Time PyPI Trusted Publishing Setup
130
+
131
+ 1. Create the `tarnished-cli` project on PyPI.
132
+ 2. In the PyPI project settings, add a Trusted Publisher for this GitHub repository.
133
+ 3. Use these values:
134
+ - Owner: `markoonakic`
135
+ - Repository: `tarnished`
136
+ - Workflow name: `release.yml`
137
+ - Environment name: `pypi`
138
+
139
+ ### One-Time Homebrew Tap Automation Setup
140
+
141
+ 1. Create a write-enabled deploy key for `markoonakic/homebrew-tap`.
142
+ 2. Add the private key to this repository as the `HOMEBREW_TAP_DEPLOY_KEY` secret.
143
+ 3. The dedicated `homebrew-tap.yml` workflow will:
144
+ - wait until the target PyPI release is older than Homebrew's 24-hour Python resource cooldown window
145
+ - update `Formula/tarnished-cli.rb` from PyPI metadata
146
+ - refresh Python resource blocks with `brew update-python-resources`
147
+ - build the formula from source and run `brew test tarnished-cli`
148
+ - push the tap update to `markoonakic/homebrew-tap`
149
+
150
+ Why separate from the main release workflow?
151
+
152
+ - Homebrew's Python dependency resolver intentionally excludes PyPI uploads from the last 24 hours when refreshing resource blocks.
153
+ - Keeping tap sync separate avoids false-negative release failures while still following Homebrew's documented Python-formula workflow.
154
+
155
+ ### Release Outputs
156
+
157
+ `release.yml` publishes:
158
+
159
+ - `cli/dist/*.whl`
160
+ - `cli/dist/*.tar.gz`
161
+
162
+ to the GitHub release and publishes the package to PyPI.
163
+
164
+ `homebrew-tap.yml` then updates the Homebrew tap separately once the published
165
+ sdist is old enough for `brew update-python-resources` to resolve safely.
166
+
167
+ ## License
168
+
169
+ MIT. The wheel and source distribution include the LICENSE file.
@@ -0,0 +1 @@
1
+ { "meaning": "rejected", "expected_revision": 0 }
@@ -0,0 +1,4 @@
1
+ {
2
+ "text": "Synthetic pasted document text for analysis. Replace with the document's real text.",
3
+ "expected_revision": 0
4
+ }
@@ -0,0 +1 @@
1
+ { "to_meaning": "interviewing", "expected_revision": 0 }
@@ -0,0 +1 @@
1
+ { "period": "all", "as_of": "2026-01-07T09:00:00Z" }
@@ -0,0 +1,5 @@
1
+ {
2
+ "intent_id": "22222222-2222-2222-2222-222222222222",
3
+ "config_revision": "0",
4
+ "period": "30d"
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "intent_id": "11111111-1111-1111-1111-111111111111",
3
+ "generation": 0,
4
+ "config_revision": "0"
5
+ }
@@ -0,0 +1 @@
1
+ { "response_evidence": null, "expected_revision": 0 }
@@ -0,0 +1,7 @@
1
+ {
2
+ "response_evidence": {
3
+ "occurred_on": null,
4
+ "reference": "Synthetic employer response"
5
+ },
6
+ "expected_revision": 0
7
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "segments": [
3
+ {
4
+ "id": "replace-with-a-real-segment-id",
5
+ "text": "Corrected sentence text.",
6
+ "speaker": null
7
+ }
8
+ ]
9
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "text": "Interviewer: Thanks for joining.\nCandidate: Glad to be here.",
3
+ "format": "txt"
4
+ }
@@ -1,8 +1,10 @@
1
1
  [project]
2
2
  name = "tarnished-cli"
3
- version = "0.1.7"
4
- description = "Agent-first CLI for Tarnished"
3
+ version = "0.2.0"
4
+ description = "Command-line interface for Tarnished"
5
5
  readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
6
8
  requires-python = ">=3.12"
7
9
  dependencies = [
8
10
  "httpx>=0.28.1",
@@ -10,13 +12,19 @@ dependencies = [
10
12
  "pydantic>=2.12.0",
11
13
  "email-validator>=2.0.0",
12
14
  "typer>=0.23.1",
15
+ "tzlocal>=5.3.1",
13
16
  ]
14
17
 
18
+ [project.urls]
19
+ Homepage = "https://markoonakic.github.io/tarnished/"
20
+ Repository = "https://github.com/markoonakic/tarnished"
21
+ Documentation = "https://markoonakic.github.io/tarnished/how-to/use-the-cli/"
22
+
15
23
  [project.scripts]
16
24
  tarnished = "tarnished_cli.main:app"
17
25
 
18
26
  [build-system]
19
- requires = ["hatchling"]
27
+ requires = ["hatchling>=1.27"]
20
28
  build-backend = "hatchling.build"
21
29
 
22
30
  [tool.hatch.build.targets.wheel]
@@ -6,7 +6,7 @@ from typing import Any
6
6
  from pydantic import ValidationError
7
7
 
8
8
  from tarnished_cli.auth_storage import StoredAuth
9
- from tarnished_cli.client import CLIError
9
+ from tarnished_cli.client import CLIError, redact_credentials
10
10
  from tarnished_cli.models.auth import AuthDiagnostics, LiveAuthIdentity
11
11
 
12
12
  API_KEY_PREFIX_LENGTH = 8
@@ -26,6 +26,7 @@ CLI_REQUIRED_SCOPES = (
26
26
  "round_types:write",
27
27
  "dashboard:read",
28
28
  "analytics:read",
29
+ "analytics:generate",
29
30
  "streak:read",
30
31
  "streak:write",
31
32
  "preferences:read",
@@ -49,7 +50,7 @@ def parse_live_identity(payload: object) -> LiveAuthIdentity:
49
50
 
50
51
 
51
52
  def stored_api_key_prefix(api_key: str | None) -> str | None:
52
- if not api_key:
53
+ if not api_key or len(api_key) <= API_KEY_PREFIX_LENGTH:
53
54
  return None
54
55
  return api_key[:API_KEY_PREFIX_LENGTH]
55
56
 
@@ -63,7 +64,7 @@ def build_auth_diagnostics(
63
64
  ) -> AuthDiagnostics:
64
65
  return AuthDiagnostics(
65
66
  profile=profile,
66
- base_url=base_url,
67
+ base_url=redact_credentials(base_url, stored_auth.api_key),
67
68
  has_stored_api_key=bool(stored_auth.api_key),
68
69
  stored_api_key_prefix=stored_api_key_prefix(stored_auth.api_key),
69
70
  live_identity=_coerce_live_identity(live_identity),
@@ -9,7 +9,9 @@ import keyring
9
9
  from keyring.errors import KeyringError, PasswordDeleteError
10
10
  from pydantic import BaseModel
11
11
 
12
+ from tarnished_cli.client import CLIError
12
13
  from tarnished_cli.config import resolve_config_dir
14
+ from tarnished_cli.files import write_private_file
13
15
 
14
16
  API_KEY_ENV = "TARNISHED_API_KEY"
15
17
  KEYRING_SERVICE = "tarnished-cli"
@@ -41,21 +43,29 @@ def _env_auth() -> StoredAuth | None:
41
43
  def _read_auth_file(path: Path) -> StoredAuth:
42
44
  if not path.exists():
43
45
  return StoredAuth()
44
- return StoredAuth.model_validate(json.loads(path.read_text()))
46
+ return StoredAuth.model_validate(json.loads(path.read_text(encoding="utf-8")))
45
47
 
46
48
 
47
49
  def _write_auth_file(path: Path, auth: StoredAuth) -> None:
48
- path.parent.mkdir(parents=True, exist_ok=True)
49
- with contextlib.suppress(OSError):
50
- path.parent.chmod(0o700)
51
- path.write_text(json.dumps(auth.model_dump(mode="json"), sort_keys=True) + "\n")
52
- with contextlib.suppress(OSError):
53
- path.chmod(0o600)
50
+ try:
51
+ path.parent.mkdir(parents=True, exist_ok=True)
52
+ with contextlib.suppress(OSError):
53
+ path.parent.chmod(0o700)
54
+ content = json.dumps(auth.model_dump(mode="json"), sort_keys=True) + "\n"
55
+ write_private_file(path, content.encode("utf-8"))
56
+ except OSError:
57
+ raise CLIError(
58
+ "Could not save API key. Check config directory permissions and disk space."
59
+ ) from None
54
60
 
55
61
 
56
62
  def _remove_auth_file(path: Path) -> None:
57
- if path.exists():
58
- path.unlink()
63
+ try:
64
+ path.unlink(missing_ok=True)
65
+ except OSError:
66
+ raise CLIError(
67
+ "Could not remove stored API key. Check config directory permissions."
68
+ ) from None
59
69
 
60
70
 
61
71
  def load_auth(