Skip to content

feat: add --fields flag for context-window-friendly JSON output#373

Merged
BYK merged 3 commits intomainfrom
feat/fields-flag
Mar 10, 2026
Merged

feat: add --fields flag for context-window-friendly JSON output#373
BYK merged 3 commits intomainfrom
feat/fields-flag

Conversation

@BYK
Copy link
Copy Markdown
Member

@BYK BYK commented Mar 9, 2026

Summary

Add a --fields flag to all 14 commands that support --json output, allowing agents (and humans) to request only specific fields. This reduces token consumption and keeps agent context windows focused.

Fixes #347

Motivation

From You Need to Rewrite Your CLI for AI Agents:

"APIs return massive blobs. A single Gmail message can consume a meaningful fraction of an agent's context window. Humans don't care — humans scroll. Agents pay per token and lose reasoning capacity with every irrelevant field."

A full sentry issue view --json dumps the entire issue object, latest event, and span tree. An agent that just needs id, title, and status is forced to ingest thousands of tokens of irrelevant data.

Usage

# Select specific top-level fields
sentry issue list --json --fields id,shortId,title,status

# Dot-notation for nested fields
sentry issue view --json --fields id,title,metadata.value

# Works with all --json commands
sentry event view --json --fields eventID,dateCreated,contexts.trace
sentry project list --json --fields slug,platform

Design

Option A from the issue: Simple --fields flag with comma-separated field paths. Covers the 90% case for agents, trivial to implement, no external dependencies.

Core implementation (src/lib/formatters/json.ts)

  • filterFields(data, fields) — picks specified fields from objects/arrays, supports dot-notation for nested access, silently skips missing fields
  • parseFieldsList(input) — parses comma-separated input with dedup, whitespace trimming, empty segment filtering
  • writeJson(stream, data, fields?) — optional fields param filters output before serialization

Shared flag (src/lib/list-command.ts)

  • FIELDS_FLAG — Stricli flag constant shared by all commands with --json

Commands updated (14 total)

auth whoami, event view, issue explain, issue list, issue plan, issue view, log list, log view, org list, project create, project list, project view, trace list, trace view

Edge cases

  • Empty fields array → outputs full object (no filtering)
  • Missing fields → silently ignored (no errors)
  • Dot-notation with null/primitive intermediaries → skipped
  • Array data → each element filtered independently
  • Primitive data (string, number) → passed through unchanged
  • Keys containing dots → inherent dot-notation ambiguity (documented limitation)
  • --fields without --json → silently ignored

Testing

  • 12 property-based tests — identity, subset invariant, idempotency, array mapping, deduplication, whitespace tolerance, round-trip
  • 36 unit tests — specific edge cases, dot-notation, integration with writeJson
  • All 3088 existing tests continue to pass (1 pre-existing failure in log formatter)

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 9, 2026

Semver Impact of This PR

🟡 Minor (new features)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features ✨

  • (init) Add init command for guided Sentry project setup by betegon in #283
  • (issue-list) Redesign table to match Sentry web UI by BYK in #372
  • Add --fields flag for context-window-friendly JSON output by BYK in #373
  • Magic @ selectors (@latest, @most_frequent) for issue commands by BYK in #371
  • Input hardening against agent hallucinations by BYK in #370
  • Add response caching for read-only API calls by BYK in #330

Bug Fixes 🐛

  • (docs) Remove double borders and fix column alignment on landing page tables by betegon in #369
  • Add trace ID validation to trace view + UUID dash-stripping by BYK in #375

Internal Changes 🔧

  • (init) Remove dead determine-pm step label by betegon in #374

🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 9, 2026

Codecov Results 📊

104 passed | Total: 104 | Pass Rate: 100% | Execution Time: 0ms

📊 Comparison with Base Branch

Metric Change
Total Tests
Passed Tests
Failed Tests
Skipped Tests

✨ No test changes detected

All tests are passing successfully.

✅ Patch coverage is 99.60%. Project has 900 uncovered lines.
✅ Project coverage is 95.45%. Comparing base (base) to head (head).

