hfox 1.0.0rc1__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 (49) hide show
  1. hfox-1.0.0rc1/.gitignore +24 -0
  2. hfox-1.0.0rc1/LICENSE +21 -0
  3. hfox-1.0.0rc1/PKG-INFO +423 -0
  4. hfox-1.0.0rc1/README.md +401 -0
  5. hfox-1.0.0rc1/pyproject.toml +62 -0
  6. hfox-1.0.0rc1/src/hfox/__init__.py +4 -0
  7. hfox-1.0.0rc1/src/hfox/__main__.py +7 -0
  8. hfox-1.0.0rc1/src/hfox/cli/__init__.py +1 -0
  9. hfox-1.0.0rc1/src/hfox/cli/_util.py +429 -0
  10. hfox-1.0.0rc1/src/hfox/cli/assets.py +270 -0
  11. hfox-1.0.0rc1/src/hfox/cli/auth.py +306 -0
  12. hfox-1.0.0rc1/src/hfox/cli/cf.py +103 -0
  13. hfox-1.0.0rc1/src/hfox/cli/contacts.py +303 -0
  14. hfox-1.0.0rc1/src/hfox/cli/context.py +421 -0
  15. hfox-1.0.0rc1/src/hfox/cli/main.py +286 -0
  16. hfox-1.0.0rc1/src/hfox/cli/output.py +343 -0
  17. hfox-1.0.0rc1/src/hfox/cli/system.py +110 -0
  18. hfox-1.0.0rc1/src/hfox/cli/tickets.py +763 -0
  19. hfox-1.0.0rc1/src/hfox/core/__init__.py +1 -0
  20. hfox-1.0.0rc1/src/hfox/core/client.py +353 -0
  21. hfox-1.0.0rc1/src/hfox/core/config.py +538 -0
  22. hfox-1.0.0rc1/src/hfox/core/errors.py +144 -0
  23. hfox-1.0.0rc1/tests/conftest.py +86 -0
  24. hfox-1.0.0rc1/tests/test_assets_cov.py +725 -0
  25. hfox-1.0.0rc1/tests/test_auth.py +782 -0
  26. hfox-1.0.0rc1/tests/test_auth_config_client_cov.py +706 -0
  27. hfox-1.0.0rc1/tests/test_cf.py +168 -0
  28. hfox-1.0.0rc1/tests/test_cli.py +70 -0
  29. hfox-1.0.0rc1/tests/test_client.py +492 -0
  30. hfox-1.0.0rc1/tests/test_config.py +756 -0
  31. hfox-1.0.0rc1/tests/test_confirm.py +237 -0
  32. hfox-1.0.0rc1/tests/test_contacts_cov.py +903 -0
  33. hfox-1.0.0rc1/tests/test_context_util_cov.py +860 -0
  34. hfox-1.0.0rc1/tests/test_dry_run_writes.py +158 -0
  35. hfox-1.0.0rc1/tests/test_errors.py +162 -0
  36. hfox-1.0.0rc1/tests/test_exit_contract.py +205 -0
  37. hfox-1.0.0rc1/tests/test_global_flags.py +592 -0
  38. hfox-1.0.0rc1/tests/test_helpers_dry_run.py +135 -0
  39. hfox-1.0.0rc1/tests/test_inputs.py +584 -0
  40. hfox-1.0.0rc1/tests/test_main_system_cov.py +498 -0
  41. hfox-1.0.0rc1/tests/test_output_cov.py +537 -0
  42. hfox-1.0.0rc1/tests/test_output_sanitize.py +306 -0
  43. hfox-1.0.0rc1/tests/test_render_stdout_capture.py +47 -0
  44. hfox-1.0.0rc1/tests/test_staff_resolution.py +288 -0
  45. hfox-1.0.0rc1/tests/test_tickets_cov.py +537 -0
  46. hfox-1.0.0rc1/tests/test_tickets_read.py +126 -0
  47. hfox-1.0.0rc1/tests/test_tickets_update.py +452 -0
  48. hfox-1.0.0rc1/tests/test_tickets_write.py +545 -0
  49. hfox-1.0.0rc1/tests/test_transport.py +479 -0
