Skip to content

feat: add api subcommand for raw GraphQL access#121

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

feat: add api subcommand for raw GraphQL access#121
bendrucker wants to merge 10 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

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.

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.

couple changes - let me know if you agree! open to push back. also depending on your bandwidth, i'm happy to make the changes i suggested for you.

variables: Record<string, unknown>,
headers: Record<string, string>,
silent: boolean,
): Promise<void> {
Copy link
Owner

Choose a reason for hiding this comment

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

i'm worried this pagination strategy only supports certain graphql queries. e.g. if a graphql document is supplied that has two different fields, it will not work right?

linear api 'query GetBoth($after: String) {
  viewer {
    assignedIssues(first: 2, after: $after) {
      nodes { identifier }
      pageInfo { hasNextPage endCursor }
    }
    createdIssues(first: 2, after: $after) {
      nodes { identifier }
      pageInfo { hasNextPage endCursor }
    }
  }
}' --paginate

can you either change this to work reliably, or remove the pagination feature? my recommendation would be to omit it from this PR so we can land it, and if you determine reliable pagination is viable to implement that in a follow up PR.

if dropping it, perhaps we can bake in someting into the help text that helps agents understand they need to paginate stuff, i.e. suggest using pageInfo and cursor based pagination, something like that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good flag, I've used the paginate flag heavily with gh, but this is a legitimate sharp edge. I'll look into how gh attempts to deal with this, if at all (e.g., does it error when --paginate is used with an incompatible query?). If there's no clean and simple answer, taking it out sounds fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looked into it — gh api --paginate has the exact same limitation, it just silently paginates the first connection and ignores the rest. Added detection that errors if there are multiple connections, with guidance to paginate manually. Also added test coverage for the nested connection case (sub-issues inside issues) to make sure that doesn't false-positive.

return variables
}

async function resolveTypedValue(value: string): Promise<unknown> {
Copy link
Owner

Choose a reason for hiding this comment

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

this relates to prev discussion

without GraphQL schema type information, there's no way to know if "007" should be string "007" or number 7

repro/explanation here:
https://gist.github.com/schpetbot/60dc745b48cd1fe1f7cf31a9d60421e2

i DO think you could do this with graphql, but are you up for omitting this way of providing variables and doing json to start, and splitting the variable entry tuple parsing stuff on follow up PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, similar to above I will think on this a little and if there's is no simple answer I'll cut this into a series of 2+ stacked PRs so you can land the simple parts and we can keep iterating on the trickier bits.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a roundtrip check that handles this plus a bunch of adjacent cases — hex, octal, leading +, trailing decimals like 1.0, etc. all stay as strings. Added test coverage for leading zeros and scientific notation specifically. --variables-json is still there as the escape hatch.

@bendrucker
Copy link
Contributor Author

Added workarounds for both comments, but happy to back these features out for this PR as well.

String(Number(value)) === value prevents "007" from becoming 7 and
"1e5" from becoming 100000.
gh api --paginate silently paginates only the first connection.
Instead of replicating that footgun, detect multiple connections in
the response and error with guidance to paginate manually.
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