Skip to content

Commit 3b76618

Browse files
committed
add clean JSON output: --format json (clean) vs --format json-full (raw)
json: per-resource cleanup — drop HAL noise, HTML→md bodies, flatten persons, drop sentinels/empty fields, hoist embedded sub-resources, rename for clarity. json-full: unfiltered API pass-through for write operations and debugging.
1 parent 6a01a49 commit 3b76618

22 files changed

+1387
-74
lines changed

.agents/plans/inbox/json-format-spec.md

Lines changed: 553 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ hs inbox auth logout
8888
| Flag | Description | Default |
8989
|------|-------------|---------|
9090
| `--config` | Config file path | `~/.config/hs/config.yaml` |
91-
| `--format` | Output format: `table`, `json`, `csv` | `table` |
91+
| `--format` | Output format: `table`, `json`, `json-full`, `csv` | `table` |
9292
| `--no-paginate` | Fetch all pages (combine all results) | `false` |
9393
| `--page` | Page number | `1` |
9494
| `--per-page` | Results per page | `25` |
@@ -125,7 +125,7 @@ hs inbox config path
125125
| `--client-id` | string | HelpScout Client ID |
126126
| `--client-secret` | string | HelpScout Client Secret |
127127
| `--default-mailbox` | int | Default mailbox ID |
128-
| `--format` | string | Output format: table, json, csv |
128+
| `--format` | string | Output format: table, json, json-full, csv |
129129

130130
### Self-update
131131

@@ -867,13 +867,31 @@ ID NAME EMAIL SLUG
867867
Page 1 of 1 (2 total)
868868
```
869869

870-
### JSON
870+
### JSON (clean)
871871

872872
```bash
873873
hs inbox mailboxes list --format json
874874
```
875875

876-
When using `--format json`, raw API responses are pretty-printed. List commands output a JSON array of objects.
876+
Read-optimized output. Compared to the raw API response, `--format json` applies per-resource cleanup:
877+
878+
- Drops HAL noise (`_links`, `_embedded` wrappers)
879+
- Converts HTML bodies to markdown (threads, saved replies)
880+
- Flattens person objects to `"Name (email)"` strings
881+
- Drops sentinel values (`closedBy: 0`, `closedByUser: {id: 0, ...}`)
882+
- Drops empty arrays/strings and default-noise fields (`state: "published"`, `photoUrl`, etc.)
883+
- Hoists embedded sub-resources to top level (e.g. customer `_embedded.emails``emails`)
884+
- Renames for clarity (`userUpdatedAt``updatedAt`, `threads` count → `threadCount`)
885+
886+
**Caveat:** `--format json` is read-only safe. HTML→markdown conversion and field removal make the output unsuitable as input for write operations (e.g. updating saved replies, composing thread bodies). Use `--format json-full` when you need write-safe data.
887+
888+
### JSON-full (raw)
889+
890+
```bash
891+
hs inbox mailboxes list --format json-full
892+
```
893+
894+
Unfiltered API pass-through. Identical to the raw HelpScout API response, pretty-printed. Use this for debugging, round-tripping data back into write operations, or when you need the complete response including HAL links and embedded wrappers.
877895

878896
### CSV
879897

@@ -915,7 +933,7 @@ Use `hs inbox config set` to write values, `hs inbox config get` to read them, a
915933
| `client_id` | `--client-id` | HelpScout Client ID |
916934
| `client_secret` | `--client-secret` | HelpScout Client Secret |
917935
| `default_mailbox` | `--default-mailbox` | Auto-filter conversations to this mailbox |
918-
| `format` | `--format` | Output format: `table`, `json`, or `csv` |
936+
| `format` | `--format` | Output format: `table`, `json`, `json-full`, or `csv` |
919937

920938
### Environment variables
921939

@@ -1025,6 +1043,7 @@ internal/
10251043
store.go OS keyring storage
10261044
cmd/
10271045
root.go Root command, global flags, PersistentPreRunE
1046+
json_clean.go Per-resource JSON cleanup (json vs json-full)
10281047
auth.go login / status / logout
10291048
config.go config set / get / path
10301049
update.go self-update command

internal/cmd/attachments.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ func newConversationAttachmentsCmd() *cobra.Command {
7878
return err
7979
}
8080

81-
if getFormat() == "json" {
81+
if isJSON() {
8282
return output.PrintRaw(mustMarshal(attachments))
8383
}
8484

@@ -105,7 +105,7 @@ func newConversationAttachmentsCmd() *cobra.Command {
105105
return err
106106
}
107107

108-
if getFormat() == "json" {
108+
if isJSON() {
109109
return output.PrintRaw(data)
110110
}
111111

internal/cmd/conversations.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,15 @@ func conversationsListCmd() *cobra.Command {
133133
params.Set("embed", v)
134134
}
135135

136-
if getFormat() == "json" {
136+
if isJSON() {
137137
items, _, err := api.PaginateAll(ctx, apiClient.ListConversations, params, "conversations", noPaginate)
138138
if err != nil {
139139
return err
140140
}
141-
return output.PrintRaw(mustMarshal(items))
141+
if !isJSONClean() {
142+
return output.PrintRaw(mustMarshal(items))
143+
}
144+
return output.PrintRaw(mustMarshal(cleanRawItems(items, cleanConversation)))
142145
}
143146

144147
items, pageInfo, err := api.PaginateAll(ctx, apiClient.ListConversations, params, "conversations", noPaginate)
@@ -196,8 +199,11 @@ func conversationsGetCmd() *cobra.Command {
196199
return err
197200
}
198201

199-
if getFormat() == "json" {
200-
return output.PrintRaw(data)
202+
if isJSON() {
203+
if !isJSONClean() {
204+
return output.PrintRaw(data)
205+
}
206+
return output.PrintRaw(mustMarshal(cleanRawObject(data, cleanConversation)))
201207
}
202208

203209
var c types.Conversation

internal/cmd/customers.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,15 @@ func customersListCmd() *cobra.Command {
152152
params.Set("sortOrder", v)
153153
}
154154

155-
if getFormat() == "json" {
155+
if isJSON() {
156156
items, _, err := api.PaginateAll(ctx, apiClient.ListCustomers, params, "customers", noPaginate)
157157
if err != nil {
158158
return err
159159
}
160-
return output.PrintRaw(mustMarshal(items))
160+
if !isJSONClean() {
161+
return output.PrintRaw(mustMarshal(items))
162+
}
163+
return output.PrintRaw(mustMarshal(cleanRawItems(items, cleanCustomer)))
161164
}
162165

163166
items, pageInfo, err := api.PaginateAll(ctx, apiClient.ListCustomers, params, "customers", noPaginate)
@@ -205,8 +208,11 @@ func customersGetCmd() *cobra.Command {
205208
return err
206209
}
207210

208-
if getFormat() == "json" {
209-
return output.PrintRaw(data)
211+
if isJSON() {
212+
if !isJSONClean() {
213+
return output.PrintRaw(data)
214+
}
215+
return output.PrintRaw(mustMarshal(cleanRawObject(data, cleanCustomer)))
210216
}
211217

212218
var c types.Customer

0 commit comments

Comments
 (0)