Documentation
¶
Index ¶
- Constants
- func CreateLinearCommands() *cobra.Command
- func IsAuthError(err error) bool
- func ResolveAPIKey() string
- func ResolveDefaultTeam() string
- func ResolveWorkspaceID() string
- type APIError
- type AccountStatus
- type Client
- func (c *Client) AddComment(ctx context.Context, issueID, body string) (*Comment, error)
- func (c *Client) CreateCycle(ctx context.Context, input CreateCycleInput) (*Cycle, error)
- func (c *Client) CreateIssue(ctx context.Context, input CreateIssueInput) (*Issue, error)
- func (c *Client) CreateLabel(ctx context.Context, teamID, name, color string) (*Label, error)
- func (c *Client) CreateProject(ctx context.Context, input CreateProjectInput) (*Project, error)
- func (c *Client) Debug() bool
- func (c *Client) DefaultTeam() string
- func (c *Client) Do(ctx context.Context, query string, variables map[string]any, out any) error
- func (c *Client) FindLabelByName(ctx context.Context, teamID, name string) (*Label, error)
- func (c *Client) FindUserByDisplayName(ctx context.Context, displayName string) (*User, error)
- func (c *Client) GetCycle(ctx context.Context, id string) (*Cycle, error)
- func (c *Client) GetDocument(ctx context.Context, id string) (*Document, error)
- func (c *Client) GetIssue(ctx context.Context, id string) (*Issue, error)
- func (c *Client) GetIssueComments(ctx context.Context, issueID string, limit int) ([]Comment, error)
- func (c *Client) GetProject(ctx context.Context, id string) (*Project, error)
- func (c *Client) GetTeam(ctx context.Context, idOrKey string) (*Team, []WorkflowState, error)
- func (c *Client) GetWorkspace(ctx context.Context) (*Workspace, *User, error)
- func (c *Client) ListCycles(ctx context.Context, filter CycleFilter) ([]Cycle, error)
- func (c *Client) ListDocuments(ctx context.Context) ([]Document, error)
- func (c *Client) ListIssues(ctx context.Context, filter IssueFilter, first int, after string) ([]Issue, PageInfo, error)
- func (c *Client) ListLabels(ctx context.Context, teamID string) ([]Label, error)
- func (c *Client) ListProjects(ctx context.Context, filter ProjectFilter, first int, after string) ([]Project, PageInfo, error)
- func (c *Client) ListTeams(ctx context.Context) ([]Team, error)
- func (c *Client) ListUsers(ctx context.Context) ([]User, error)
- func (c *Client) ResolveIssueID(ctx context.Context, idOrIdentifier string) (string, error)
- func (c *Client) SetHTTPClient(hc *http.Client)
- func (c *Client) UpdateCycle(ctx context.Context, id string, input UpdateCycleInput) (*Cycle, error)
- func (c *Client) UpdateIssue(ctx context.Context, id string, input UpdateIssueInput) (*Issue, error)
- func (c *Client) UpdateProject(ctx context.Context, id string, input UpdateProjectInput) (*Project, error)
- func (c *Client) WorkspaceID() string
- type Comment
- type ConversationEntry
- type ConversationHistory
- func (h *ConversationHistory) AddEntry(question, answer, workspaceID string)
- func (h *ConversationHistory) GetAccountStatusContext() string
- func (h *ConversationHistory) GetRecentContext(maxEntries int) string
- func (h *ConversationHistory) Load() error
- func (h *ConversationHistory) Save() error
- func (h *ConversationHistory) UpdateAccountStatus(status *AccountStatus)
- type CreateCycleInput
- type CreateIssueInput
- type CreateProjectInput
- type Cycle
- type CycleFilter
- type Document
- type GraphQLError
- type Issue
- type IssueFilter
- type Label
- type PageInfo
- type Project
- type ProjectFilter
- type Team
- type UpdateCycleInput
- type UpdateIssueInput
- type UpdateProjectInput
- type User
- type WorkflowState
- type Workspace
Constants ¶
const ( MaxHistoryEntries = 20 MaxAnswerLengthInContext = 500 )
Variables ¶
This section is empty.
Functions ¶
func CreateLinearCommands ¶
CreateLinearCommands builds the `clanker linear` command tree. The ask subcommand is added separately by cmd/linear.go so internal/linear doesn't import internal/ai.
func IsAuthError ¶
IsAuthError reports whether err is a 401/403 from Linear (or a 400 with a `AUTHENTICATION_ERROR` extension — Linear's most common shape).
func ResolveAPIKey ¶
func ResolveAPIKey() string
func ResolveDefaultTeam ¶
func ResolveDefaultTeam() string
func ResolveWorkspaceID ¶
func ResolveWorkspaceID() string
Types ¶
type APIError ¶
type APIError struct {
Status int
Body string
Errors []GraphQLError
}
APIError carries the HTTP status plus the first GraphQL error message.
type AccountStatus ¶
type AccountStatus struct {
Timestamp time.Time `json:"timestamp"`
WorkspaceID string `json:"workspace_id"`
WorkspaceName string `json:"workspace_name,omitempty"`
TeamCount int `json:"team_count"`
StartedIssueCount int `json:"started_issue_count"`
ActiveProjectCount int `json:"active_project_count"`
}
AccountStatus is the at-a-glance snapshot the ask command stashes in conversation history so follow-ups can be answered without re-fetching.
func GatherAccountStatus ¶
func GatherAccountStatus(ctx context.Context, c *Client, workspaceID string) (*AccountStatus, error)
GatherAccountStatus collects an at-a-glance snapshot for the conversation history. The four queries run concurrently — ask cold-start latency is dominated by these round-trips and they're independent. Errors are non-fatal: a partial snapshot beats blocking the ask command on a single flaky endpoint.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is a thin GraphQL wrapper around the Linear API.
Auth is a Personal API Key from Settings → API → Personal API keys. IMPORTANT: Linear's auth header is `Authorization: <key>` — there is NO `Bearer ` prefix. This is the #1 Linear footgun; sending Bearer returns a 400 with a confusing error.
func NewClient ¶
NewClient returns a Client. apiKey is required; workspaceID and team can be empty (callers either pass them via flags or set defaults later).
func (*Client) AddComment ¶
func (*Client) CreateCycle ¶
func (*Client) CreateIssue ¶
func (*Client) CreateLabel ¶
CreateLabel creates a team-scoped label. color is a hex string like "#5e6ad2".
func (*Client) CreateProject ¶
func (*Client) DefaultTeam ¶
func (*Client) Do ¶
Do issues a GraphQL POST. variables may be nil. Decodes the `data` field into `out` (a pointer to a struct shaped like the query's selection set). 429s are retried with backoff that honors Retry-After when present.
func (*Client) FindLabelByName ¶
FindLabelByName returns the first label matching name (case-sensitive) within an optional team scope. Used by the annotation layer to look up `infra:<type>:<id>` labels without paginating the full set.
func (*Client) FindUserByDisplayName ¶
FindUserByDisplayName scans the user list for an exact match. Used by the assign command which takes a username. For large workspaces this is O(n) but n is bounded by the workspace's user count (typically <500).
func (*Client) GetDocument ¶
func (*Client) GetIssueComments ¶
func (c *Client) GetIssueComments(ctx context.Context, issueID string, limit int) ([]Comment, error)
GetIssueComments returns top-level comments for an issue in creation order (oldest first — matches Linear's GraphQL `orderBy: createdAt` ascending default and lets the prompt builder render the thread linearly).
func (*Client) GetProject ¶
func (*Client) GetTeam ¶
GetTeam returns a team with its workflow states inlined so the kanban renderer doesn't need a second round-trip per team.
func (*Client) GetWorkspace ¶
GetWorkspace returns the workspace the API key belongs to plus the viewer (the user the key was issued for). Linear calls the workspace `viewer.organization` — there's no direct "current workspace" query.
func (*Client) ListCycles ¶
func (*Client) ListDocuments ¶
func (*Client) ListIssues ¶
func (c *Client) ListIssues(ctx context.Context, filter IssueFilter, first int, after string) ([]Issue, PageInfo, error)
ListIssues returns a page of issues. cursor may be empty for the first page; pass the returned PageInfo.EndCursor on subsequent calls.
func (*Client) ListLabels ¶
ListLabels returns labels, optionally filtered to a single team. Pass teamID="" for org-wide labels (rare — most labels are team-scoped).
func (*Client) ListProjects ¶
func (*Client) ResolveIssueID ¶
ResolveIssueID accepts either a UUID or a human identifier (ENG-42) and returns the UUID. Linear's `issue(id:)` query accepts either form so this is cheap; we make it explicit because mutation endpoints (`issueUpdate`, `commentCreate`) only accept the UUID and silently 4xx on identifiers.
func (*Client) SetHTTPClient ¶
func (*Client) UpdateCycle ¶
func (*Client) UpdateIssue ¶
func (*Client) UpdateProject ¶
func (*Client) WorkspaceID ¶
type Comment ¶
type Comment struct {
ID string `json:"id"`
Body string `json:"body"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
URL string `json:"url"`
User *User `json:"user,omitempty"`
IssueID string `json:"-"`
}
Comment is a top-level comment on an issue. Threads (replies) are represented via Parent — for MVP we only render top-level comments and flatten replies into the same view.
type ConversationEntry ¶
type ConversationEntry struct {
Timestamp time.Time `json:"timestamp"`
Question string `json:"question"`
Answer string `json:"answer"`
WorkspaceID string `json:"workspace_id"`
}
ConversationEntry is a single Q&A turn against the Linear ask agent.
type ConversationHistory ¶
type ConversationHistory struct {
Entries []ConversationEntry `json:"entries"`
WorkspaceID string `json:"workspace_id"`
LastStatus *AccountStatus `json:"last_status,omitempty"`
// contains filtered or unexported fields
}
ConversationHistory persists Linear ask sessions per-workspace under ~/.clanker/linear-{workspaceID}.json — same pattern as Sentry's history.
func NewConversationHistory ¶
func NewConversationHistory(workspaceID string) *ConversationHistory
func (*ConversationHistory) AddEntry ¶
func (h *ConversationHistory) AddEntry(question, answer, workspaceID string)
func (*ConversationHistory) GetAccountStatusContext ¶
func (h *ConversationHistory) GetAccountStatusContext() string
func (*ConversationHistory) GetRecentContext ¶
func (h *ConversationHistory) GetRecentContext(maxEntries int) string
func (*ConversationHistory) Load ¶
func (h *ConversationHistory) Load() error
func (*ConversationHistory) Save ¶
func (h *ConversationHistory) Save() error
func (*ConversationHistory) UpdateAccountStatus ¶
func (h *ConversationHistory) UpdateAccountStatus(status *AccountStatus)
type CreateCycleInput ¶
type CreateIssueInput ¶
type CreateIssueInput struct {
Title string `json:"title"`
Description string `json:"description,omitempty"`
TeamID string `json:"teamId"`
ProjectID string `json:"projectId,omitempty"`
CycleID string `json:"cycleId,omitempty"`
StateID string `json:"stateId,omitempty"`
AssigneeID string `json:"assigneeId,omitempty"`
Priority int `json:"priority,omitempty"`
Estimate float64 `json:"estimate,omitempty"`
LabelIDs []string `json:"labelIds,omitempty"`
DueDate string `json:"dueDate,omitempty"` // YYYY-MM-DD
}
CreateIssueInput is the strongly-typed subset of IssueCreateInput we expose. Linear's full input is larger; we add fields here as the agent surface needs them.
type CreateProjectInput ¶
type CreateProjectInput struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
TeamIDs []string `json:"teamIds"`
LeadID string `json:"leadId,omitempty"`
State string `json:"state,omitempty"`
StartDate string `json:"startDate,omitempty"`
TargetDate string `json:"targetDate,omitempty"`
}
type Cycle ¶
type Cycle struct {
ID string `json:"id"`
Number int `json:"number"`
Name string `json:"name"`
StartsAt time.Time `json:"startsAt"`
EndsAt time.Time `json:"endsAt"`
CompletedAt *time.Time `json:"completedAt"`
Progress float64 `json:"progress"`
TeamID string `json:"-"`
}
Cycle is a time-boxed iteration (a sprint) belonging to a single team.
type CycleFilter ¶
type Document ¶
type Document struct {
ID string `json:"id"`
Title string `json:"title"`
URL string `json:"url"`
Content json.RawMessage `json:"content"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ProjectID string `json:"-"`
}
Document is a free-form doc attached to a project or team. Body is Linear's rich-text JSON which we expose as-is for now — rendering it nicely in the desktop UI is PR4 territory (parallels Notion blocks).
type GraphQLError ¶
type GraphQLError struct {
Message string `json:"message"`
Path []any `json:"path"`
Extensions map[string]any `json:"extensions,omitempty"`
}
GraphQLError is one element of the GraphQL `errors` envelope.
type Issue ¶
type Issue struct {
ID string `json:"id"`
Identifier string `json:"identifier"` // human-facing, e.g. "ENG-42"
Title string `json:"title"`
Description string `json:"description"`
Priority int `json:"priority"` // 0 (none) | 1 (urgent) | 2 (high) | 3 (medium) | 4 (low)
Estimate float64 `json:"estimate"`
URL string `json:"url"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
StartedAt *time.Time `json:"startedAt"`
CompletedAt *time.Time `json:"completedAt"`
CanceledAt *time.Time `json:"canceledAt"`
DueDate *time.Time `json:"dueDate"`
// Nested via GraphQL — populated by the query, not separate calls.
State *WorkflowState `json:"state,omitempty"`
Team *Team `json:"team,omitempty"`
Project *Project `json:"project,omitempty"`
Cycle *Cycle `json:"cycle,omitempty"`
Assignee *User `json:"assignee,omitempty"`
Creator *User `json:"creator,omitempty"`
Labels struct {
Nodes []Label `json:"nodes"`
} `json:"labels"`
}
Issue is the central work unit. ShortID is what Linear calls the "identifier" — operator-facing (e.g. "ENG-123"). ID is the UUID required for any mutation.
type IssueFilter ¶
type IssueFilter struct {
StateType string // "started" | "completed" | "cancelled" | "unstarted" | "backlog" | "triage"
TeamID string // UUID
TeamKey string // e.g. "ENG" — convenience for the CLI
ProjectID string
CycleID string
AssigneeID string // UUID
AssigneeIsMe bool // filter to issues assigned to the API key's viewer
LabelName string // exact label name (used by annotation lookup)
LabelIDs []string // multi-label match
Priority int // 0..4; 0 means "any"
IncludeArchived bool
}
IssueFilter mirrors a subset of Linear's GraphQL IssueFilter input. Empty fields are omitted from the marshalled GraphQL variables. The shape is intentionally narrow — we expose the filters the ask flow and the kanban actually use; more can be added when needed.
type PageInfo ¶
PageInfo is the Relay-style cursor for Linear's connection types. We always pass `first` and read `endCursor`; backwards pagination is not used.
type Project ¶
type Project struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
State string `json:"state"` // backlog, planned, started, paused, completed, canceled
Progress float64 `json:"progress"`
StartDate *time.Time `json:"startDate"`
TargetDate *time.Time `json:"targetDate"`
CreatedAt time.Time `json:"createdAt"`
URL string `json:"url"`
LeadID string `json:"-"`
}
Project is a delivery effort that groups issues across one or more cycles. Note: collides with "project" in Notion's vocabulary — be explicit in UI.
type ProjectFilter ¶
type ProjectFilter struct {
State string // "backlog" | "planned" | "started" | "paused" | "completed" | "canceled"
TeamID string
}
ProjectFilter exposes the subset of Linear's ProjectFilter we use today.
type UpdateCycleInput ¶
type UpdateIssueInput ¶
type UpdateIssueInput struct {
Title *string `json:"title,omitempty"`
Description *string `json:"description,omitempty"`
StateID *string `json:"stateId,omitempty"`
AssigneeID *string `json:"assigneeId,omitempty"`
ProjectID *string `json:"projectId,omitempty"`
CycleID *string `json:"cycleId,omitempty"`
Priority *int `json:"priority,omitempty"`
Estimate *float64 `json:"estimate,omitempty"`
LabelIDs []string `json:"labelIds,omitempty"`
DueDate *string `json:"dueDate,omitempty"`
}
UpdateIssueInput is the patch for issueUpdate. Empty fields are omitted, so callers can pass a tiny struct to update one property.
type UpdateProjectInput ¶
type UpdateProjectInput struct {
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
State *string `json:"state,omitempty"`
LeadID *string `json:"leadId,omitempty"`
StartDate *string `json:"startDate,omitempty"`
TargetDate *string `json:"targetDate,omitempty"`
TeamIDs []string `json:"teamIds,omitempty"`
}
type WorkflowState ¶
type WorkflowState struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Color string `json:"color"`
TeamID string `json:"-"`
}
WorkflowState is one column in a team's kanban (e.g. "In Progress"). Type is one of "triage", "backlog", "unstarted", "started", "completed", "cancelled". Mutations target state by ID; the canonical workflow position is per-team so two teams' "In Progress" states are distinct objects.