ask-linear 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +157 -0
- data/lib/ask/linear/client.rb +117 -0
- data/lib/ask/linear/context.rb +42 -0
- data/lib/ask/linear/error_guide.rb +107 -0
- data/lib/ask/linear/version.rb +7 -0
- data/lib/ask-linear.rb +6 -0
- metadata +118 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2cdf635069d34d0554d7e73ff22a0b589627ab59ace75b8376cfff7b11a502d5
|
|
4
|
+
data.tar.gz: 81cc288e84cb63fb8b622032adbdc911844cbe29097d2b99f5d0a19548fc8f69
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 75e4a052079f42fdb445fcaea78010597c19708a5872a399c970b0f28f72c0ce77a76c0b6df4fe9d2c5681a060cff28cba750fd8de999e6438f745297c7ce87f
|
|
7
|
+
data.tar.gz: e4e4cb88175a66383b416409f85e05ad2661168f28df182dd82c230429d3ec1b2fef341886f12255a3434af258d74bfc1a40beeff104298f6e4d59b53bbcd631
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kaka Ruto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# ask-linear
|
|
2
|
+
|
|
3
|
+
Linear service context for AI agents in the ask-rb ecosystem.
|
|
4
|
+
|
|
5
|
+
Provides an authenticated GraphQL client, metadata constants for system prompts, and a structured error guide for common Linear API issues.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
gem "ask-linear"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
### Get an authenticated client
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
require "ask-linear"
|
|
17
|
+
|
|
18
|
+
client = Ask::Linear.client
|
|
19
|
+
|
|
20
|
+
# List all teams
|
|
21
|
+
result = client.query("query { teams { nodes { id key name } } }")
|
|
22
|
+
|
|
23
|
+
# Create an issue
|
|
24
|
+
result = client.query(
|
|
25
|
+
"mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { id identifier title url } } }",
|
|
26
|
+
{ input: { teamId: "TEAM_ID", title: "My issue", description: "Description here" } }
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Fetch a specific issue
|
|
30
|
+
result = client.query(
|
|
31
|
+
"query($id: String!) { issue(id: $id) { id identifier title description state { name } assignee { name } url } }",
|
|
32
|
+
{ id: "ISSUE_ID" }
|
|
33
|
+
)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The client is a thin wrapper over Faraday that sends GraphQL queries to `https://api.linear.app/graphql`. Wrapped in a proxy that converts `Faraday::UnauthorizedError` (HTTP 401) into `Ask::Auth::InvalidCredential` with actionable error messages.
|
|
37
|
+
|
|
38
|
+
### Authentication
|
|
39
|
+
|
|
40
|
+
The client resolves a Linear API key via `Ask::Auth.resolve(:linear_api_key)`. API keys can be provided through any configured auth provider:
|
|
41
|
+
|
|
42
|
+
1. **Environment variable:** `LINEAR_API_KEY`
|
|
43
|
+
2. **Credentials file:** `~/.ask/credentials.yml`
|
|
44
|
+
3. **Rails credentials:** `Rails.application.credentials.linear_api_key`
|
|
45
|
+
4. **OAuth / Database:** Custom providers
|
|
46
|
+
|
|
47
|
+
Generate an API key at [linear.app/settings/api](https://linear.app/settings/api).
|
|
48
|
+
|
|
49
|
+
## Context Constants
|
|
50
|
+
|
|
51
|
+
Use these constants to build system prompts for AI agents:
|
|
52
|
+
|
|
53
|
+
| Constant | Value |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `Ask::Linear::DESCRIPTION` | "Linear — issue tracking, project management, roadmaps, sprints" |
|
|
56
|
+
| `Ask::Linear::DOCS_URL` | https://developers.linear.app/docs |
|
|
57
|
+
| `Ask::Linear::GRAPHQL_URL` | https://api.linear.app/graphql |
|
|
58
|
+
| `Ask::Linear::AUTH_NAME` | `:linear_api_key` |
|
|
59
|
+
| `Ask::Linear::AUTH_HOW` | "https://linear.app/settings/api — generate a personal API key" |
|
|
60
|
+
| `Ask::Linear::GEM_NAME` | `"faraday"` |
|
|
61
|
+
| `Ask::Linear::QUICK_START` | Copy-paste Ruby code snippet with common GraphQL operations |
|
|
62
|
+
|
|
63
|
+
## Error Guide
|
|
64
|
+
|
|
65
|
+
`Ask::Linear::Errors` provides structured knowledge for agents:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
# Look up GraphQL error extension codes
|
|
69
|
+
Ask::Linear::Errors.for("AUTHENTICATION_ERROR")
|
|
70
|
+
# => { message: "...", action: "..." }
|
|
71
|
+
|
|
72
|
+
# Describe HTTP status codes
|
|
73
|
+
Ask::Linear::Errors.status_code_description(401)
|
|
74
|
+
# => "Unauthorized — API key is missing, invalid, or revoked."
|
|
75
|
+
|
|
76
|
+
# Rate limit info
|
|
77
|
+
Ask::Linear::Errors::RATE_LIMIT[:authenticated]
|
|
78
|
+
# => "100 requests per minute per API key"
|
|
79
|
+
|
|
80
|
+
# Pagination guidance
|
|
81
|
+
Ask::Linear::Errors::PAGINATION[:cursor_based]
|
|
82
|
+
# => "Linear uses cursor-based pagination with first/after or last/before arguments."
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Supported Error Codes
|
|
86
|
+
|
|
87
|
+
| Extension Code | When It Occurs |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `AUTHENTICATION_ERROR` | Missing, invalid, or revoked API key |
|
|
90
|
+
| `FORBIDDEN` | API key lacks permission for the resource |
|
|
91
|
+
| `NOT_FOUND` | Resource doesn't exist or is inaccessible |
|
|
92
|
+
| `RATE_LIMITED` | API rate limit exceeded (100 req/min/key) |
|
|
93
|
+
| `INPUT_VALIDATION_ERROR` | Input data fails validation |
|
|
94
|
+
| `DUPLICATE_INPUT` | Resource with same data already exists |
|
|
95
|
+
| `INTERNAL_ERROR` | Linear server error |
|
|
96
|
+
| `USER_SUSPENDED` | Authenticated user account is suspended |
|
|
97
|
+
| `WORKSPACE_SUSPENDED` | Workspace is suspended or deactivated |
|
|
98
|
+
|
|
99
|
+
## Client API
|
|
100
|
+
|
|
101
|
+
### `Ask::Linear.client`
|
|
102
|
+
|
|
103
|
+
Returns an authenticated `Ask::Linear::Client` wrapped in a `ClientProxy` that catches 401 errors.
|
|
104
|
+
|
|
105
|
+
### `client.query(gql, variables = {})`
|
|
106
|
+
|
|
107
|
+
Executes a GraphQL query or mutation against the Linear API.
|
|
108
|
+
|
|
109
|
+
**Arguments:**
|
|
110
|
+
- `gql` (String) — The GraphQL query or mutation string
|
|
111
|
+
- `variables` (Hash) — Variables to interpolate into the query (optional)
|
|
112
|
+
|
|
113
|
+
**Returns:** Hash with `"data"` key containing the response
|
|
114
|
+
|
|
115
|
+
**Raises:**
|
|
116
|
+
- `Ask::Auth::MissingCredential` if no API key is configured
|
|
117
|
+
- `Ask::Auth::InvalidCredential` if the API key returns 401
|
|
118
|
+
- `RuntimeError` if the API returns GraphQL errors
|
|
119
|
+
|
|
120
|
+
### Example: Common Operations
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
client = Ask::Linear.client
|
|
124
|
+
|
|
125
|
+
# List teams
|
|
126
|
+
teams = client.query("query { teams { nodes { id key name } } }")
|
|
127
|
+
|
|
128
|
+
# List my assigned issues
|
|
129
|
+
my_issues = client.query("query { viewer { assignedIssues { nodes { id identifier title state { name } } } } }")
|
|
130
|
+
|
|
131
|
+
# Get workflow states for a team
|
|
132
|
+
states = client.query("query($teamId: String!) { team(id: $teamId) { states { nodes { id name type } } } }",
|
|
133
|
+
teamId: "TEAM_ID")
|
|
134
|
+
|
|
135
|
+
# Update issue state
|
|
136
|
+
result = client.query(
|
|
137
|
+
"mutation($id: String!, $input: IssueUpdateInput!) { issueUpdate(id: $id, input: $input) { success issue { id identifier state { name } } } }",
|
|
138
|
+
{ id: "ISSUE_ID", input: { stateId: "STATE_ID" } }
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Add comment
|
|
142
|
+
result = client.query(
|
|
143
|
+
"mutation($input: CommentCreateInput!) { commentCreate(input: $input) { success comment { id body } } }",
|
|
144
|
+
{ input: { issueId: "ISSUE_ID", body: "Working on this" } }
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Development
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
bundle install
|
|
152
|
+
bundle exec rake test
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "ask/auth"
|
|
5
|
+
|
|
6
|
+
module Ask
|
|
7
|
+
module Linear
|
|
8
|
+
# Returns an authenticated GraphQL client for the Linear API.
|
|
9
|
+
#
|
|
10
|
+
# Resolves the API key via +Ask::Auth.resolve(:linear_api_key)+ and
|
|
11
|
+
# returns a +Client+ that wraps Faraday and sends GraphQL queries to
|
|
12
|
+
# +https://api.linear.app/graphql+.
|
|
13
|
+
#
|
|
14
|
+
# Configuration:
|
|
15
|
+
# - +read_timeout+: +30+ seconds
|
|
16
|
+
# - +open_timeout+: +10+ seconds
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# client = Ask::Linear.client
|
|
20
|
+
# result = client.query("query { teams { nodes { id name } } }")
|
|
21
|
+
#
|
|
22
|
+
# @return [Ask::Linear::Client] an authenticated GraphQL client
|
|
23
|
+
# @raise [Ask::Auth::MissingCredential] if no Linear API key is configured
|
|
24
|
+
# @raise [Ask::Auth::InvalidCredential] if the API key is rejected (401)
|
|
25
|
+
# @raise [ArgumentError] if query arguments are invalid
|
|
26
|
+
# @raise [RuntimeError] if Linear returns GraphQL error messages
|
|
27
|
+
# @raise [Faraday::Error] if the HTTP request fails
|
|
28
|
+
def self.client
|
|
29
|
+
api_key = Ask::Auth.resolve(:linear_api_key)
|
|
30
|
+
ClientProxy.new(Client.new(api_key))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# A lightweight GraphQL client for the Linear API.
|
|
34
|
+
class Client
|
|
35
|
+
# Base URL for the Linear GraphQL API.
|
|
36
|
+
BASE_URL = "https://api.linear.app/graphql"
|
|
37
|
+
|
|
38
|
+
# @return [Faraday::Connection] the underlying Faraday connection
|
|
39
|
+
attr_reader :connection
|
|
40
|
+
|
|
41
|
+
# @param api_key [String] Linear personal API key
|
|
42
|
+
def initialize(api_key)
|
|
43
|
+
@api_key = api_key
|
|
44
|
+
@connection = Faraday.new(url: BASE_URL) do |f|
|
|
45
|
+
f.request :json
|
|
46
|
+
f.response :json
|
|
47
|
+
f.response :raise_error
|
|
48
|
+
f.options.read_timeout = 30
|
|
49
|
+
f.options.open_timeout = 10
|
|
50
|
+
f.adapter Faraday.default_adapter
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Execute a GraphQL query or mutation against the Linear API.
|
|
55
|
+
#
|
|
56
|
+
# @param gql [String] GraphQL query or mutation string
|
|
57
|
+
# @param variables [Hash] Variables to interpolate into the query (default: {})
|
|
58
|
+
# @return [Hash] Parsed response body from the Linear API
|
|
59
|
+
# @raise [ArgumentError] if +gql+ is not a String or +variables+ is not a Hash
|
|
60
|
+
# @raise [RuntimeError] if the API returns errors in the response body
|
|
61
|
+
# @raise [Faraday::Error] if the HTTP request fails
|
|
62
|
+
def query(gql, variables = {})
|
|
63
|
+
unless gql.is_a?(::String)
|
|
64
|
+
raise ::ArgumentError, "gql must be a String, got #{gql.class}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
unless variables.is_a?(::Hash)
|
|
68
|
+
raise ::ArgumentError, "variables must be a Hash, got #{variables.class}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
response = @connection.post do |req|
|
|
72
|
+
req.headers["Authorization"] = @api_key
|
|
73
|
+
req.body = { query: gql, variables: variables }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
body = response.body
|
|
77
|
+
|
|
78
|
+
if body.is_a?(Hash) && body["errors"]
|
|
79
|
+
messages = body["errors"].map { |e| e["message"] }.join("; ")
|
|
80
|
+
raise ::RuntimeError, "Linear API error: #{messages}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
body
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Proxies method calls to a +Client+, converting authentication
|
|
88
|
+
# errors into +Ask::Auth::InvalidCredential+.
|
|
89
|
+
class ClientProxy < BasicObject
|
|
90
|
+
def initialize(client)
|
|
91
|
+
@client = client
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def method_missing(name, ...)
|
|
95
|
+
@client.public_send(name, ...)
|
|
96
|
+
rescue ::Faraday::UnauthorizedError => e
|
|
97
|
+
response = e.response
|
|
98
|
+
status = response.is_a?(::Hash) ? response[:status] : nil
|
|
99
|
+
if status == 401
|
|
100
|
+
::Kernel.raise ::Ask::Auth::InvalidCredential, :linear_api_key
|
|
101
|
+
end
|
|
102
|
+
::Kernel.raise
|
|
103
|
+
rescue ::Faraday::ClientError => e
|
|
104
|
+
response = e.response
|
|
105
|
+
status = response.is_a?(::Hash) ? response[:status] : nil
|
|
106
|
+
if status == 401
|
|
107
|
+
::Kernel.raise ::Ask::Auth::InvalidCredential, :linear_api_key
|
|
108
|
+
end
|
|
109
|
+
::Kernel.raise
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def respond_to_missing?(name, include_private = false)
|
|
113
|
+
@client.respond_to?(name, include_private) || super
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ask
|
|
4
|
+
module Linear
|
|
5
|
+
# Human-readable description of the Linear service context.
|
|
6
|
+
DESCRIPTION = "Linear — issue tracking, project management, roadmaps, sprints"
|
|
7
|
+
|
|
8
|
+
# Base URL for Linear API documentation.
|
|
9
|
+
DOCS_URL = "https://developers.linear.app/docs"
|
|
10
|
+
|
|
11
|
+
# URL for the Linear GraphQL API schema (available via introspection).
|
|
12
|
+
GRAPHQL_URL = "https://api.linear.app/graphql"
|
|
13
|
+
|
|
14
|
+
# Credential name used with Ask::Auth.resolve.
|
|
15
|
+
AUTH_NAME = :linear_api_key
|
|
16
|
+
|
|
17
|
+
# Instructions for obtaining a Linear personal API key.
|
|
18
|
+
AUTH_HOW = "https://linear.app/settings/api — generate a personal API key"
|
|
19
|
+
|
|
20
|
+
# Gem name for the Linear API client.
|
|
21
|
+
GEM_NAME = "faraday"
|
|
22
|
+
|
|
23
|
+
# Required gem version constraint.
|
|
24
|
+
GEM_VERSION = "~> 2.0"
|
|
25
|
+
|
|
26
|
+
# URL for Faraday Ruby library documentation.
|
|
27
|
+
GEM_DOCS = "https://lostisland.github.io/faraday"
|
|
28
|
+
|
|
29
|
+
# Quick-start Ruby code snippet for agents to copy-paste.
|
|
30
|
+
QUICK_START = <<~RUBY
|
|
31
|
+
client = Ask::Linear.client
|
|
32
|
+
# List teams
|
|
33
|
+
result = client.query("query { teams { nodes { id key name } } }")
|
|
34
|
+
# Get issue by ID
|
|
35
|
+
result = client.query("query($id: String!) { issue(id: $id) { id identifier title description url } }", { id: "ISSUE_ID" })
|
|
36
|
+
# Create issue
|
|
37
|
+
result = client.query("mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { id identifier title url } } }", { input: { teamId: "TEAM_ID", title: "New issue", description: "Description" } })
|
|
38
|
+
# List issues for a team
|
|
39
|
+
result = client.query("query { team(id: \\"TEAM_ID\\") { issues { nodes { id identifier title state { name } priority } } } }")
|
|
40
|
+
RUBY
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ask
|
|
4
|
+
module Linear
|
|
5
|
+
# Structured error knowledge for AI agents working with the Linear API.
|
|
6
|
+
#
|
|
7
|
+
# Provides human-readable guidance for common HTTP status codes, rate
|
|
8
|
+
# limiting, authentication errors, and GraphQL error extensions
|
|
9
|
+
# encountered when using the Linear GraphQL API.
|
|
10
|
+
module Errors
|
|
11
|
+
# Rate limit information.
|
|
12
|
+
#
|
|
13
|
+
# - Authenticated requests: 100 requests per minute per API key
|
|
14
|
+
#
|
|
15
|
+
# When rate-limited, Linear returns a 429 status code. The agent
|
|
16
|
+
# should wait and retry.
|
|
17
|
+
RATE_LIMIT = {
|
|
18
|
+
authenticated: "100 requests per minute per API key",
|
|
19
|
+
error_class: "Faraday::ClientError (HTTP 429)",
|
|
20
|
+
action: "Wait for the rate limit window and retry. Linear uses a sliding window rate limiter."
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
# Common HTTP status codes returned by the Linear GraphQL API and how to handle them.
|
|
24
|
+
#
|
|
25
|
+
# Linear wraps most errors in GraphQL error responses (HTTP 200 with errors body),
|
|
26
|
+
# but some authentication and rate-limit errors come through as HTTP status codes.
|
|
27
|
+
STATUS_CODES = {
|
|
28
|
+
200 => "OK — The request succeeded. Check the response body for GraphQL errors.",
|
|
29
|
+
400 => "Bad Request — The GraphQL query is malformed or invalid variables.",
|
|
30
|
+
401 => "Unauthorized — API key is missing, invalid, or revoked. Re-authenticate.",
|
|
31
|
+
403 => "Forbidden — API key lacks access to the requested resource.",
|
|
32
|
+
404 => "Not Found — The requested resource does not exist.",
|
|
33
|
+
422 => "Unprocessable Entity — Validation failed. Check request parameters.",
|
|
34
|
+
429 => "Too Many Requests — Rate limit exceeded. Wait before retrying.",
|
|
35
|
+
500 => "Internal Server Error — Linear server issue. Retry with backoff.",
|
|
36
|
+
503 => "Service Unavailable — Linear is temporarily unavailable. Retry later."
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
# Pagination guidance for the Linear GraphQL API.
|
|
40
|
+
PAGINATION = {
|
|
41
|
+
cursor_based: "Linear uses cursor-based pagination with first/after or last/before arguments.",
|
|
42
|
+
page_size: "Use the 'first' argument to control page size (default: 50, max: 100).",
|
|
43
|
+
nodes: "List fields return a connection with 'nodes', 'pageInfo', and 'edges'.",
|
|
44
|
+
page_info: "Check pageInfo.hasNextPage and pageInfo.hasPreviousPage for more pages.",
|
|
45
|
+
example: 'query { issues(first: 50, after: "cursor") { nodes { id title } pageInfo { hasNextPage endCursor } } }'
|
|
46
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
# Map of common GraphQL error messages to human-readable guidance.
|
|
49
|
+
#
|
|
50
|
+
# Linear returns errors in the GraphQL format: +{ "errors": [{ "message": "...", "extensions": { ... } }] }+
|
|
51
|
+
ERRORS = {
|
|
52
|
+
"AUTHENTICATION_ERROR" => {
|
|
53
|
+
message: "The API key is missing, invalid, or has been revoked.",
|
|
54
|
+
action: "Generate a new API key at https://linear.app/settings/api and update your credentials."
|
|
55
|
+
},
|
|
56
|
+
"FORBIDDEN" => {
|
|
57
|
+
message: "Your API key does not have permission to access this resource.",
|
|
58
|
+
action: "Check that your API key has the necessary scopes. You may need an admin to grant access."
|
|
59
|
+
},
|
|
60
|
+
"NOT_FOUND" => {
|
|
61
|
+
message: "The requested resource could not be found.",
|
|
62
|
+
action: "Verify the ID or identifier is correct. Resources may have been deleted or you may not have access."
|
|
63
|
+
},
|
|
64
|
+
"RATE_LIMITED" => {
|
|
65
|
+
message: "API rate limit exceeded.",
|
|
66
|
+
action: "Wait before retrying. Linear allows 100 requests per minute per API key."
|
|
67
|
+
},
|
|
68
|
+
"INPUT_VALIDATION_ERROR" => {
|
|
69
|
+
message: "The input data failed validation.",
|
|
70
|
+
action: "Check required fields, data types, and constraints. Linear returns detailed error messages."
|
|
71
|
+
},
|
|
72
|
+
"DUPLICATE_INPUT" => {
|
|
73
|
+
message: "A resource with the same input data already exists.",
|
|
74
|
+
action: "Check for existing resources before creating new ones with duplicate fields."
|
|
75
|
+
},
|
|
76
|
+
"INTERNAL_ERROR" => {
|
|
77
|
+
message: "Linear encountered an internal server error.",
|
|
78
|
+
action: "Retry with exponential backoff. If the issue persists, check https://linearstatus.com."
|
|
79
|
+
},
|
|
80
|
+
"USER_SUSPENDED" => {
|
|
81
|
+
message: "The authenticated user account has been suspended.",
|
|
82
|
+
action: "Contact your Linear workspace administrator to resolve the suspension."
|
|
83
|
+
},
|
|
84
|
+
"WORKSPACE_SUSPENDED" => {
|
|
85
|
+
message: "The Linear workspace has been suspended or deactivated.",
|
|
86
|
+
action: "Contact your Linear workspace administrator or check billing status."
|
|
87
|
+
}
|
|
88
|
+
}.freeze
|
|
89
|
+
|
|
90
|
+
# Look up guidance for a GraphQL error extension code.
|
|
91
|
+
#
|
|
92
|
+
# @param error_code [String] The error extension code (e.g., "AUTHENTICATION_ERROR")
|
|
93
|
+
# @return [Hash, nil] A hash with +:message+ and +:action+ keys, or nil if unknown
|
|
94
|
+
def self.for(error_code)
|
|
95
|
+
ERRORS[error_code]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Describe an HTTP status code.
|
|
99
|
+
#
|
|
100
|
+
# @param code [Integer] HTTP status code
|
|
101
|
+
# @return [String, nil] Description of the status code
|
|
102
|
+
def self.status_code_description(code)
|
|
103
|
+
STATUS_CODES[code]
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
data/lib/ask-linear.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ask-linear
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Kaka Ruto
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ask-auth
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: faraday
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: minitest
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '5.25'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '5.25'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: mocha
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.1'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '3.1'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rake
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '13.0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '13.0'
|
|
82
|
+
description: Provides authenticated GraphQL client, context metadata, and error guide
|
|
83
|
+
for AI agents working with the Linear API.
|
|
84
|
+
email:
|
|
85
|
+
- kaka@myrrlabs.com
|
|
86
|
+
executables: []
|
|
87
|
+
extensions: []
|
|
88
|
+
extra_rdoc_files: []
|
|
89
|
+
files:
|
|
90
|
+
- LICENSE
|
|
91
|
+
- README.md
|
|
92
|
+
- lib/ask-linear.rb
|
|
93
|
+
- lib/ask/linear/client.rb
|
|
94
|
+
- lib/ask/linear/context.rb
|
|
95
|
+
- lib/ask/linear/error_guide.rb
|
|
96
|
+
- lib/ask/linear/version.rb
|
|
97
|
+
homepage: https://github.com/ask-rb/ask-linear
|
|
98
|
+
licenses:
|
|
99
|
+
- MIT
|
|
100
|
+
metadata: {}
|
|
101
|
+
rdoc_options: []
|
|
102
|
+
require_paths:
|
|
103
|
+
- lib
|
|
104
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - ">="
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: '3.2'
|
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
110
|
+
requirements:
|
|
111
|
+
- - ">="
|
|
112
|
+
- !ruby/object:Gem::Version
|
|
113
|
+
version: '0'
|
|
114
|
+
requirements: []
|
|
115
|
+
rubygems_version: 4.0.3
|
|
116
|
+
specification_version: 4
|
|
117
|
+
summary: Linear service context for the ask-rb ecosystem
|
|
118
|
+
test_files: []
|