linear

package
v0.0.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 25, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Index

Constants

View Source
const (
	MaxHistoryEntries        = 20
	MaxAnswerLengthInContext = 500
)

Variables

This section is empty.

Functions

func CreateLinearCommands

func CreateLinearCommands() *cobra.Command

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

func IsAuthError(err error) bool

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.

func (*APIError) Error

func (e *APIError) Error() string

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

func NewClient(apiKey, workspaceID, defaultTeam string, debug bool) (*Client, error)

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 (c *Client) AddComment(ctx context.Context, issueID, body string) (*Comment, error)

func (*Client) CreateCycle

func (c *Client) CreateCycle(ctx context.Context, input CreateCycleInput) (*Cycle, error)

func (*Client) CreateIssue

func (c *Client) CreateIssue(ctx context.Context, input CreateIssueInput) (*Issue, error)

func (*Client) CreateLabel

func (c *Client) CreateLabel(ctx context.Context, teamID, name, color string) (*Label, error)

CreateLabel creates a team-scoped label. color is a hex string like "#5e6ad2".

func (*Client) CreateProject

func (c *Client) CreateProject(ctx context.Context, input CreateProjectInput) (*Project, error)

func (*Client) Debug

func (c *Client) Debug() bool

func (*Client) DefaultTeam

func (c *Client) DefaultTeam() string

func (*Client) Do

func (c *Client) Do(ctx context.Context, query string, variables map[string]any, out any) error

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

func (c *Client) FindLabelByName(ctx context.Context, teamID, name string) (*Label, error)

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

func (c *Client) FindUserByDisplayName(ctx context.Context, displayName string) (*User, error)

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) GetCycle

func (c *Client) GetCycle(ctx context.Context, id string) (*Cycle, error)

func (*Client) GetDocument

func (c *Client) GetDocument(ctx context.Context, id string) (*Document, error)

func (*Client) GetIssue

func (c *Client) GetIssue(ctx context.Context, id string) (*Issue, error)

GetIssue fetches a single issue by ID (UUID).

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 (c *Client) GetProject(ctx context.Context, id string) (*Project, error)

func (*Client) GetTeam

func (c *Client) GetTeam(ctx context.Context, idOrKey string) (*Team, []WorkflowState, error)

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

func (c *Client) GetWorkspace(ctx context.Context) (*Workspace, *User, error)

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 (c *Client) ListCycles(ctx context.Context, filter CycleFilter) ([]Cycle, error)

func (*Client) ListDocuments

func (c *Client) ListDocuments(ctx context.Context) ([]Document, error)

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

func (c *Client) ListLabels(ctx context.Context, teamID string) ([]Label, error)

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 (c *Client) ListProjects(ctx context.Context, filter ProjectFilter, first int, after string) ([]Project, PageInfo, error)

func (*Client) ListTeams

func (c *Client) ListTeams(ctx context.Context) ([]Team, error)

func (*Client) ListUsers

func (c *Client) ListUsers(ctx context.Context) ([]User, error)

func (*Client) ResolveIssueID

func (c *Client) ResolveIssueID(ctx context.Context, idOrIdentifier string) (string, error)

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 (c *Client) SetHTTPClient(hc *http.Client)

func (*Client) UpdateCycle

func (c *Client) UpdateCycle(ctx context.Context, id string, input UpdateCycleInput) (*Cycle, error)

func (*Client) UpdateIssue

func (c *Client) UpdateIssue(ctx context.Context, id string, input UpdateIssueInput) (*Issue, error)

func (*Client) UpdateProject

func (c *Client) UpdateProject(ctx context.Context, id string, input UpdateProjectInput) (*Project, error)

func (*Client) WorkspaceID

func (c *Client) WorkspaceID() string

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 CreateCycleInput struct {
	TeamID   string `json:"teamId"`
	Name     string `json:"name,omitempty"`
	StartsAt string `json:"startsAt"` // RFC 3339
	EndsAt   string `json:"endsAt"`
}

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 CycleFilter struct {
	TeamID   string
	IsActive bool // shortcut: completedAt is null and startsAt <= now <= endsAt
	IsFuture bool // startsAt > now
}

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 Label

type Label struct {
	ID     string `json:"id"`
	Name   string `json:"name"`
	Color  string `json:"color"`
	TeamID string `json:"-"`
}

type PageInfo

type PageInfo struct {
	HasNextPage bool   `json:"hasNextPage"`
	EndCursor   string `json:"endCursor"`
}

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 Team

type Team struct {
	ID          string    `json:"id"`
	Key         string    `json:"key"` // short prefix used in identifiers e.g. "ENG"
	Name        string    `json:"name"`
	Description string    `json:"description"`
	CreatedAt   time.Time `json:"createdAt"`
}

type UpdateCycleInput

type UpdateCycleInput struct {
	Name     *string `json:"name,omitempty"`
	StartsAt *string `json:"startsAt,omitempty"`
	EndsAt   *string `json:"endsAt,omitempty"`
}

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 User

type User struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	DisplayName string `json:"displayName"`
	Email       string `json:"email"`
	Active      bool   `json:"active"`
	AvatarURL   string `json:"avatarUrl"`
}

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.

type Workspace

type Workspace struct {
	ID        string    `json:"id"`
	Name      string    `json:"name"`
	URLKey    string    `json:"urlKey"`
	CreatedAt time.Time `json:"createdAt"`
	UserCount int       `json:"userCount"`
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL