Skip to content

feat: add api subcommand for raw GraphQL access#121

Open
bendrucker wants to merge 8 commits intoschpet:mainfrom
bendrucker:add-api-subcommand
Open

feat: add api subcommand for raw GraphQL access#121
bendrucker wants to merge 8 commits intoschpet:mainfrom
bendrucker:add-api-subcommand

Conversation

@bendrucker
Copy link
Contributor

@bendrucker bendrucker commented Feb 3, 2026

Adds a linear api subcommand for making raw GraphQL requests, mirroring gh api conventions.

Changes

  • Accepts a GraphQL query as a positional arg, from stdin with -, or via auto-detected piped input
  • --variable key=value for typed variable coercion (booleans, numbers, null, @file for file reads, @- for stdin)
  • --variables-json '{"key": "value"}' for passing all variables as a JSON object (merged with --variable, which takes precedence)
  • --paginate walks pageInfo.endCursor automatically and outputs concatenated nodes array
  • --silent suppresses response output while exit code still reflects errors
  • Pretty-prints JSON when stdout is a TTY, raw JSON otherwise for piping to jq
  • Exits with code 1 on HTTP errors (status >= 400) and GraphQL-level errors
  • Uses raw fetch so users see the exact server response including both data and errors fields

Testing

  • Snapshot tests using MockLinearServer cover query resolution, variable handling (type coercion, @file, --variables-json, precedence), output modes, pagination (multi-page, single-page, non-connection), auth errors, and --silent behavior for both successful and HTTP error responses
  • Manual testing against live Linear API: cycles, workflow states, notifications (with --paginate), non-existent issue lookup, variable type mismatch errors, stdin piping

Related

@bendrucker
Copy link
Contributor Author

bendrucker commented Feb 3, 2026

Fixing CI... (forgot to re-generate the skill)

Also backing out some of those unrelated changes in the skill refs, just the SKILL.md + template

@schpet
Copy link
Owner

schpet commented Feb 3, 2026

@bendrucker nice one! i'll take this for a spin tomorrow

Use `after` instead of `endCursor` as the injected pagination variable
to match Linear's GraphQL schema conventions.

Add tests for: no API key, null/false coercion, values containing
equals signs, single-page pagination, non-connection pagination with
--paginate, and file-not-found errors for -F @path.
@bendrucker
Copy link
Contributor Author

bendrucker commented Feb 3, 2026

❤️ I also need to kick the tires on this a bit, feel free to review but I'm gonna convert back to a draft pending a bit more manual testing (and CI fix).

@bendrucker bendrucker marked this pull request as draft February 3, 2026 17:21
Copy link
Owner

@schpet schpet left a comment

Choose a reason for hiding this comment

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

i think in general, at least to start, going with a more json oriented api might be more simple and easy? open to ideas but that's my hunch

"-F, --typed-field <field:string>",
"Typed variable in key=value format (coerces booleans, numbers, null; @file reads from path)",
{ collect: true },
)
Copy link
Owner

@schpet schpet Feb 3, 2026

Choose a reason for hiding this comment

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

i feel like just requiring json inputs for variables is a lot less likely to cause grief, WYT?

i assume in this day and age, this feature will mostly be used by agents and i would expect them to have an easier time with something like this

linear api query "$(cat /tmp/query.graphql)" --variables "$(cat /tmp/variables.json)"

than with this:

linear api query "$(cat /tmp/query.graphql)" -f foo=bar -f abc=123

Copy link
Contributor Author

Choose a reason for hiding this comment

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

easier time with something like this... than with this

Why though? If the agent is actually writing the variables to a temporary file, that's an extra tool call. If it's passing a JSON string directly, it's negligibly fewer tokens. JSON itself is more, but eliminating the repeated -f flags makes it come out ahead by a token or two for the example.

In any case, this came from gh, where the api subcommand deals with both REST and GraphQL. The field flags make a lot more sense with REST. Since Linear is just GQL, it probably does make sense to diverge. But I'd still tend to expect --variable foo=bar --variable abc=123 would perform equal or better to --variables '{"foo":"bar","abc":123}' for that simple case of a few variables.

Having --variables is going to be most beneficial when the variables come from some data. If they are already on disk or otherwise are emitted as the output of some command, having to split them into repeated flags is extra work.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

https://github.com/hasura/graphqurl

Some more design inspiration. I don't want to overload an agent with options either, but --variable and --variable-json is an option to allow for both cases.

Copy link
Owner

Choose a reason for hiding this comment

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

got it, yeah i think you see a clear design here! i am just averse to string parsing, but i think you make a good case for it. i'm onboard.

