Skip to content

Add --compact and --fields flags for token-efficient output #220

@EthycalOne

Description

@EthycalOne

Add --compact and --fields flags for token-efficient output

Problem

linearis is positioned for "LLM agents and humans who prefer structured data" (from package.json description), and the usage two-tier discovery pattern materially beats the official Linear MCP at session bootstrap. But the actual response payloads are still full-shape, pretty-printed JSON via:

// src/common/output.ts
export function outputSuccess(data: unknown): void {
  console.log(JSON.stringify(data, null, 2));
}

For agent consumers this leaves real token waste on the table:

  • Pretty-printed JSON is ~40–50% whitespace.
  • The CompleteIssueFields fragment returns ~30 fields per issue (state, assignee, team, project, labels, cycle, milestone, parent, children, relations, inverseRelations, …). A 50-issue issues list is ~25–40k tokens at default; an agent that only needs identifier + title + state.name can't trim it.

Today the only workaround is post-processing with jq in the agent's bash call, which is framing the problem on the agent side rather than at the CLI boundary where the same data shape is needed by every consumer.

Proposal

Add two global output-shaping flags (mirrors --api-token's plumbing):

  • --compact — emit single-line JSON (no indent). Pure whitespace removal, no shape change.
  • --fields <list> — comma-separated dot-paths to include, preserving nested object shape. Supports mid-path array traversal so --fields labels.nodes.name returns {"labels":{"nodes":[{"name":"bug"}, …]}}.

Examples

$ linearis issues list --limit 1
[
  {
    "identifier": "ENG-1",
    "title": "Fix login bug",
    "state": { "id": "...", "name": "In Progress", "type": "started" },
    "assignee": { ... },
    "team": { ... },
    "project": { ... },
    "labels": { "nodes": [...] },
    ...
  }
]

$ linearis --compact --fields identifier,title,state.name issues list --limit 1
[{"identifier":"ENG-1","title":"Fix login bug","state":{"name":"In Progress"}}]

A 50-issue list goes from ~25–40k tokens to ~2–3k tokens with both flags applied.

Default behavior unchanged

With neither flag set, output is the same indented full-shape JSON as today. Error envelopes (outputError, outputAuthError) are intentionally left pretty-printed — they're read by humans and the structure should remain stable.

Implementation sketch

I have a working branch ready (~218 LOC, 4 files touched, all existing tests pass + 10 new cases for the helpers). Happy to PR if greenlit. Approach:

  • Single change site: outputSuccess in src/common/output.ts. Module-level currentOutputOptions populated once via program.hook("preAction", (_t, action) => setOutputOptions(getRootOpts(action))) in main.ts. Keeps all ~120 existing outputSuccess(result) call sites across 13 command modules untouched.
  • Field selection is a small recursive pickFields(value, paths[]) helper in output.ts. No codegen interaction — the data has already been serialized to a POJO by the time it reaches outputSuccess. Mirrors the dot-path-with-array-traversal pattern Finesssee/linear-cli uses in Rust (select_fields/set_path).
  • CommandOptions gets two optional fields (compact?: boolean, fields?: string[]).
  • parseFieldsList(value) is exported as a commander option-parser so the CLI gets a string[] (with trim + empty-entry filtering) rather than each command re-parsing.

Out of scope for v1

  • Wildcards / array-index syntax in --fields (e.g. items[].title, state.*).
  • Trimming outputError/outputAuthError — error structure is a separate concern.

Questions for maintainers before I PR

  1. Plumbing route: I went with the preAction-hook + module-state route to avoid touching all 120 outputSuccess(result) call sites. The alternative is making outputSuccess(data, opts?) accept opts explicitly and threading getRootOpts(command) through every command. Cleaner functionally but invasive. Preference?
  2. Scope: PR both flags together as one feat(output): commit, or split into two PRs (--compact first as a trivially-safe change, --fields second)?
  3. Error-path behavior: keep outputError/outputAuthError pretty-printed regardless of --compact, or also honor the flag for stderr? My draft keeps them unchanged.

Happy to push the draft branch to a fork if it'd help review, but waiting on direction here first per CONTRIBUTING.md ("Please open an issue first to discuss" for larger features).

Metadata

Metadata

Assignees

Labels

priority:P1Highest-priority planned work; should be implemented next
No fields configured for Feature.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions