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
- 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?
- Scope: PR both flags together as one
feat(output): commit, or split into two PRs (--compact first as a trivially-safe change, --fields second)?
- 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).
Add
--compactand--fieldsflags for token-efficient outputProblem
linearisis positioned for "LLM agents and humans who prefer structured data" (frompackage.jsondescription), and theusagetwo-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:For agent consumers this leaves real token waste on the table:
CompleteIssueFieldsfragment returns ~30 fields per issue (state, assignee, team, project, labels, cycle, milestone, parent, children, relations, inverseRelations, …). A 50-issueissues listis ~25–40k tokens at default; an agent that only needsidentifier + title + state.namecan't trim it.Today the only workaround is post-processing with
jqin 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.namereturns{"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:
outputSuccessinsrc/common/output.ts. Module-levelcurrentOutputOptionspopulated once viaprogram.hook("preAction", (_t, action) => setOutputOptions(getRootOpts(action)))inmain.ts. Keeps all ~120 existingoutputSuccess(result)call sites across 13 command modules untouched.pickFields(value, paths[])helper inoutput.ts. No codegen interaction — the data has already been serialized to a POJO by the time it reachesoutputSuccess. Mirrors the dot-path-with-array-traversal pattern Finesssee/linear-cli uses in Rust (select_fields/set_path).CommandOptionsgets two optional fields (compact?: boolean,fields?: string[]).parseFieldsList(value)is exported as a commander option-parser so the CLI gets astring[](with trim + empty-entry filtering) rather than each command re-parsing.Out of scope for v1
--fields(e.g.items[].title,state.*).outputError/outputAuthError— error structure is a separate concern.Questions for maintainers before I PR
outputSuccess(result)call sites. The alternative is makingoutputSuccess(data, opts?)accept opts explicitly and threadinggetRootOpts(command)through every command. Cleaner functionally but invasive. Preference?feat(output):commit, or split into two PRs (--compactfirst as a trivially-safe change,--fieldssecond)?outputError/outputAuthErrorpretty-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).