Copy link
Contributor Author

@bendrucker bendrucker Feb 4, 2026

Choose a reason for hiding this comment

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

Will definitely think through this more, arguably the most important tradeoff is this:

  • Linear is just GraphQL, gh api is designed for REST and adapted for GraphQL, there's no such need to span both styles here
  • gh is heavily represented in LLM training data and even in system prompts (e.g., Claude Code), though probably rarely with -f

That said this gets hard to pin down, because similarity to popular tools could be beneficial, but could also be confusing to an agent trying to apply known patterns to a tool with similar but subtly different semantics.

Rather than try to infer how this might work by squinting at it, I'm just going to end up just testing this on a variety of natural language queries to try to back up some of these idea with some limited amount of data. If there's an obvious winner on performance that's the answer, otherwise if the agent is equally happy with any format there's more room for human-friendly syntax.

Copy link
Owner

Choose a reason for hiding this comment

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

cool, i'm very into the 'desire path' (thank you yegge for coining that!) approach.

i'm familiar with tools like graphiql which make you use json for variables so maybe that informs my interest in that option, too.

appreciate your consideration around this, looking forward to improving the plumbing.

Copy link
Contributor Author

@bendrucker bendrucker Feb 4, 2026

Choose a reason for hiding this comment

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

Added notes from a quick ad-hoc eval below, --variables-json and --variable seems like a winner. Provides flexibility to do things like have JSON variables in a file but a single override. Agent still does a good job of picking plain flags for simple cases and JSON for complex ones.

#121 (comment)

Copy link
Owner

Choose a reason for hiding this comment

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

awesome, thanks so much! i'll aim to try this out tonight. please feel free to drop the 'draft' status when you consider this PR to be merge-ready.

export const apiCommand = new Command()
.name("api")
.description("Make a raw GraphQL API request")
.arguments("[query:string]")
Copy link
Owner

Choose a reason for hiding this comment

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

would be good imo to support mutations, too

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this should be supported and just needs clearer docs, will update

Redesign the api subcommand variable interface for GraphQL-native semantics:

- --variable key=value (repeated) with type coercion and @file support
- --variables-json for complex nested objects
- --variable takes precedence over --variables-json on key conflicts
- Custom Cliffy Type for key=value parsing instead of hand-rolled split
- Fix deno fmt issue on api-filter.json fixture
@bendrucker
Copy link
Contributor Author

Ran claude --print (Sonnet) with the skill doc as system prompt to test whether agents can correctly use the redesigned variable flags. 5 prompts × 3 runs for 15 samples.

Results

Prompt Runs Flag usage Score
Single string variable (--variable id=abc-123) 3/3 correct --variable 3/3
Numeric variable (--variable first=10) 3/3 correct --variable 3/3
Complex nested filter (IssueFilter) 3/3 correct --variables-json 3/3
Mutation with multiple variables 3/3 correct --variable × 2 3/3
Mixed types (string + numeric filter) 3/3 correct --variable × 2 3/3
  • No confusion between the two flags across any run
  • Values with spaces handled correctly (--variable title='Fix login bug')
  • Type coercion worked as expected (numeric values passed as --variable first=10 without quoting)

Sample Outputs

Simple Variable

linear api 'query($id: String\!) { issue(id: $id) { id identifier title } }' --variable id=abc-123

Complex Filter

linear api 'query($filter: IssueFilter\!) { issues(filter: $filter) { nodes { identifier title } } }' \
  --variables-json '{"filter": {"state": {"name": {"eq": "In Progress"}}}}'

Multiple Variables

linear api 'mutation($title: String\!, $teamId: String\!) { issueCreate(input: { title: $title, teamId: $teamId }) { success issue { id identifier title } } }' \
  --variable title='Fix login bug' --variable teamId=team-xyz

Methodology

  • Model: Claude Sonnet via claude --print --model sonnet
  • System prompt: full SKILL.md content
  • Each prompt explicitly asked for parameterized GraphQL with variables (to avoid the agent correctly choosing higher-level CLI commands like linear issue view)
  • 3 independent runs per prompt to check consistency

The --silent flag was not suppressing output when the API returned HTTP
400+ status codes in both executeSingle and executePaginated paths.
@bendrucker bendrucker marked this pull request as ready for review February 6, 2026 17:56
@bendrucker
Copy link
Contributor Author

bendrucker commented Feb 6, 2026

Ready for review! Did another self-review/testing pass on this, and only came up with 9647a7d.

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 api subcommand for raw GraphQL access

2 participants