Files with missing lines (5)
File Patch % Lines
org-list.ts 95.10% ⚠️ 19 Missing
view.ts 94.34% ⚠️ 12 Missing
log.ts 97.18% ⚠️ 5 Missing
output.ts 89.47% ⚠️ 2 Missing
list-command.ts 99.35% ⚠️ 1 Missing
Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
+ Coverage    95.42%    95.45%    +0.03%
==========================================
  Files          141       141         —
  Lines        19672     19768       +96
  Branches         0         0         —
==========================================
+ Hits         18771     18868       +97
- Misses         901       900        -1
- Partials         0         0         —

Generated by Codecov Action

@BYK
Copy link
Copy Markdown
Member Author

BYK commented Mar 9, 2026

Re: BugBot — Fields filtering operates on wrapper, not nested data

Fixed in the latest push. Created a writeJsonList helper that filters each array element individually before wrapping in {data, hasMore, ...}. All list-style commands (issue list, project list, trace list, org list) now use writeJsonList instead of manually constructing the wrapper + writeJson.

For non-list wrappers like issue view ({issue, event, trace}), --fields correctly filters the top-level keys — e.g. --fields issue returns just the issue object, and --fields issue.id,issue.title drills into nested fields via dot-notation. This is the intended behavior for those commands.

Also added 17 dedicated tests for writeJsonList covering per-element filtering, wrapper metadata preservation, and edge cases.

@BYK BYK force-pushed the feat/fields-flag branch from db73f9d to 5e68615 Compare March 9, 2026 22:11
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

@BYK
Copy link
Copy Markdown
Member Author

BYK commented Mar 9, 2026

Re: BugBot — Truthy check drops falsy nextCursor values silently

Fixed in this push. Changed if (nextCursor) to if (nextCursor !== null && nextCursor !== undefined && nextCursor !== ""). Empty-string and null cursors are now explicitly omitted (empty string is semantically equivalent to "no cursor"), while any real cursor string is included. Added a test for the empty-string case.

@BYK
Copy link
Copy Markdown
Member Author

BYK commented Mar 9, 2026

Re: Seer — hasMore: true without nextCursor

Not a bug. In multi-target mode (handleResolvedTargets), hasMore means "results were truncated to fit the --limit budget" and does NOT mean "there's a next page to fetch." This is by design — the compound cursor for multi-target pagination is passed via the separate --cursor flag path, not the nextCursor envelope field.

The single-target org-all mode always pairs hasMore with a valid nextCursor, which is the standard pagination contract. The multi-target mode intentionally breaks this because the pagination model is fundamentally different (per-project cursors encoded as JSON).

@BYK BYK force-pushed the feat/fields-flag branch from 4ca82c5 to 1f6e740 Compare March 9, 2026 22:23
@BYK
Copy link
Copy Markdown
Member Author

BYK commented Mar 9, 2026

Re: BugBot — Redundant fields parameter alongside flags.fields in option types (log/list.ts)

Fixed. Removed the standalone fields?: string[] from SingleFetchOptions, TraceSingleFetchOptions, and FollowConfig. All three now use flags.fields directly. Also applied the same fix to OrgAllIssuesOptions and ResolvedTargetsOptions in issue/list.ts (from the earlier BugBot comment on the previous push).

BYK added 2 commits March 9, 2026 22:40
Add a --fields flag to all 14 commands that support --json output,
allowing agents (and humans) to request only specific fields. This
reduces token consumption and keeps agent context windows focused.

Features:
- Comma-separated field selection: --fields id,title,status
- Dot-notation for nested fields: --fields id,metadata.value
- Array support: each element filtered independently
- Silently ignores missing fields (no error for non-existent paths)

Implementation:
- filterFields() in src/lib/formatters/json.ts handles dot-notation
  traversal, nested object reconstruction, and array mapping
- parseFieldsList() parses comma-separated input with dedup and trim
- writeJson() accepts optional fields parameter for filtering
- Shared FIELDS_FLAG constant in src/lib/list-command.ts
- Threaded through all writeJson call sites in 14 command files

Fixes #347
…dCommand

Lift --json and --fields flag definitions out of 17 individual commands
into buildCommand's output mode system. When a command declares
output: "json", the wrapper automatically:

- Injects --json (boolean) and --fields (parsed) flags
- Pre-parses --fields from comma-string to string[] before func runs
- Respects command-owned --json flags (skips injection, still adds --fields)

Also introduces writeJsonList() helper that applies --fields filtering
to each array element inside the {data, hasMore} wrapper, fixing the
BugBot-reported issue where filtering operated on the wrapper instead
of the nested data.

Changes:
- src/lib/command.ts: JSON_FLAG, FIELDS_FLAG constants, output mode logic
- src/lib/formatters/json.ts: writeJsonList() with extra? option
- src/lib/formatters/output.ts: fields? on WriteOutputOptions
- src/lib/list-command.ts: passes output through to buildCommand
- src/lib/org-list.ts: fields on BaseListFlags, writeJsonList at wrapper site
- 17 command files: migrated to output: "json", removed manual flag defs
- 3 commands gained --fields: auth/refresh, org/view, trace/logs
- test/lib/command.test.ts: 14 new tests for json injection + fields parsing
- test/lib/formatters/json.test.ts: 17 new tests for writeJsonList
@BYK BYK force-pushed the feat/fields-flag branch from 63ea406 to 4dab6b2 Compare March 9, 2026 22:44
@BYK BYK merged commit 45903a8 into main Mar 10, 2026
21 checks passed
@BYK BYK deleted the feat/fields-flag branch March 10, 2026 09:33
BYK added a commit that referenced this pull request Mar 10, 2026
## Phase 2: `writeOutput` convergence

Builds on #373 (`output: "json"` centralization).

Enhance `writeOutput` with two new options and migrate 3 Tier 1 commands
to use it.

### New options on `writeOutput`

- **`footer?: string`** — muted hint after human output, suppressed in
JSON mode. Replaces separate `writeFooter()` calls.
- **`jsonData?: J`** — separate data object for JSON when the serialized
shape differs from the human formatter's input.

### Commands migrated

| Command | What changed |
|---------|-------------|
| `auth/whoami` | `jsonData` narrows user to `{id, name, username,
email}` for JSON |
| `auth/refresh` | Inline `formatHuman` replaces 3-branch if/else |
| `issue/explain` | `footer` replaces separate `writeFooter()` call |

Commands with multi-part output or divergent data assembly
(`issue/view`, `event/view`, `trace/view`, `project/create`, `log/view`)
are intentionally left as-is — they don't fit the single-formatter
pattern.

### Tests

16 new tests in `test/lib/formatters/output.test.ts` cover JSON mode,
human mode, footer, detectedFrom, and jsonData divergence.
BYK added a commit that referenced this pull request Mar 10, 2026
## Summary