@@ -0,0 +1,24 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ .venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+
9
+ # Test / tooling
10
+ .pytest_cache/
11
+ .ruff_cache/
12
+ .coverage
13
+ coverage.xml
14
+ htmlcov/
15
+
16
+ # Local secrets / config
17
+ .env
18
+ token.json
19
+
20
+ # OS / editor
21
+ .DS_Store
22
+ .idea/
23
+ .vscode/
24
+ .claude/
hfox-1.0.0rc1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Leo Herzog
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.
hfox-1.0.0rc1/PKG-INFO ADDED
@@ -0,0 +1,423 @@
1
+ Metadata-Version: 2.5
2
+ Name: hfox
3
+ Version: 1.0.0rc1
4
+ Summary: Command-line interface for the HappyFox REST API
5
+ Project-URL: Homepage, https://github.com/leoherzog/hfox
6
+ Project-URL: Source, https://github.com/leoherzog/hfox
7
+ Project-URL: Issues, https://github.com/leoherzog/hfox/issues
8
+ Author: Leo Herzog
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: cli,happyfox,helpdesk,support,tickets
12
+ Classifier: Environment :: Console
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Requires-Python: >=3.11
18
+ Requires-Dist: httpx>=0.28.1
19
+ Requires-Dist: rich>=15.0.0
20
+ Requires-Dist: typer<0.28,>=0.27.2
21
+ Description-Content-Type: text/markdown
22
+
23
+ # 🦊 hfox
24
+
25
+ A command-line interface for the [HappyFox](https://www.happyfox.com/) REST API.
26
+
27
+ `hfox` covers tickets, contacts, contact groups, assets, asset types and reference data
28
+ with a `noun verb` structure modeled on the [Google Workspace CLI (`gws`)](https://github.com/googleworkspace/cli).
29
+ Humans get `--help`, tables and `--dry-run`; scripts and AI agents get JSON by default,
30
+ structured errors and stable exit codes.
31
+
32
+ ## Install / run
33
+
34
+ `hfox` is a [`uv` tool](https://docs.astral.sh/uv/) and requires Python 3.11+.
35
+
36
+ ```bash
37
+ uv tool install hfox # install it as a persistent tool
38
+ uvx hfox --help # or run it without installing
39
+ uvx --from . hfox --help # run from a checkout
40
+ ```
41
+
42
+ Each [GitHub release](https://github.com/leoherzog/hfox/releases) also attaches single-file
43
+ binaries for Linux, macOS and Windows that need no Python. Mark a download executable with
44
+ `chmod +x`; on macOS, a browser download also needs `xattr -d com.apple.quarantine <file>`.
45
+ Every binary, the wheel and the sdist carry a build provenance attestation, checked with
46
+ `gh attestation verify <file> --repo leoherzog/hfox`.
47
+
48
+ ## Authenticate
49
+
50
+ `hfox` needs an **API key** and its **auth code**. In HappyFox, open *Apps → Goodies → API*,
51
+ click **Enable**, add a key with **+**, then hover its row and click **see auth code**.
52
+
53
+ ```bash
54
+ hfox auth login # prompts for subdomain, region, API key, auth code
55
+ hfox auth login --subdomain acme --email agent@acme.com # also sets your default staff id
56
+ HFOX_API_KEY=... HFOX_AUTH_CODE=... hfox auth login --subdomain acme # scripted
57
+ hfox auth status --check
58
+ hfox auth logout
59
+ ```
60
+
61
+ Login checks the credentials against `staff/` before saving them. It takes each value from
62
+ its flag, then from `HFOX_SUBDOMAIN`, `HFOX_REGION`, `HFOX_API_KEY` or `HFOX_AUTH_CODE`, then
63
+ from a prompt on stderr. Keep the two secrets off the command line, where the shell history
64
+ and the process list expose them. When stdin is not a terminal, a missing subdomain, key or
65
+ code exits 3 instead of prompting, and a missing region means `us`. The error names the
66
+ missing flags and the variable to set for each one.
67
+
68
+ `--email` saves the id of the one agent with that email as the default staff id. Without
69
+ `--email`, a login to the same subdomain, region and base URL keeps the stored default, and a
70
+ login to a different target removes it. An `--email` that matches nobody, matches several
71
+ agents or matches an agent with an unusable id sets no default and removes a stored one; the
72
+ login still succeeds. `default_staff_id` in the login JSON is the value stored
73
+ after the save.
74
+
75
+ Each `auth` command prints one JSON document that never holds a secret. `auth status` masks
76
+ the API key and exits 2 when credentials are missing, after printing its payload.
77
+ `auth status --check` sends one request and adds `verified`; a failed check adds
78
+ `check_error` and exits with that error's code. A `staff/` answer that is not a list fails
79
+ the check with exit 5.
80
+
81
+ ### Configuration
82
+
83
+ The config directory is `$XDG_CONFIG_HOME/hfox` when that variable holds an absolute path,
84
+ otherwise `~/.config/hfox`, on every OS. `--config-dir` or `HFOX_CONFIG_DIR` overrides both.
85
+
86
+ | File | Contents |
87
+ |------|----------|
88
+ | `token.json` | secrets: `api_key`, `auth_code`, `subdomain`, `region`, optional `base_url` (mode `0600`) |
89
+ | `config.toml` | non-secret: `subdomain`, `region`, `default_format`, `default_staff_id` |
90
+
91
+ Every field resolves as environment variable, then file, then default. `config.toml` also
92
+ accepts a hand-written `base_url`, read after `HFOX_BASE_URL` and `token.json`.
93
+
94
+ A file that cannot be parsed, or that holds a non-string `subdomain`, `region`, `base_url`,
95
+ `api_key` or `auth_code`, exits 5 with an error of type `config` that names the file and the
96
+ key. When no home directory can be determined, set `HFOX_CONFIG_DIR` to an absolute path; a
97
+ config directory whose `~` cannot be expanded also exits 5.
98
+
99
+ | Variable | Sets |
100
+ |----------|------|
101
+ | `HFOX_SUBDOMAIN`, `HFOX_REGION` | the account host |
102
+ | `HFOX_API_KEY`, `HFOX_AUTH_CODE` | the credentials |
103
+ | `HFOX_BASE_URL` | an `http(s)://host` root for a proxied account |
104
+ | `HFOX_STAFF_ID` | the default acting staff id |
105
+ | `HFOX_FORMAT` | the default output format |
106
+ | `HFOX_TIMEOUT`, `HFOX_MAX_RETRIES` | the defaults of `--timeout` and `--max-retries` |
107
+ | `HFOX_CONFIG_DIR` | the config directory |
108
+
109
+ `--region eu` targets `*.happyfox.net`; a custom domain goes in `--subdomain` as the full host
110
+ (`support.acme.com`). For proxied accounts, set `HFOX_BASE_URL`; a trailing `/api/1.1/json`
111
+ is stripped and `auth login` stores the value. A stored base URL wins over `--subdomain`, so
112
+ `auth login --subdomain` warns on stderr when one is in effect.
113
+
114
+ A base URL with embedded credentials, a query or a fragment exits 3. The error leaves out any
115
+ value that holds `@`, `?` or `#`, since it may carry a secret. An `http` base URL on a
116
+ non-loopback host sends the credentials in cleartext, so hfox warns on stderr once per
117
+ invocation and proceeds.
118
+
119
+ `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY` and `NO_PROXY` are honored through httpx. TLS trust
120
+ comes from the certifi bundle unless `SSL_CERT_FILE` or `SSL_CERT_DIR` names another one.
121
+ Redirects are never followed.
122
+ [SECURITY.md](https://github.com/leoherzog/hfox/blob/main/SECURITY.md) describes the
123
+ credential model.
124
+
125
+ ## Global flags
126
+
127
+ Global flags go before the resource: `hfox -f table tickets list`, not `hfox tickets list -f table`.
128
+
129
+ | Flag | Purpose |
130
+ |------|---------|
131
+ | `-f, --format {json\|table\|csv\|yaml}` | output format (default `json`) |
132
+ | `--dry-run` | print the exact request without sending it |
133
+ | `--page-all` | fetch up to `--page-limit` pages from `--page`; JSON streams NDJSON, one page per line |
134
+ | `--page-limit N` / `--page-delay MS` | bound `--page-all` (default 10 pages / 100 ms) |
135
+ | `--staff TEXT` / `--staff-id N` | acting staff for write actions, by email or name, or by id |
136
+ | `--timeout SECONDS` | per-attempt timeout for connecting and for each read or write (default 30) |
137
+ | `--max-retries N` | retries after HTTP 429 or a network error (default 5) |
138
+ | `--quiet` | suppress status messages and retry notices |
139
+ | `--config-dir DIR` | override the config directory |
140
+ | `--no-color` | disable color (also honors `NO_COLOR`) |
141
+ | `-v, --version` | show the release version, or `dev` when run from source |
142
+
143
+ A global flag after the resource exits 3 with an error of type `usage` that says where the
144
+ flag goes. That also holds when the flag follows an option that takes a value, so
145
+ `tickets note 5 --text --dry-run` is refused. Write `--text=--dry-run` to send such a literal.
146
+
147
+ A global option that takes a value refuses one that starts with `-`, so
148
+ `hfox --staff --dry-run tickets list` exits 3 with type `usage` instead of reading `--dry-run`
149
+ as the staff name. The `--opt=value` form is checked the same way. A numeric option such as
150
+ `--timeout` still takes a negative number, which its range check then rejects.
151
+
152
+ An unknown format from `--format`, `HFOX_FORMAT` or `default_format` exits 3. In `config.toml`
153
+ only an empty string means unset, so `default_format = false` is an unknown format.
154
+
155
+ `--timeout` bounds one attempt, not the whole command. Retries, their backoff and any
156
+ `Retry-After` wait come on top of it. Before each retry hfox prints one stderr line with the
157
+ reason and the wait. GETs retry any network error; writes retry only when no connection was
158
+ made.
159
+
160
+ ## Listing and search
161
+
162
+ For full exports, pass `--size 50` with `--page-all`. When `--page-limit` stops the walk
163
+ early, hfox warns on stderr and still exits 0, so raise the limit for a complete export. On
164
+ `tickets list`, add a stable `--sort` such as `ticketa`; `-q` searches always sort by
165
+ relevance.
166
+
167
+ On `tickets list` and `contacts list`, `-q` means `--query`:
168
+
169
+ - Tickets: text or filters such as `status:"In Progress","New"`; a comma means any-of and a
170
+ space replaces the docs' `+`. Multi-word text is ANDed, which HappyFox does not document.
171
+ - Contacts: `field:value` filters on `name`, `email`, `phone`, `updated_since` or
172
+ `created_since`, ANDed when space-separated. Omit `+` from phone numbers.
173
+
174
+ Endpoints without server-side search take local filters instead. Every `system` command and
175
+ `contacts groups list` take `--name`, and `system staff` also takes `--email`. A filter keeps
176
+ rows whose field contains the text, ignoring case; every filter must match, and a missing or
177
+ non-text field never matches. Filters are never sent to HappyFox, and no match prints an empty
178
+ listing with exit `0`.
179
+
180
+ `assets list`, `assets types list` and `assets custom-fields list` take `--page` and `--size`.
181
+ They take `--name` only with the global `--page-all`, since one page would miss matches. Each
182
+ NDJSON line keeps the server's `page_info`, which counts rows before filtering.
183
+
184
+ ## Acting staff
185
+
186
+ Commands that HappyFox records against an agent take `--staff` or `--staff-id`, never both.
187
+ `--staff` is an email or a name and `--staff-id` the numeric id. The pair is on ticket
188
+ `reply`, `note`, `update`, `update-cf`, `tags`, `forward`, `move` and `delete`, and on asset
189
+ `create`, `update` and `delete`. On `tickets subscribe` and `unsubscribe` the pair names the
190
+ agent being added or removed.
191
+
192
+ The identity resolves in this order: the flag on the command, the global flag, then
193
+ `HFOX_STAFF_ID` or the default saved by `auth login --email`. `--staff` reads `staff/` and
194
+ matches the email first, then the name, ignoring case. No match or several matches exit 3,
195
+ and a value made of digits is refused with a pointer to `--staff-id`. The lookup is a read,
196
+ so it is also sent under `--dry-run` and needs credentials.
197
+
198
+ `--assignee`, `--assign-to` and `--agents` take staff ids; `--alert` takes `s`, `c` or a
199
+ staff id. None accepts an email or name. Find the ids with `hfox system staff`.
200
+
201
+ ## Input from files and stdin
202
+
203
+ Message bodies can come from a file: `--text-file` and `--html-file` on `tickets create`,
204
+ `reply` and `note`, `--text-file` on `user-reply`, `--message-file` on `forward` and
205
+ `--note-file` on `move`. Pass `-` to read stdin. The inline flag and its file form exclude
206
+ each other, and a file or stdin body that is empty or whitespace only exits 3.
207
+
208
+ The JSON `--file` flags of `tickets create-bulk`, `contacts create-bulk` and
209
+ `tickets set-cf-choices` accept `-` as well. Only one input per invocation may read stdin.
210
+ Files and stdin are read as strict UTF-8, and a leading byte order mark is dropped.
211
+ `--attachment` always names a file.
212
+
213
+ A path must name a regular file. A directory, FIFO, device or process substitution exits 3
214
+ with "is not a regular file", and a missing path exits 3 with "not found". A leading `~` that
215
+ names no known home directory is used as written.
216
+
217
+ hfox refuses to read a file you name when it lies inside the config directory, including
218
+ through a symlink or a hard link to `token.json`. It also refuses to send a file or stdin
219
+ whose content holds the configured API key or auth code. Both checks run under `--dry-run`.
220
+
221
+ ## Confirmation
222
+
223
+ `tickets delete`, `assets delete` and `tickets set-cf-choices` ask before acting, on stderr.
224
+ `--yes` skips the question, and `--dry-run` never asks. When stdin is not a terminal the
225
+ command exits 3 unless `--yes` is given. Declining, or pressing Ctrl-C at the prompt, prints
226
+ an error of type `cancelled` and exits 5.
227
+
228
+ ## Output
229
+
230
+ JSON is the default; `-f table`, `-f csv` and `-f yaml` render the same data. Status lines,
231
+ warnings and prompts go to stderr, so stdout stays parseable. CSV ends every row with one
232
+ CRLF on every platform, and a newline inside a cell stays a bare LF.
233
+
234
+ Table and CSV cells drop control, bidi and zero-width characters, so ticket text cannot drive
235
+ the terminal. Newline, tab and the zero-width joiner and non-joiner are kept. A CSV text cell
236
+ that starts with `=`, `+`, `-`, `@` or a tab gets a leading apostrophe so a spreadsheet does
237
+ not run it as a formula; plain signed decimals such as `-5` and `+15551234567` stay as they
238
+ are.
239
+
240
+ JSON and NDJSON write C1 controls, bidi controls, line and paragraph separators and tag
241
+ characters as `\uXXXX` escapes. A JSON parser returns the same strings, and YAML carries the
242
+ data unchanged.
243
+
244
+ ## Errors and exit codes
245
+
246
+ | Code | Meaning |
247
+ |------|---------|
248
+ | `0` | success |
249
+ | `1` | HappyFox returned an error response, or part of a bulk request failed |
250
+ | `2` | missing or rejected credentials |
251
+ | `3` | bad arguments or input |
252
+ | `4` | resource not found (HTTP 404) |
253
+ | `5` | anything else: network, timeout, config file, cancelled prompt, internal |
254
+
255
+ A failure prints one JSON object on stdout:
256
+
257
+ ```json
258
+ {
259
+ "error": "Rate limit exceeded (HTTP 429). HappyFox enforces a 10-minute cooldown after the limit is hit.",
260
+ "type": "rate_limited",
261
+ "exit_code": 1,
262
+ "status_code": 429,
263
+ "retry_after": 600
264
+ }
265
+ ```
266
+
267
+ `error`, `type` and `exit_code` are always present. `status_code`, `detail`, `hint`,
268
+ `retry_after` and `outcome_unknown` appear only when set. `type` is one of `api`,
269
+ `rate_limited`, `auth`, `validation`, `usage`, `not_found`, `network`, `timeout`, `config`,
270
+ `cancelled`, `internal` and `other`. `usage` is a structural mistake such as an unknown or
271
+ misplaced flag, a missing argument or a global option without its value, and `validation` is
272
+ a rejected value.
273
+
274
+ `retry_after` is the server's `Retry-After` in seconds on a 429 that outlasted the retries.
275
+ `outcome_unknown: true` marks a write that failed after the request may have left, or that
276
+ HappyFox answered with HTTP 500 or above. The write may have been applied, so check the
277
+ resource before sending it again. A failure to connect, a pool timeout, a proxy error and an
278
+ unsupported URL scheme never set it, and neither does a GET.
279
+
280
+ A nonzero exit carries that error object except in these cases:
281
+
282
+ - A partial bulk or group-membership failure prints the response and exits 1. Removing a
283
+ contact that is not in the group does not count.
284
+ - `auth status` without credentials prints its payload and exits 2. A failed
285
+ `auth status --check` prints the payload with `check_error`.
286
+ - With `--page-all` in JSON, the error is the last NDJSON line.
287
+ - Ctrl-C outside a prompt exits 130 with nothing on stdout.
288
+
289
+ Usage errors also print the usage hint on stderr, except a misplaced global flag and a global
290
+ option without its value. Bare group invocations (`hfox tickets`) print help and exit `0`.
291
+
292
+ ## Stability
293
+
294
+ These are contract from 1.0 on: the JSON that hfox itself produces, CSV output, exit codes,
295
+ flag names and environment variable names. The hfox JSON is the error object, the
296
+ `--dry-run` preview `{dry_run, method, url, params, body, attachments}`, the `auth` payloads
297
+ and the NDJSON framing of one page per line. Truncation at `--page-limit` keeps exit 0 and
298
+ the stderr warning.
299
+
300
+ These are not contract: table layout, the files inside the config directory, the bodies
301
+ HappyFox returns and hfox passes through, and the content of `detail`.
302
+
303
+ Minor releases may add keys, `type` values, flags and environment variables, so ignore the
304
+ ones you do not know. Removing or renaming any of them is a breaking change. The names
305
+ `--columns`, `--profile` and `HFOX_PROFILE` are reserved.
306
+
307
+ ## Command overview
308
+
309
+ ```
310
+ hfox auth login | status | logout
311
+ hfox tickets list | get | create | create-bulk | inline-attachment | reply | note | user-reply
312
+ update | update-cf | tags | subscribe | unsubscribe | forward | move | delete
313
+ set-cf-choices
314
+ hfox contacts list | get | create | update | create-bulk
315
+ hfox contacts groups list | get | create | update | add-contacts | remove-contacts
316
+ hfox assets list | get | create | update | delete
317
+ hfox assets types list | get
318
+ hfox assets custom-fields list | get
319
+ hfox system categories | priorities | staff | statuses | ticket-custom-fields | contact-custom-fields
320
+ ```
321
+
322
+ Ticket commands take the numeric ticket id, not the display id. `tickets update` changes
323
+ status, priority, assignee, due date, tags, time spent and custom fields without posting a
324
+ message, and `tickets create --unassign` sends an explicit empty assignee. `contacts create`
325
+ also edits the contact with the same email and resets custom fields it does not send.
326
+
327
+ ## Examples
328
+
329
+ ```bash
330
+ # Reference data (ids you need elsewhere)
331
+ hfox -f table system priorities
332
+ hfox -f table system ticket-custom-fields # choices shown as text=id
333
+ hfox system staff --email jane@ # find a staff id
334
+
335
+ # Tickets
336
+ hfox -f table tickets list --status _all --size 50 --fields id,display_id,subject
337
+ hfox --page-all tickets list -q 'priority:"CRITICAL"' --size 50
338
+ hfox tickets list -q 'id:DC00000003' # find the numeric id of a display id
339
+ hfox tickets get 1234 --show-cf-changes
340
+ hfox tickets create --subject "Printer down" --category 3 \
341
+ --name "Han Solo" --email han@rebels.org --text "It is on fire." \
342
+ --cf 7=Urgent --attachment ./photo.png
343
+ hfox tickets reply 1234 --text "On it." --status 5 --update-customer
344
+ hfox tickets reply 1234 --html-file reply.html --staff jane@acme.com
345
+ git log -5 | hfox tickets note 1234 --text-file -
346
+ hfox tickets update 1234 --status 3 --unassign # change properties without a message
347
+ hfox tickets inline-attachment ./diagram.png # temporary url for an <img src>
348
+ hfox tickets move 1234 --to-category 4 --note "Wrong queue"
349
+ hfox --dry-run tickets delete 1234 # preview, no prompt
350
+ hfox tickets delete 1234 --yes
351
+
352
+ # Contacts & groups
353
+ hfox -f table contacts list -q name:han
354
+ hfox contacts create --name "Leia" --email leia@rebels.org --cf 4=VIP
355
+ hfox contacts create-bulk --file - < contacts.json
356
+ hfox contacts groups add-contacts 12 --contacts 41,200 --access-tickets
357
+
358
+ # Assets
359
+ hfox -f table assets list --asset-type 1
360
+ hfox --page-all -f table assets list --asset-type 1 --size 50 --name latitude
361
+ hfox assets types list --page 2 --size 50
362
+ hfox assets create --asset-type 1 --name "MBP-14" --display-id "LAP-001" \
363
+ --cf 5=4 --cf '6=[3,4]' --staff-id 12
364
+ ```
365
+
366
+ ### Custom fields
367
+
368
+ `--cf <id>=<value>` is repeatable. Canonical decimals become numbers, `<id>=[a,b]` sends a
369
+ list, and anything else is sent as the exact string.
370
+
371
+ ```bash
372
+ --cf 3="In Progress" # text field
373
+ --cf 5=4 # dropdown choice id
374
+ --cf '6=[3,4,5]' # multiple-option ids -> [3,4,5]; '6=[4]' -> [4]
375
+ --cf 7=2025-12-25 # date
376
+ --cf 8=02134 # stays the string "02134"
377
+ ```
378
+
379
+ An all-blank value such as `--cf 5=` exits 3. `--cf-json '{"5": ""}'` sends values uncoerced.
380
+ With `--attachment`, `null` values are dropped, or rejected on reply and note. `tickets create`,
381
+ `reply` and `note` take contact fields through `--contact-cf` and `--contact-cf-json`.
382
+
383
+ Keys are numeric ids from `hfox system ticket-custom-fields`, `contact-custom-fields` or
384
+ `hfox assets custom-fields list`, not agent-portal URLs. A key may carry its endpoint's
385
+ prefix (`t-cf-5`); any other key exits 3.
386
+
387
+ ### Custom-field choices
388
+
389
+ `tickets set-cf-choices <field_id>` replaces the whole choice list of a ticket dropdown or
390
+ multiple-option field. The choices come from `--file`, stdin or `--choices-json`, as a JSON
391
+ array or as an object `{"choices": [...]}`.
392
+
393
+ ```bash
394
+ hfox tickets set-cf-choices 7 --yes \
395
+ --choices-json '[{"id": 11, "text": "Low"}, {"id": 12, "text": "High"}, {"id": null, "text": "Urgent"}]'
396
+ ```
397
+
398
+ Every choice needs a non-blank `text` and an `id`. An existing choice keeps its id, a new one
399
+ has `"id": null`, and an existing choice left out of the list is deleted. Read the current
400
+ ids with `hfox system ticket-custom-fields` first and carry each one you want to keep. The
401
+ command states how many choices are kept and how many are new before it asks.
402
+
403
+ ## Architecture
404
+
405
+ ```
406
+ src/hfox/
407
+ ├── core/ # HTTP client, config/secrets, errors and exit codes; no Typer, no printing
408
+ └── cli/ # Typer apps and output formatting, one module per resource
409
+ ```
410
+
411
+ ## Development
412
+
413
+ ```bash
414
+ uv sync # set up the environment
415
+ uv run hfox --help # run from source
416
+ uv run python -m pytest # offline tests
417
+ uv run ruff check . # lint
418
+ uv build # build sdist + wheel
419
+ ```
420
+
421
+ Read [AGENTS.md](https://github.com/leoherzog/hfox/blob/main/AGENTS.md) before extending the
422
+ CLI. [skills/hfox/SKILL.md](https://github.com/leoherzog/hfox/blob/main/skills/hfox/SKILL.md)
423
+ is a usage guide for AI agents that drive it.