@subsimplicity/pi-extension-linear 0.0.1

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.
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # linear
2
+
3
+ `@subsimplicity/pi-extension-linear` is a [pi](https://pi.dev) extension that gives agents focused Linear issue tools while they work in a coding session.
4
+
5
+ The extension is intentionally a local Linear toolbelt first: it uses Linear's GraphQL API with a personal API key or OAuth access token, avoids polling, and only injects issue context automatically when a prompt mentions Linear issue identifiers such as `ENG-123`.
6
+
7
+ ## Supported surface
8
+
9
+ This package is a focused Pi extension for Linear issue workflows. It is not a standalone MCP server or a full Linear API wrapper.
10
+
11
+ ### Issue read/search
12
+
13
+ - Fetch an issue by UUID or identifier, e.g. `ENG-123`.
14
+ - Include issue description, URL, priority, estimate, due date, timestamps, workflow state, team, project, labels, creator, assignee, delegate, and recent comments.
15
+ - Search recently updated issues with filters for:
16
+ - text query against identifier/title/description/searchable content
17
+ - team UUID, key, or exact name
18
+ - workflow state name or state type
19
+ - label names
20
+ - priority
21
+ - assignee UUID or email
22
+ - project name
23
+ - archived issue inclusion
24
+ - List issues assigned to and recently created by the authenticated user.
25
+
26
+ ### Team/workflow discovery
27
+
28
+ - List Linear teams with UUIDs, keys, and names.
29
+ - List workflow states for a team by team UUID, key, or exact name.
30
+
31
+ ### Issue writes
32
+
33
+ - Create issues with team, title, and optional markdown description, assignee, due date, label IDs, priority, project, and state.
34
+ - Update issues by UUID or identifier. Supported fields are title, description, assignee, due date, replacement label IDs, priority, project, and state.
35
+ - Add markdown comments to issues.
36
+
37
+ ### Pi integration
38
+
39
+ - Show a `linear:` status indicator in pi.
40
+ - Inject concise Linear context automatically when a prompt mentions issue keys such as `ENG-123`.
41
+ - Add `#...` autocomplete for recently updated Linear issues.
42
+
43
+ ## Requirements
44
+
45
+ Set one of these environment variables before starting pi:
46
+
47
+ ```bash
48
+ export LINEAR_API_KEY=lin_api_...
49
+ # or, for OAuth bearer-token auth:
50
+ export LINEAR_ACCESS_TOKEN=...
51
+ ```
52
+
53
+ Personal API keys are easiest for local use. OAuth/app-actor and webhook-based Linear Agents are intentionally out of scope for this first package.
54
+
55
+ ## Install / use locally
56
+
57
+ From this package directory:
58
+
59
+ ```bash
60
+ bun install
61
+ pi -e ./src/index.ts
62
+ ```
63
+
64
+ To install from a local checkout so pi loads it automatically:
65
+
66
+ ```bash
67
+ pi install ./path/to/linear
68
+ ```
69
+
70
+ Use a project-local install when you want pi to record it in that project's `.pi/settings.json`:
71
+
72
+ ```bash
73
+ pi install -l ./path/to/linear
74
+ ```
75
+
76
+ ## Tools
77
+
78
+ | Tool | Purpose |
79
+ | ---------------------- | ----------------------------------------------------------------------- |
80
+ | `linear_setup` | Check Linear credentials and return the authenticated user. |
81
+ | `linear_get_issue` | Fetch an issue by UUID or identifier, e.g. `ENG-123`. |
82
+ | `linear_search_issues` | Search issues with focused filters and recently-updated ordering. |
83
+ | `linear_my_work` | List issues assigned to and recently created by the authenticated user. |
84
+ | `linear_list_teams` | List Linear teams with keys and UUIDs. |
85
+ | `linear_list_states` | List workflow states for a team. |
86
+ | `linear_create_issue` | Create a Linear issue. |
87
+ | `linear_update_issue` | Update a Linear issue. |
88
+ | `linear_comment_issue` | Add a markdown comment to a Linear issue. |
89
+
90
+ ## Commands
91
+
92
+ - `/linear-setup` — check credentials and show the authenticated Linear user.
93
+ - `/linear-issue ENG-123` — fetch an issue and add it to the transcript.
94
+ - `/linear-search text` — search recently updated issues by text.
95
+ - `/linear-my-work` — show issues assigned to and recently created by you.
96
+ - `/linear-teams` — list team keys and UUIDs.
97
+ - `/linear-states TEAM_KEY` — list workflow states for a team.
98
+
99
+ ## Autocomplete
100
+
101
+ Type `#` followed by part of an issue key/title to autocomplete recently updated issues. Completing `#ENG` inserts `ENG-123`.
102
+
103
+ ## Development
104
+
105
+ ```bash
106
+ bun install
107
+ bun run typecheck
108
+ bun run lint
109
+ ```
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@subsimplicity/pi-extension-linear",
3
+ "version": "0.0.1",
4
+ "description": "Pi extension for working with Linear issues from coding sessions",
5
+ "module": "src/index.ts",
6
+ "exports": "./src/index.ts",
7
+ "type": "module",
8
+ "keywords": [
9
+ "linear",
10
+ "pi-package",
11
+ "project-management",
12
+ "issues"
13
+ ],
14
+ "files": [
15
+ "src",
16
+ "README.md",
17
+ "package.json"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public",
21
+ "registry": "https://registry.npmjs.org/"
22
+ },
23
+ "repository": {
24
+ "directory": "linear",
25
+ "type": "git",
26
+ "url": "https://github.com/subsimplicity/pi-extensions-public/blob/main/linear/README.md"
27
+ },
28
+ "scripts": {
29
+ "format": "prettier --write .",
30
+ "lint": "eslint --fix .",
31
+ "typecheck": "tsc"
32
+ },
33
+ "pi": {
34
+ "extensions": [
35
+ "./src/index.ts"
36
+ ]
37
+ },
38
+ "devDependencies": {
39
+ "@subsimplicity/eslint-config-subsimplicity": "latest",
40
+ "@types/bun": "^1.3.14",
41
+ "eslint": "^10.4.0",
42
+ "prettier": "^3.8.3",
43
+ "typescript": "^6.0.3"
44
+ },
45
+ "peerDependencies": {
46
+ "@earendil-works/pi-coding-agent": "*",
47
+ "@earendil-works/pi-tui": "*"
48
+ }
49
+ }
@@ -0,0 +1,140 @@
1
+ import type {
2
+ AutocompleteItem,
3
+ AutocompleteProvider,
4
+ AutocompleteSuggestions,
5
+ } from "@earendil-works/pi-tui";
6
+ import { LinearClient, hasLinearCredentials } from "./client.ts";
7
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
+ import type { LinearIssueSummary } from "./types.ts";
9
+ import { fuzzyFilter } from "@earendil-works/pi-tui";
10
+
11
+ const MAX_PRELOADED_ISSUES = 100;
12
+ const MAX_SUGGESTIONS = 20;
13
+
14
+ function isAbortError(error: unknown): boolean {
15
+ return error instanceof Error && error.name === "AbortError";
16
+ }
17
+
18
+ function extractLinearToken(textBeforeCursor: string): string | undefined {
19
+ const match = /(?:^|[ \t])#([A-Za-z0-9-]*)$/.exec(textBeforeCursor);
20
+ return match?.[1];
21
+ }
22
+
23
+ function issueToItem(issue: LinearIssueSummary): AutocompleteItem {
24
+ return {
25
+ description: `${issue.state?.name ?? "no state"} · ${issue.title}`,
26
+ label: issue.identifier,
27
+ value: issue.identifier,
28
+ };
29
+ }
30
+
31
+ function filterIssues(
32
+ issues: LinearIssueSummary[],
33
+ query: string,
34
+ ): AutocompleteItem[] {
35
+ if (!query.trim()) {
36
+ return issues.slice(0, MAX_SUGGESTIONS).map(issueToItem);
37
+ }
38
+
39
+ const upper = query.toUpperCase();
40
+ const identifierMatches = issues
41
+ .filter((issue) => issue.identifier.startsWith(upper))
42
+ .slice(0, MAX_SUGGESTIONS)
43
+ .map(issueToItem);
44
+ if (identifierMatches.length > 0) return identifierMatches;
45
+
46
+ return fuzzyFilter(
47
+ issues,
48
+ query,
49
+ (issue) => `${issue.identifier} ${issue.title}`,
50
+ )
51
+ .slice(0, MAX_SUGGESTIONS)
52
+ .map(issueToItem);
53
+ }
54
+
55
+ function createLinearAutocompleteProvider(
56
+ current: AutocompleteProvider,
57
+ getIssues: (signal: AbortSignal) => Promise<LinearIssueSummary[] | undefined>,
58
+ ): AutocompleteProvider {
59
+ return {
60
+ applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
61
+ return current.applyCompletion(
62
+ lines,
63
+ cursorLine,
64
+ cursorCol,
65
+ item,
66
+ prefix,
67
+ );
68
+ },
69
+
70
+ async getSuggestions(
71
+ lines,
72
+ cursorLine,
73
+ cursorCol,
74
+ options,
75
+ ): Promise<AutocompleteSuggestions | null> {
76
+ const currentLine = lines[cursorLine] ?? "";
77
+ const token = extractLinearToken(currentLine.slice(0, cursorCol));
78
+ if (token === undefined) {
79
+ return current.getSuggestions(lines, cursorLine, cursorCol, options);
80
+ }
81
+
82
+ const issues = await getIssues(options.signal);
83
+ if (options.signal.aborted || !issues?.length) {
84
+ return current.getSuggestions(lines, cursorLine, cursorCol, options);
85
+ }
86
+
87
+ const items = filterIssues(issues, token);
88
+ if (items.length === 0) {
89
+ return current.getSuggestions(lines, cursorLine, cursorCol, options);
90
+ }
91
+
92
+ return { items, prefix: `#${token}` };
93
+ },
94
+
95
+ shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
96
+ return (
97
+ current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ??
98
+ true
99
+ );
100
+ },
101
+ };
102
+ }
103
+
104
+ export function registerLinearAutocomplete(pi: ExtensionAPI): void {
105
+ const client = new LinearClient();
106
+
107
+ pi.on("session_start", (_event, ctx) => {
108
+ if (!hasLinearCredentials()) return;
109
+
110
+ let issuesPromise: Promise<LinearIssueSummary[] | undefined> | undefined;
111
+ let loadErrorShown = false;
112
+
113
+ const getIssues = async (
114
+ signal: AbortSignal,
115
+ ): Promise<LinearIssueSummary[] | undefined> => {
116
+ issuesPromise ??= client
117
+ .searchIssues({ limit: MAX_PRELOADED_ISSUES }, signal)
118
+ .catch((error: unknown) => {
119
+ issuesPromise = undefined;
120
+ if (signal.aborted || isAbortError(error)) return undefined;
121
+
122
+ if (!loadErrorShown) {
123
+ loadErrorShown = true;
124
+ const message =
125
+ error instanceof Error ? error.message : String(error);
126
+ ctx.ui.notify(
127
+ `linear: failed to preload issue autocomplete: ${message}`,
128
+ "warning",
129
+ );
130
+ }
131
+ return undefined;
132
+ });
133
+ return issuesPromise;
134
+ };
135
+
136
+ ctx.ui.addAutocompleteProvider((current) =>
137
+ createLinearAutocompleteProvider(current, getIssues),
138
+ );
139
+ });
140
+ }