Phase 3 of the output convergence plan ([Phase 1:
#373](#373), [Phase 2:
#376](#376)).

Commands can now declare an `OutputConfig` on `buildCommand` and return
bare data from `func`. The wrapper intercepts the return value and
renders automatically — selecting human vs JSON format based on
`--json`, applying `--fields` filtering, and writing to stdout. Runtime
hints like detection source or next-step footers use `[data, { hint }]`
tuples.

### What changed

**Core infrastructure** (`src/lib/formatters/output.ts`,
`src/lib/command.ts`):
- `OutputConfig<T>` — declared on `buildCommand` with `human` formatter
- `OutputMeta` — runtime hints (`{ hint?: string }`) returned via tuple
- `renderCommandOutput()` — renders data using config + merged runtime
context
- `buildCommand` `output` field accepts two forms:
  - `"json"` (string) — flag injection only (`--json`, `--fields`)
  - `{ json: true, human: fn }` — flag injection + auto-render
- Detects bare data vs `[data, meta]` tuple returns; `void`/`Error`
returns are ignored
- Renamed `detectedFrom` → `hint` throughout the output layer (callers
compose full message)

**4 Category A commands migrated** to return-based pattern:
- `auth/whoami` — `return user`
- `auth/refresh` — `return payload` with `formatRefreshResult` in config
- `issue/explain` — `return [causes, { hint }]` for next-step footer
- `org/view` — `return [org, { hint }]` for detection source, or bare
`org`

**jsonData eliminated** — single canonical data object for both JSON and
human renderers. The `--fields` flag handles field selection for
context-window-friendly output.

**JSON shapes normalized** for consistent `jq` ergonomics:
- `issue/view` — always includes `event: null` instead of omitting the
key
- `log/view` — always emits array (was single object for single ID)
- `project/view` — always emits array (was single object for single
project)

**Removed** (replaced by simpler pattern):
- `OutputResult<T, J>` branded type
- `output()` helper function
- `isOutputResult()` / `renderOutputResult()` functions
- `WriteOutputDivergentOptions<T, J>` type and `jsonData` from all
output APIs

**Tests**: All assertions updated for new shapes. Zero regressions (260
pre-existing failures on main, same on branch).

### Design decisions

- **Two-form output**: `"json"` for flag-only injection (most commands),
`OutputConfig` for full auto-render (4 migrated commands). No breaking
changes to existing `output: "json"` commands.
- **Bare return, not branded wrapper**: Commands return plain data
instead of `output(data, opts)`. Simpler, less ceremony, no brand symbol
machinery.
- **`[data, { hint }]` for runtime context**: Hints depend on
execution-time values (resolved issue arg, detection source). Declared
at return time, not build time.
- **`hint` replaces `detectedFrom` + `footer`**: Single generic field.
Callers compose the full text.
- **No jsonData**: One data shape for both human and JSON renderers. Use
`--fields` to control what appears in JSON output.
- **Always-array JSON**: Commands that return data return arrays even
for single results, for consistent `jq` pipelines.
- **Complex commands deferred**: event/view, trace/view, project/create,
issue/plan have `--web`, polling, stderr progress, etc. These need a
more advanced pattern (likely `buildStreamingCommand` variant) in
follow-up PRs.

### Stats
14 files changed, 756 insertions, 305 deletions
BYK added a commit that referenced this pull request Mar 10, 2026
Convert event view, issue view, issue plan, project create, and trace
view to the new return-based output pattern where commands return
{data} and the framework handles JSON serialization and human
formatting.

Each command now declares an output config with json: true and a human
formatter function, replacing manual stdout.write + writeJson calls.

Changes:
- event/view: extract buildEventData + formatEventOutput
- issue/view: extract buildIssueData + formatIssueOutput
- issue/plan: extract buildPlanData + formatPlanOutput, remove
  outputSolution helper
- project/create: extract formatProjectCreateOutput
- trace/view: extract buildTraceData + formatTraceOutput

Infrastructure:
- Add OutputResult type export from lib/command.ts
- Add writeHuman helper to formatters/output.ts for consistent
  newline-terminated human output
- Update tests to match new return signatures

Part of the output convergence plan (PR sequence #373#376#380).
BYK added a commit that referenced this pull request Mar 10, 2026
)

## Summary

Convert 5 remaining Tier 1 commands to the return-based
`CommandOutput<T>` pattern introduced in #380.

Each command now returns `{ data, hint?, footer? }` and declares an
`OutputConfig` with `json: true` and a `human` formatter — the framework
handles JSON serialization and human-readable rendering.

## Commands converted

| Command | Human formatter | Return shape |
|---------|----------------|-------------|
| `event/view` | `formatEventView()` | `{ data: { event, trace,
spanTreeLines }, hint }` |
| `issue/view` | `formatIssueView()` | `{ data: { issue, event, trace,
spanTreeLines }, footer }` |
| `issue/plan` | `formatPlanOutput()` | `{ data: { run_id, status,
solution } }` |
| `project/create` | `formatProjectCreated()` | `{ data:
ProjectCreatedResult }` |
| `trace/view` | `formatTraceView()` | `{ data: { summary, spans,
spanTreeLines }, footer }` |

## Infrastructure changes

- Added `footer?: string` to `CommandOutput<T>` and `RenderContext`
- `renderCommandOutput()` now renders footer via `writeFooter()`
- `handleReturnValue` in `command.ts` passes `value.footer` through

## What was removed

- `writeHumanOutput()` from trace/view (replaced by `formatTraceView`)
- `outputSolution()` from issue/plan (replaced by `formatPlanOutput` +
`buildPlanData`)
- Manual `writeJson`/`writeFooter`/`stdout.write` calls from all 5
commands
- `Writer` type imports (no longer needed)

## Test updates

- `test/commands/trace/view.func.test.ts`: `writeHumanOutput` tests
replaced with `formatTraceView` pure function tests
- `test/commands/project/create.test.ts`: `parsed.slug` →
`parsed.project.slug` (JSON shape now uses full `ProjectCreatedResult`)

Net: **-33 lines** (188 insertions, 221 deletions)

Part of the output convergence plan: #373#376#380 → this PR
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add --fields flag for context-window-friendly JSON output

1 participant