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.
- hfox-1.0.0rc1/.gitignore +24 -0
- hfox-1.0.0rc1/LICENSE +21 -0
- hfox-1.0.0rc1/PKG-INFO +423 -0
- hfox-1.0.0rc1/README.md +401 -0
- hfox-1.0.0rc1/pyproject.toml +62 -0
- hfox-1.0.0rc1/src/hfox/__init__.py +4 -0
- hfox-1.0.0rc1/src/hfox/__main__.py +7 -0
- hfox-1.0.0rc1/src/hfox/cli/__init__.py +1 -0
- hfox-1.0.0rc1/src/hfox/cli/_util.py +429 -0
- hfox-1.0.0rc1/src/hfox/cli/assets.py +270 -0
- hfox-1.0.0rc1/src/hfox/cli/auth.py +306 -0
- hfox-1.0.0rc1/src/hfox/cli/cf.py +103 -0
- hfox-1.0.0rc1/src/hfox/cli/contacts.py +303 -0
- hfox-1.0.0rc1/src/hfox/cli/context.py +421 -0
- hfox-1.0.0rc1/src/hfox/cli/main.py +286 -0
- hfox-1.0.0rc1/src/hfox/cli/output.py +343 -0
- hfox-1.0.0rc1/src/hfox/cli/system.py +110 -0
- hfox-1.0.0rc1/src/hfox/cli/tickets.py +763 -0
- hfox-1.0.0rc1/src/hfox/core/__init__.py +1 -0
- hfox-1.0.0rc1/src/hfox/core/client.py +353 -0
- hfox-1.0.0rc1/src/hfox/core/config.py +538 -0
- hfox-1.0.0rc1/src/hfox/core/errors.py +144 -0
- hfox-1.0.0rc1/tests/conftest.py +86 -0
- hfox-1.0.0rc1/tests/test_assets_cov.py +725 -0
- hfox-1.0.0rc1/tests/test_auth.py +782 -0
- hfox-1.0.0rc1/tests/test_auth_config_client_cov.py +706 -0
- hfox-1.0.0rc1/tests/test_cf.py +168 -0
- hfox-1.0.0rc1/tests/test_cli.py +70 -0
- hfox-1.0.0rc1/tests/test_client.py +492 -0
- hfox-1.0.0rc1/tests/test_config.py +756 -0
- hfox-1.0.0rc1/tests/test_confirm.py +237 -0
- hfox-1.0.0rc1/tests/test_contacts_cov.py +903 -0
- hfox-1.0.0rc1/tests/test_context_util_cov.py +860 -0
- hfox-1.0.0rc1/tests/test_dry_run_writes.py +158 -0
- hfox-1.0.0rc1/tests/test_errors.py +162 -0
- hfox-1.0.0rc1/tests/test_exit_contract.py +205 -0
- hfox-1.0.0rc1/tests/test_global_flags.py +592 -0
- hfox-1.0.0rc1/tests/test_helpers_dry_run.py +135 -0
- hfox-1.0.0rc1/tests/test_inputs.py +584 -0
- hfox-1.0.0rc1/tests/test_main_system_cov.py +498 -0
- hfox-1.0.0rc1/tests/test_output_cov.py +537 -0
- hfox-1.0.0rc1/tests/test_output_sanitize.py +306 -0
- hfox-1.0.0rc1/tests/test_render_stdout_capture.py +47 -0
- hfox-1.0.0rc1/tests/test_staff_resolution.py +288 -0
- hfox-1.0.0rc1/tests/test_tickets_cov.py +537 -0
- hfox-1.0.0rc1/tests/test_tickets_read.py +126 -0
- hfox-1.0.0rc1/tests/test_tickets_update.py +452 -0
- hfox-1.0.0rc1/tests/test_tickets_write.py +545 -0
- hfox-1.0.0rc1/tests/test_transport.py +479 -0
hfox-1.0.0rc1/.gitignore
ADDED
|
@@ -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.
|