@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 +109 -0
- package/package.json +49 -0
- package/src/autocomplete.ts +140 -0
- package/src/client.ts +571 -0
- package/src/commands.ts +140 -0
- package/src/events.ts +45 -0
- package/src/format.ts +155 -0
- package/src/index.ts +16 -0
- package/src/results.ts +10 -0
- package/src/schemas.ts +256 -0
- package/src/status.ts +51 -0
- package/src/tools.ts +202 -0
- package/src/types.ts +114 -0
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
|
+
}
|