@fink-andreas/pi-linear-tools 0.5.1 → 0.7.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.
- package/CHANGELOG.md +38 -0
- package/README.md +44 -16
- package/extensions/pi-linear-tools.js +210 -27
- package/package.json +2 -2
- package/settings.json.example +6 -2
- package/src/cli.js +101 -8
- package/src/handlers.js +405 -18
- package/src/linear-client.js +120 -14
- package/src/linear.js +319 -26
- package/src/settings.js +22 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.7.0 (2026-05-05)
|
|
4
|
+
|
|
5
|
+
Issue image fetching and file download release.
|
|
6
|
+
|
|
7
|
+
### New Features
|
|
8
|
+
- **`linear_issue(action="images")`**: Fetch markdown/HTML embedded images from issue descriptions and comments. Returns images as tool `image` content (base64-encoded). Includes special auth handling for Linear upload URLs (`uploads.linear.app`).
|
|
9
|
+
- **`linear_issue(action="download")`**: Download Linear issue attachments to local filesystem. Supports attachment selection by ID, title, URL, or index. Includes relative path validation, filename sanitization, and `allow_overwrite_files` config guard (default: `false`).
|
|
10
|
+
|
|
11
|
+
### Performance / API Usage
|
|
12
|
+
- **N+1 issue resolution optimization**: Merged N+1-per-issue lazy loads in milestone details into a single GraphQL batch query (introduced in v0.6.0).
|
|
13
|
+
|
|
14
|
+
### Documentation
|
|
15
|
+
- **Updated smoke tests**: Added coverage for `images` and `download` actions.
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
- **Fixed workspace-wide issue listing (#4)**: `linear_issue list` now falls back to workspace-level listing (using `fetchIssues`) when no project is specified and cwd doesn't match a Linear project. This allows `linear_issue list assignee=me` to work without requiring a project context.
|
|
19
|
+
- Fixed project archive mutation to actually archive instead of delete (INN-301).
|
|
20
|
+
- Fixed `linear_issue comment` to render submitted comment text in result (INN-305).
|
|
21
|
+
- Fixed rate limit display to not show hardcoded total=5000 (INN-304).
|
|
22
|
+
- Fixed `linear_project` and `linear_project_update` to have rate-limit pre-check (INN-303).
|
|
23
|
+
|
|
24
|
+
## v0.6.0 (2026-04-23)
|
|
25
|
+
|
|
26
|
+
Rate-limit resilience for milestone operations.
|
|
27
|
+
|
|
28
|
+
### Bug Fixes
|
|
29
|
+
- **Fixed linear_milestone view rate-limit crash**: `linear_milestone view` no longer crashes when hitting Linear API rate limits. Errors are now properly surfaced as safe tool results instead of propagating as crashes.
|
|
30
|
+
- **Fixed milestone issues rate-limit swallowing**: `fetchMilestoneDetails()` now correctly propagates rate-limit errors from lazy-loaded issue data instead of silently returning partial results.
|
|
31
|
+
|
|
32
|
+
### Performance / API Usage
|
|
33
|
+
- **Replaced N+1 lazy loading with single GraphQL query**: Milestone details now fetch all issue state/assignee in ONE request instead of N+1 per-issue lazy loads. This dramatically reduces API calls and rate-limit exposure for large milestones.
|
|
34
|
+
- **Added cached rate-limit pre-check**: `linear_milestone` now has the same cached pre-check that `linear_issue` uses to short-circuit before making API calls when the cache already knows about a rate limit.
|
|
35
|
+
- **Exported `isRateLimitError()` helper**: Made available for consistent error handling across tools.
|
|
36
|
+
|
|
37
|
+
### Documentation
|
|
38
|
+
- **Reorganized README**: Moved Key Concepts section for better onboarding
|
|
39
|
+
- **Added combined Linear+Pi logo**: Light/dark theme support for branding
|
|
40
|
+
|
|
3
41
|
## v0.5.1 (2026-04-17)
|
|
4
42
|
|
|
5
43
|
### Bug Fixes
|
package/README.md
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
# pi-linear-tools
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p align="center">
|
|
4
|
+
<picture>
|
|
5
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/images/linear-pi-combined-white.svg">
|
|
6
|
+
<source media="(prefers-color-scheme: light)" srcset="docs/images/linear-pi-combined-black.svg">
|
|
7
|
+
<img src="docs/images/linear-pi-combined-black.svg" alt="Linear + Pi" width="340">
|
|
8
|
+
</picture>
|
|
9
|
+
</p>
|
|
4
10
|
|
|
5
|
-
|
|
6
|
-
- `issue update` changes issue fields; `issue comment` adds discussion; `issue activity` reads the Activity timeline
|
|
7
|
-
- `project update` changes project fields; `project-update` manages Updates tab entries
|
|
8
|
-
- `project-update` maps to Linear project updates in the Updates tab
|
|
9
|
-
- `sync-doc init` scaffolds `.linear-tools/config.json` in the folder you point at
|
|
10
|
-
- `sync-doc run` and `sync-doc check` default to all configured targets in `.linear-tools/config.json`
|
|
11
|
-
- `sync-doc --target X` narrows the operation to one configured target
|
|
12
|
-
|
|
13
|
-
Reference conventions:
|
|
14
|
-
- issues use issue key or issue ID
|
|
15
|
-
- projects use project name or project ID
|
|
16
|
-
- project updates use project update ID
|
|
17
|
-
- milestones use milestone ID
|
|
11
|
+
`pi-linear-tools` is a token eficcient Pi extension for the [Pi coding agent](https://github.com/badlogic/pi-mono) that lets you manage [Linear](https://linear.app/about) issues, projects, and milestones via LLM tool calls and CLI commands.
|
|
18
12
|
|
|
19
13
|
## Install
|
|
20
14
|
|
|
@@ -47,6 +41,7 @@ Optional non-interactive commands:
|
|
|
47
41
|
/linear-tools-config --api-key lin_xxx
|
|
48
42
|
/linear-tools-config --default-team ENG
|
|
49
43
|
/linear-tools-config --team ENG --project "My Project"
|
|
44
|
+
/linear-tools-config --allow-overwrite-files true|false
|
|
50
45
|
```
|
|
51
46
|
|
|
52
47
|
## Extension commands
|
|
@@ -54,10 +49,30 @@ Optional non-interactive commands:
|
|
|
54
49
|
- `/linear-tools-config`
|
|
55
50
|
- `/linear-tools-help`
|
|
56
51
|
|
|
52
|
+
## Key Concepts
|
|
53
|
+
|
|
54
|
+
Useful mental model:
|
|
55
|
+
- `issue update` changes issue fields; `issue comment` adds discussion; `issue activity` reads the Activity timeline
|
|
56
|
+
- `project update` changes project fields; `project-update` manages Updates tab entries
|
|
57
|
+
- `project-update` maps to Linear project updates in the Updates tab
|
|
58
|
+
- `sync-doc init` scaffolds `.linear-tools/config.json` in the folder you point at
|
|
59
|
+
- `sync-doc run` and `sync-doc check` default to all configured targets in `.linear-tools/config.json`
|
|
60
|
+
- `sync-doc --target X` narrows the operation to one configured target
|
|
61
|
+
|
|
62
|
+
Reference conventions:
|
|
63
|
+
- issues use issue key or issue ID
|
|
64
|
+
- projects use project name or project ID
|
|
65
|
+
- project updates use project update ID
|
|
66
|
+
- milestones use milestone ID
|
|
67
|
+
|
|
57
68
|
## LLM-callable tools
|
|
58
69
|
|
|
59
70
|
### `linear_issue`
|
|
60
|
-
Actions: `list`, `view`, `activity`, `create`, `update`, `comment`, `start`, `delete`
|
|
71
|
+
Actions: `list`, `view`, `images`, `download`, `activity`, `create`, `update`, `comment`, `start`, `delete`
|
|
72
|
+
|
|
73
|
+
`images` fetches image URLs embedded in issue markdown/comments and returns image content inline.
|
|
74
|
+
|
|
75
|
+
`download` fetches Linear issue attachments only. Destination directories must be relative paths. Existing files are not overwritten unless both `overwrite: true` is provided and the config guard is enabled with `/linear-tools-config --allow-overwrite-files true`.
|
|
61
76
|
|
|
62
77
|
### `linear_project`
|
|
63
78
|
Actions: `list`, `view`, `create`, `update`, `delete`, `archive`, `unarchive`
|
|
@@ -84,11 +99,12 @@ pi-linear-tools config
|
|
|
84
99
|
pi-linear-tools config --api-key lin_xxx
|
|
85
100
|
pi-linear-tools config --default-team ENG
|
|
86
101
|
pi-linear-tools config --team ENG --project "My Project"
|
|
102
|
+
pi-linear-tools config --allow-overwrite-files true|false
|
|
87
103
|
```
|
|
88
104
|
|
|
89
105
|
### Issue commands
|
|
90
106
|
|
|
91
|
-
Use `update` to change the issue itself. Use `comment` to add discussion. Use `activity` to read the Activity timeline shown in Linear.
|
|
107
|
+
Use `update` to change the issue itself. Use `comment` to add discussion. Use `activity` to read the Activity timeline shown in Linear. Use `images` to fetch image attachments embedded in issue markdown/comments.
|
|
92
108
|
|
|
93
109
|
```bash
|
|
94
110
|
# List issues
|
|
@@ -100,10 +116,18 @@ pi-linear-tools issue list --project "My Project" --assignee me
|
|
|
100
116
|
pi-linear-tools issue view ENG-123
|
|
101
117
|
pi-linear-tools issue view ENG-123 --no-comments
|
|
102
118
|
|
|
119
|
+
# Fetch markdown-embedded issue/comment images
|
|
120
|
+
pi-linear-tools issue images ENG-123
|
|
121
|
+
pi-linear-tools issue images ENG-123 --limit 5
|
|
122
|
+
|
|
103
123
|
# Read Activity timeline
|
|
104
124
|
pi-linear-tools issue activity ENG-123 --limit 20
|
|
105
125
|
pi-linear-tools issue activity https://linear.app/workspace/issue/ENG-123/example --limit 20
|
|
106
126
|
|
|
127
|
+
# Download a Linear issue attachment
|
|
128
|
+
pi-linear-tools issue download ENG-123 --attachment-index 1 --directory downloads
|
|
129
|
+
pi-linear-tools issue download ENG-123 --attachment-id att_xxx --directory downloads --filename spec.pdf
|
|
130
|
+
|
|
107
131
|
# Create issue
|
|
108
132
|
pi-linear-tools issue create --title "Fix login bug" --team ENG
|
|
109
133
|
pi-linear-tools issue create --title "New feature" --team ENG --project "My Project" --priority 2 --assignee me
|
|
@@ -114,6 +138,10 @@ pi-linear-tools issue update ENG-123 --title "Updated title" --assignee me
|
|
|
114
138
|
pi-linear-tools issue update ENG-123 --milestone "Sprint 1"
|
|
115
139
|
pi-linear-tools issue update ENG-123 --sub-issue-of ENG-100
|
|
116
140
|
|
|
141
|
+
# Issue priority uses Linear's native scale: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low.
|
|
142
|
+
# You can also use aliases: none, urgent, high, medium, low.
|
|
143
|
+
pi-linear-tools issue update ENG-123 --priority urgent
|
|
144
|
+
|
|
117
145
|
# Comment on issue
|
|
118
146
|
pi-linear-tools issue comment ENG-123 --body "This is fixed in PR #456"
|
|
119
147
|
pi-linear-tools issue comment ENG-123 --body "Blocked on API review"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { loadSettings, saveSettings } from '../src/settings.js';
|
|
2
|
-
import { createLinearClient, checkAndClearRateLimit, markRateLimited, getClientRequestMetrics } from '../src/linear-client.js';
|
|
2
|
+
import { createLinearClient, checkAndClearRateLimit, markRateLimited, getClientRequestMetrics, getClientRateLimitInfo } from '../src/linear-client.js';
|
|
3
3
|
import { setQuietMode, debug } from '../src/logger.js';
|
|
4
4
|
import {
|
|
5
5
|
resolveProjectRef,
|
|
@@ -48,6 +48,8 @@ try {
|
|
|
48
48
|
import {
|
|
49
49
|
executeIssueList,
|
|
50
50
|
executeIssueView,
|
|
51
|
+
executeIssueImages,
|
|
52
|
+
executeIssueDownload,
|
|
51
53
|
executeIssueActivity,
|
|
52
54
|
executeIssueCreate,
|
|
53
55
|
executeIssueUpdate,
|
|
@@ -120,12 +122,13 @@ async function createAuthenticatedClient() {
|
|
|
120
122
|
return createLinearClient(await getLinearAuth());
|
|
121
123
|
}
|
|
122
124
|
|
|
123
|
-
async function withRequestUsageLogging(client, toolName, action, operation) {
|
|
125
|
+
async function withRequestUsageLogging(client, toolName, action, operation, rateLimitDebug = false) {
|
|
124
126
|
const before = getClientRequestMetrics(client);
|
|
125
127
|
|
|
126
128
|
try {
|
|
127
129
|
const result = await operation();
|
|
128
130
|
const after = getClientRequestMetrics(client);
|
|
131
|
+
const rateLimitInfo = rateLimitDebug ? getClientRateLimitInfo(client) : null;
|
|
129
132
|
|
|
130
133
|
const usageDelta = {
|
|
131
134
|
tool: toolName,
|
|
@@ -139,17 +142,63 @@ async function withRequestUsageLogging(client, toolName, action, operation) {
|
|
|
139
142
|
|
|
140
143
|
debug('[pi-linear-tools] API usage per command', usageDelta);
|
|
141
144
|
|
|
142
|
-
if (
|
|
145
|
+
if (rateLimitDebug && rateLimitInfo) {
|
|
146
|
+
debug('[pi-linear-tools] Rate limit status', {
|
|
147
|
+
tool: toolName,
|
|
148
|
+
action,
|
|
149
|
+
requestsDelta: usageDelta.requestsDelta,
|
|
150
|
+
rateLimitUsed: rateLimitInfo.used,
|
|
151
|
+
rateLimitTotal: rateLimitInfo.total,
|
|
152
|
+
rateLimitRemaining: rateLimitInfo.remaining,
|
|
153
|
+
rateLimitPercent: rateLimitInfo.usagePercent,
|
|
154
|
+
rateLimitResetAt: rateLimitInfo.resetTime,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// When rateLimitDebug is true, always include rate limit info in result details
|
|
159
|
+
// even if INCLUDE_USAGE_SUMMARY is disabled
|
|
160
|
+
if (!result || typeof result !== 'object') {
|
|
143
161
|
return result;
|
|
144
162
|
}
|
|
145
163
|
|
|
146
164
|
const details = (result.details && typeof result.details === 'object') ? result.details : {};
|
|
165
|
+
|
|
166
|
+
// Add rate limit info to details if debug is enabled
|
|
167
|
+
if (rateLimitDebug && rateLimitInfo) {
|
|
168
|
+
details.rateLimit = {
|
|
169
|
+
...rateLimitInfo,
|
|
170
|
+
requestsDelta: usageDelta.requestsDelta,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!INCLUDE_USAGE_SUMMARY && !rateLimitDebug) {
|
|
175
|
+
return {
|
|
176
|
+
...result,
|
|
177
|
+
details,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
147
181
|
const content = Array.isArray(result.content)
|
|
148
182
|
? result.content.map((item, idx) => {
|
|
149
183
|
if (idx !== 0 || item?.type !== 'text' || typeof item.text !== 'string') return item;
|
|
184
|
+
let appendedText = item.text;
|
|
185
|
+
|
|
186
|
+
if (INCLUDE_USAGE_SUMMARY) {
|
|
187
|
+
appendedText += `\n\n_${usageDelta.summary}_`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (rateLimitDebug && rateLimitInfo) {
|
|
191
|
+
const percent = rateLimitInfo.usagePercent === null ? 'unknown' : `${rateLimitInfo.usagePercent}%`;
|
|
192
|
+
const windowUsage = rateLimitInfo.remaining === null
|
|
193
|
+
? 'request window: unknown'
|
|
194
|
+
: `request window: ${rateLimitInfo.used}/${rateLimitInfo.total} used (${percent})`;
|
|
195
|
+
const reset = rateLimitInfo.resetTime ? ` • Resets at ${rateLimitInfo.resetTime}` : '';
|
|
196
|
+
appendedText += `\n\n---\n**Rate Limit**: +${usageDelta.requestsDelta} requests this call | ${windowUsage}${reset}`;
|
|
197
|
+
}
|
|
198
|
+
|
|
150
199
|
return {
|
|
151
200
|
...item,
|
|
152
|
-
text:
|
|
201
|
+
text: appendedText,
|
|
153
202
|
};
|
|
154
203
|
})
|
|
155
204
|
: result.content;
|
|
@@ -159,11 +208,12 @@ async function withRequestUsageLogging(client, toolName, action, operation) {
|
|
|
159
208
|
content,
|
|
160
209
|
details: {
|
|
161
210
|
...details,
|
|
162
|
-
apiUsage: usageDelta,
|
|
211
|
+
...(INCLUDE_USAGE_SUMMARY ? { apiUsage: usageDelta } : {}),
|
|
163
212
|
},
|
|
164
213
|
};
|
|
165
214
|
} catch (error) {
|
|
166
215
|
const after = getClientRequestMetrics(client);
|
|
216
|
+
const rateLimitInfo = rateLimitDebug ? getClientRateLimitInfo(client) : null;
|
|
167
217
|
|
|
168
218
|
debug('[pi-linear-tools] API usage per command (error)', {
|
|
169
219
|
tool: toolName,
|
|
@@ -173,6 +223,13 @@ async function withRequestUsageLogging(client, toolName, action, operation) {
|
|
|
173
223
|
failedDelta: after.failed - before.failed,
|
|
174
224
|
rateLimitedDelta: after.rateLimited - before.rateLimited,
|
|
175
225
|
error: String(error?.message || error || 'unknown'),
|
|
226
|
+
...(rateLimitDebug && rateLimitInfo ? {
|
|
227
|
+
requestsDelta: after.total - before.total,
|
|
228
|
+
rateLimitUsed: rateLimitInfo.used,
|
|
229
|
+
rateLimitTotal: rateLimitInfo.total,
|
|
230
|
+
rateLimitRemaining: rateLimitInfo.remaining,
|
|
231
|
+
rateLimitPercent: rateLimitInfo.usagePercent,
|
|
232
|
+
} : {}),
|
|
176
233
|
});
|
|
177
234
|
|
|
178
235
|
throw error;
|
|
@@ -460,21 +517,35 @@ function toToolTextResult(text, details = {}) {
|
|
|
460
517
|
}
|
|
461
518
|
|
|
462
519
|
function buildRateLimitToolResult(error, options = {}) {
|
|
463
|
-
const { cached = false } = options;
|
|
520
|
+
const { cached = false, viaCachedPreCheck = false } = options;
|
|
464
521
|
const resetTimestamp = error?.requestsResetAt || Date.now() + 3600000;
|
|
465
522
|
const resetTime = new Date(resetTimestamp).toLocaleTimeString();
|
|
466
523
|
|
|
467
524
|
markRateLimited(resetTimestamp);
|
|
468
525
|
|
|
469
526
|
return toToolTextResult(
|
|
470
|
-
|
|
471
|
-
? `Linear API rate limit exceeded (cached
|
|
472
|
-
|
|
527
|
+
viaCachedPreCheck
|
|
528
|
+
? `Linear API rate limit exceeded (cached — this request was not sent to Linear).
|
|
529
|
+
|
|
530
|
+
The rate limit resets at: ${resetTime}
|
|
531
|
+
|
|
532
|
+
Please wait before making more requests.`
|
|
533
|
+
: cached
|
|
534
|
+
? `Linear API rate limit exceeded (cached).
|
|
535
|
+
|
|
536
|
+
The rate limit resets at: ${resetTime}
|
|
537
|
+
|
|
538
|
+
Please wait before making more requests.`
|
|
539
|
+
: `Linear API rate limit exceeded.
|
|
540
|
+
|
|
541
|
+
The rate limit resets at: ${resetTime}
|
|
542
|
+
|
|
543
|
+
Please wait before making more requests, or reduce the frequency of API calls.`,
|
|
473
544
|
{
|
|
474
545
|
error: true,
|
|
475
546
|
errorType: 'Ratelimited',
|
|
476
547
|
rateLimited: true,
|
|
477
|
-
cached,
|
|
548
|
+
cached: viaCachedPreCheck || cached,
|
|
478
549
|
requestsResetAt: resetTimestamp,
|
|
479
550
|
resetTime,
|
|
480
551
|
}
|
|
@@ -487,7 +558,7 @@ function handleToolExecutionError(error, operationLabel, options = {}) {
|
|
|
487
558
|
const errorMessage = String(transformedError?.message || transformedError || 'Unknown error');
|
|
488
559
|
|
|
489
560
|
if (errorType === 'Ratelimited' || errorMessage.toLowerCase().includes('rate limit')) {
|
|
490
|
-
return buildRateLimitToolResult(
|
|
561
|
+
return buildRateLimitToolResult(transformedError, { viaCachedPreCheck: options.viaCachedPreCheck });
|
|
491
562
|
}
|
|
492
563
|
|
|
493
564
|
if (errorMessage.includes('Linear API error:')) {
|
|
@@ -571,18 +642,18 @@ async function registerLinearTools(pi) {
|
|
|
571
642
|
name: 'linear_issue',
|
|
572
643
|
label: 'Linear Issue',
|
|
573
644
|
description: 'Interact with Linear issues.',
|
|
574
|
-
promptSnippet: 'Interact with Linear issues (list, view, activity, create, update, comment, start, delete)',
|
|
645
|
+
promptSnippet: 'Interact with Linear issues (list, view, images, download, activity, create, update, comment, start, delete)',
|
|
575
646
|
parameters: {
|
|
576
647
|
type: 'object',
|
|
577
648
|
properties: {
|
|
578
649
|
action: {
|
|
579
650
|
type: 'string',
|
|
580
|
-
enum: ['list', 'view', 'activity', 'create', 'update', 'comment', 'start', 'delete'],
|
|
651
|
+
enum: ['list', 'view', 'images', 'download', 'activity', 'create', 'update', 'comment', 'start', 'delete'],
|
|
581
652
|
description: 'Action to perform on issue(s)',
|
|
582
653
|
},
|
|
583
654
|
issue: {
|
|
584
655
|
type: 'string',
|
|
585
|
-
description: 'Issue key (ABC-123) or Linear issue ID (for view, activity, update, comment, start, delete)',
|
|
656
|
+
description: 'Issue key (ABC-123) or Linear issue ID (for view, images, download, activity, update, comment, start, delete)',
|
|
586
657
|
},
|
|
587
658
|
project: {
|
|
588
659
|
type: 'string',
|
|
@@ -603,11 +674,46 @@ async function registerLinearTools(pi) {
|
|
|
603
674
|
},
|
|
604
675
|
limit: {
|
|
605
676
|
type: 'integer',
|
|
606
|
-
description: 'Maximum number of issues
|
|
677
|
+
description: 'Maximum number of issues, activity entries, or images to fetch',
|
|
607
678
|
},
|
|
608
679
|
includeComments: {
|
|
609
680
|
type: 'boolean',
|
|
610
|
-
description: 'Include comments when viewing issue (default: true)',
|
|
681
|
+
description: 'Include comments when viewing an issue or fetching images (default: true)',
|
|
682
|
+
},
|
|
683
|
+
attachmentId: {
|
|
684
|
+
type: 'string',
|
|
685
|
+
description: 'Attachment ID to download (for download). Use exactly one attachment selector when multiple attachments exist.',
|
|
686
|
+
},
|
|
687
|
+
attachmentTitle: {
|
|
688
|
+
type: 'string',
|
|
689
|
+
description: 'Attachment title to download (for download). Must uniquely match an issue attachment.',
|
|
690
|
+
},
|
|
691
|
+
attachmentUrl: {
|
|
692
|
+
type: 'string',
|
|
693
|
+
description: 'Attachment URL to download (for download). Must uniquely match an issue attachment.',
|
|
694
|
+
},
|
|
695
|
+
attachmentIndex: {
|
|
696
|
+
type: 'integer',
|
|
697
|
+
description: '1-based attachment index from the issue attachment list (for download).',
|
|
698
|
+
minimum: 1,
|
|
699
|
+
},
|
|
700
|
+
directory: {
|
|
701
|
+
type: 'string',
|
|
702
|
+
description: 'Relative destination directory for attachment downloads. Missing directories are created.',
|
|
703
|
+
},
|
|
704
|
+
filename: {
|
|
705
|
+
type: 'string',
|
|
706
|
+
description: 'Optional output filename for attachment downloads. Unsafe characters are sanitized.',
|
|
707
|
+
},
|
|
708
|
+
overwrite: {
|
|
709
|
+
type: 'boolean',
|
|
710
|
+
description: 'Overwrite existing file during download (default: false). Requires allow_overwrite_files=true in config.',
|
|
711
|
+
},
|
|
712
|
+
maxBytes: {
|
|
713
|
+
type: 'integer',
|
|
714
|
+
description: 'Maximum bytes to fetch. For images, applies per image with a 10 MiB handler limit. For download, default/max is 52428800.',
|
|
715
|
+
minimum: 1,
|
|
716
|
+
maximum: 52428800,
|
|
611
717
|
},
|
|
612
718
|
title: {
|
|
613
719
|
type: 'string',
|
|
@@ -618,8 +724,19 @@ async function registerLinearTools(pi) {
|
|
|
618
724
|
description: 'Issue description in markdown (for create, update)',
|
|
619
725
|
},
|
|
620
726
|
priority: {
|
|
621
|
-
|
|
622
|
-
|
|
727
|
+
description: 'Issue priority: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low (Linear-native scale), or one of: none, urgent, high, medium, low (for create/update)',
|
|
728
|
+
oneOf: [
|
|
729
|
+
{
|
|
730
|
+
type: 'integer',
|
|
731
|
+
minimum: 0,
|
|
732
|
+
maximum: 4,
|
|
733
|
+
multipleOf: 1,
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
type: 'string',
|
|
737
|
+
enum: ['none', 'urgent', 'high', 'medium', 'low'],
|
|
738
|
+
},
|
|
739
|
+
],
|
|
623
740
|
},
|
|
624
741
|
includeArchived: {
|
|
625
742
|
type: 'boolean',
|
|
@@ -668,6 +785,7 @@ async function registerLinearTools(pi) {
|
|
|
668
785
|
estimate: {
|
|
669
786
|
type: 'integer',
|
|
670
787
|
description: 'Estimate/story points for the issue (non-negative integer, for create/update)',
|
|
788
|
+
minimum: 0,
|
|
671
789
|
},
|
|
672
790
|
team: {
|
|
673
791
|
type: 'string',
|
|
@@ -707,6 +825,8 @@ async function registerLinearTools(pi) {
|
|
|
707
825
|
return buildRateLimitToolResult({ requestsResetAt: resetAt.getTime(), type: 'Ratelimited' }, { cached: true });
|
|
708
826
|
}
|
|
709
827
|
|
|
828
|
+
const settings = await loadSettings();
|
|
829
|
+
const rateLimitDebug = settings.rateLimitDebug || false;
|
|
710
830
|
const client = await createAuthenticatedClient();
|
|
711
831
|
|
|
712
832
|
return await withRequestUsageLogging(client, 'linear_issue', params.action, async () => {
|
|
@@ -715,6 +835,10 @@ async function registerLinearTools(pi) {
|
|
|
715
835
|
return await executeIssueList(client, params);
|
|
716
836
|
case 'view':
|
|
717
837
|
return await executeIssueView(client, params);
|
|
838
|
+
case 'images':
|
|
839
|
+
return await executeIssueImages(client, params);
|
|
840
|
+
case 'download':
|
|
841
|
+
return await executeIssueDownload(client, params, { settings });
|
|
718
842
|
case 'activity':
|
|
719
843
|
return await executeIssueActivity(client, params);
|
|
720
844
|
case 'create':
|
|
@@ -734,7 +858,7 @@ async function registerLinearTools(pi) {
|
|
|
734
858
|
default:
|
|
735
859
|
throw new Error(`Unknown action: ${params.action}`);
|
|
736
860
|
}
|
|
737
|
-
});
|
|
861
|
+
}, rateLimitDebug);
|
|
738
862
|
});
|
|
739
863
|
},
|
|
740
864
|
});
|
|
@@ -802,6 +926,14 @@ async function registerLinearTools(pi) {
|
|
|
802
926
|
renderResult: renderMarkdownResult,
|
|
803
927
|
async execute(_toolCallId, params) {
|
|
804
928
|
return executeToolSafely('Linear project operation failed', async () => {
|
|
929
|
+
// Pre-check: skip API calls if we know we're rate limited
|
|
930
|
+
const { isRateLimited, resetAt } = checkAndClearRateLimit();
|
|
931
|
+
if (isRateLimited) {
|
|
932
|
+
return buildRateLimitToolResult({ requestsResetAt: resetAt.getTime(), type: 'Ratelimited' }, { cached: true });
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const settings = await loadSettings();
|
|
936
|
+
const rateLimitDebug = settings.rateLimitDebug || false;
|
|
805
937
|
const client = await createAuthenticatedClient();
|
|
806
938
|
|
|
807
939
|
return await withRequestUsageLogging(client, 'linear_project', params.action, async () => {
|
|
@@ -823,7 +955,7 @@ async function registerLinearTools(pi) {
|
|
|
823
955
|
default:
|
|
824
956
|
throw new Error(`Unknown action: ${params.action}`);
|
|
825
957
|
}
|
|
826
|
-
});
|
|
958
|
+
}, rateLimitDebug);
|
|
827
959
|
});
|
|
828
960
|
},
|
|
829
961
|
});
|
|
@@ -879,6 +1011,14 @@ async function registerLinearTools(pi) {
|
|
|
879
1011
|
renderResult: renderMarkdownResult,
|
|
880
1012
|
async execute(_toolCallId, params) {
|
|
881
1013
|
return executeToolSafely('Linear project update operation failed', async () => {
|
|
1014
|
+
// Pre-check: skip API calls if we know we're rate limited
|
|
1015
|
+
const { isRateLimited, resetAt } = checkAndClearRateLimit();
|
|
1016
|
+
if (isRateLimited) {
|
|
1017
|
+
return buildRateLimitToolResult({ requestsResetAt: resetAt.getTime(), type: 'Ratelimited' }, { cached: true });
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const settings = await loadSettings();
|
|
1021
|
+
const rateLimitDebug = settings.rateLimitDebug || false;
|
|
882
1022
|
const client = await createAuthenticatedClient();
|
|
883
1023
|
|
|
884
1024
|
return await withRequestUsageLogging(client, 'linear_project_update', params.action, async () => {
|
|
@@ -898,7 +1038,7 @@ async function registerLinearTools(pi) {
|
|
|
898
1038
|
default:
|
|
899
1039
|
throw new Error(`Unknown action: ${params.action}`);
|
|
900
1040
|
}
|
|
901
|
-
});
|
|
1041
|
+
}, rateLimitDebug);
|
|
902
1042
|
});
|
|
903
1043
|
},
|
|
904
1044
|
});
|
|
@@ -923,6 +1063,8 @@ async function registerLinearTools(pi) {
|
|
|
923
1063
|
renderResult: renderMarkdownResult,
|
|
924
1064
|
async execute(_toolCallId, params) {
|
|
925
1065
|
return executeToolSafely('Linear team operation failed', async () => {
|
|
1066
|
+
const settings = await loadSettings();
|
|
1067
|
+
const rateLimitDebug = settings.rateLimitDebug || false;
|
|
926
1068
|
const client = await createAuthenticatedClient();
|
|
927
1069
|
|
|
928
1070
|
return await withRequestUsageLogging(client, 'linear_team', params.action, async () => {
|
|
@@ -932,7 +1074,7 @@ async function registerLinearTools(pi) {
|
|
|
932
1074
|
default:
|
|
933
1075
|
throw new Error(`Unknown action: ${params.action}`);
|
|
934
1076
|
}
|
|
935
|
-
});
|
|
1077
|
+
}, rateLimitDebug);
|
|
936
1078
|
});
|
|
937
1079
|
},
|
|
938
1080
|
});
|
|
@@ -978,8 +1120,17 @@ async function registerLinearTools(pi) {
|
|
|
978
1120
|
renderResult: renderMarkdownResult,
|
|
979
1121
|
async execute(_toolCallId, params) {
|
|
980
1122
|
return executeToolSafely('Linear milestone operation failed', async () => {
|
|
1123
|
+
// Pre-check: skip API calls if we know we're rate limited
|
|
1124
|
+
const { isRateLimited, resetAt } = checkAndClearRateLimit();
|
|
1125
|
+
if (isRateLimited) {
|
|
1126
|
+
return buildRateLimitToolResult({ requestsResetAt: resetAt.getTime(), type: 'Ratelimited' }, { viaCachedPreCheck: true });
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
const settings = await loadSettings();
|
|
1130
|
+
const rateLimitDebug = settings.rateLimitDebug || false;
|
|
981
1131
|
const client = await createAuthenticatedClient();
|
|
982
1132
|
|
|
1133
|
+
|
|
983
1134
|
return await withRequestUsageLogging(client, 'linear_milestone', params.action, async () => {
|
|
984
1135
|
switch (params.action) {
|
|
985
1136
|
case 'list':
|
|
@@ -995,7 +1146,7 @@ async function registerLinearTools(pi) {
|
|
|
995
1146
|
default:
|
|
996
1147
|
throw new Error(`Unknown action: ${params.action}`);
|
|
997
1148
|
}
|
|
998
|
-
});
|
|
1149
|
+
}, rateLimitDebug);
|
|
999
1150
|
}, {
|
|
1000
1151
|
transformError: withMilestoneScopeHint,
|
|
1001
1152
|
});
|
|
@@ -1008,13 +1159,15 @@ export default async function piLinearToolsExtension(pi) {
|
|
|
1008
1159
|
// Safety wrapper: never let extension errors crash pi
|
|
1009
1160
|
try {
|
|
1010
1161
|
pi.registerCommand('linear-tools-config', {
|
|
1011
|
-
description: 'Configure pi-linear-tools settings (API key
|
|
1162
|
+
description: 'Configure pi-linear-tools settings (API key, default team, rate limit debug, file overwrite guard)',
|
|
1012
1163
|
handler: async (argsText, ctx) => {
|
|
1013
1164
|
const args = parseArgs(argsText);
|
|
1014
1165
|
const apiKey = readFlag(args, '--api-key');
|
|
1015
1166
|
const defaultTeam = readFlag(args, '--default-team');
|
|
1016
1167
|
const projectTeam = readFlag(args, '--team');
|
|
1017
1168
|
const projectName = readFlag(args, '--project');
|
|
1169
|
+
const rateLimitDebug = readFlag(args, '--rate-limit-debug');
|
|
1170
|
+
const allowOverwriteFiles = readFlag(args, '--allow-overwrite-files');
|
|
1018
1171
|
|
|
1019
1172
|
if (apiKey) {
|
|
1020
1173
|
const settings = await loadSettings();
|
|
@@ -1035,6 +1188,28 @@ export default async function piLinearToolsExtension(pi) {
|
|
|
1035
1188
|
return;
|
|
1036
1189
|
}
|
|
1037
1190
|
|
|
1191
|
+
if (rateLimitDebug) {
|
|
1192
|
+
const settings = await loadSettings();
|
|
1193
|
+
const enabled = rateLimitDebug === 'true' || rateLimitDebug === '1';
|
|
1194
|
+
settings.rateLimitDebug = enabled;
|
|
1195
|
+
await saveSettings(settings);
|
|
1196
|
+
if (ctx?.hasUI) {
|
|
1197
|
+
ctx.ui.notify(`Rate limit debug ${enabled ? 'enabled' : 'disabled'}`, 'info');
|
|
1198
|
+
}
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
if (allowOverwriteFiles) {
|
|
1203
|
+
const settings = await loadSettings();
|
|
1204
|
+
const enabled = allowOverwriteFiles === 'true' || allowOverwriteFiles === '1';
|
|
1205
|
+
settings.allow_overwrite_files = enabled;
|
|
1206
|
+
await saveSettings(settings);
|
|
1207
|
+
if (ctx?.hasUI) {
|
|
1208
|
+
ctx.ui.notify(`File overwrite guard ${enabled ? 'allows overwrites' : 'blocks overwrites'}`, 'info');
|
|
1209
|
+
}
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1038
1213
|
if (defaultTeam) {
|
|
1039
1214
|
const settings = await loadSettings();
|
|
1040
1215
|
settings.defaultTeam = defaultTeam;
|
|
@@ -1078,18 +1253,24 @@ export default async function piLinearToolsExtension(pi) {
|
|
|
1078
1253
|
return;
|
|
1079
1254
|
}
|
|
1080
1255
|
|
|
1081
|
-
if (!apiKey && !defaultTeam && !projectTeam && !projectName && ctx?.hasUI && ctx?.ui) {
|
|
1256
|
+
if (!apiKey && !defaultTeam && !projectTeam && !projectName && !rateLimitDebug && !allowOverwriteFiles && ctx?.hasUI && ctx?.ui) {
|
|
1082
1257
|
await runInteractiveConfigFlow(ctx, pi);
|
|
1083
1258
|
return;
|
|
1084
1259
|
}
|
|
1085
1260
|
|
|
1261
|
+
// Show current settings if no valid action was specified
|
|
1262
|
+
if (apiKey || defaultTeam || projectTeam || projectName || rateLimitDebug || allowOverwriteFiles) {
|
|
1263
|
+
// Actions above already handled and returned
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1086
1267
|
const settings = await loadSettings();
|
|
1087
1268
|
const hasKey = !!(settings.apiKey || settings.linearApiKey || process.env.LINEAR_API_KEY);
|
|
1088
1269
|
const keySource = process.env.LINEAR_API_KEY ? 'environment' : (settings.apiKey || settings.linearApiKey ? 'settings' : 'not set');
|
|
1089
1270
|
|
|
1090
1271
|
pi.sendMessage({
|
|
1091
1272
|
customType: 'pi-linear-tools',
|
|
1092
|
-
content: `Configuration:\n LINEAR_API_KEY: ${hasKey ? 'configured' : 'not set'} (source: ${keySource})\n Default workspace: ${settings.defaultWorkspace?.name || 'not set'}\n Default team: ${settings.defaultTeam || 'not set'}\n Project team mappings: ${Object.keys(settings.projects || {}).length}\n\nCommands:\n /linear-tools-config --api-key lin_xxx\n /linear-tools-config --default-team ENG\n /linear-tools-config --team ENG --project MyProject\n\nNote: environment LINEAR_API_KEY takes precedence over settings file.`,
|
|
1273
|
+
content: `Configuration:\n LINEAR_API_KEY: ${hasKey ? 'configured' : 'not set'} (source: ${keySource})\n Default workspace: ${settings.defaultWorkspace?.name || 'not set'}\n Default team: ${settings.defaultTeam || 'not set'}\n Rate limit debug: ${settings.rateLimitDebug ? 'enabled' : 'disabled'}\n Allow overwrite files: ${settings.allow_overwrite_files ? 'enabled' : 'disabled'}\n Project team mappings: ${Object.keys(settings.projects || {}).length}\n\nCommands:\n /linear-tools-config --api-key lin_xxx\n /linear-tools-config --default-team ENG\n /linear-tools-config --team ENG --project MyProject\n /linear-tools-config --rate-limit-debug true|false\n /linear-tools-config --allow-overwrite-files true|false\n\nNote: environment LINEAR_API_KEY takes precedence over settings file.`,
|
|
1093
1274
|
display: true,
|
|
1094
1275
|
});
|
|
1095
1276
|
},
|
|
@@ -1115,7 +1296,7 @@ export default async function piLinearToolsExtension(pi) {
|
|
|
1115
1296
|
const showMilestoneTool = await shouldExposeMilestoneTool();
|
|
1116
1297
|
const toolLines = [
|
|
1117
1298
|
'LLM-callable tools:',
|
|
1118
|
-
' linear_issue (list/view/activity/create/update/comment/start/delete)',
|
|
1299
|
+
' linear_issue (list/view/images/download/activity/create/update/comment/start/delete)',
|
|
1119
1300
|
' linear_project (list/view/create/update/delete/archive/unarchive)',
|
|
1120
1301
|
' linear_project_update (list/view/create/update/archive/unarchive)',
|
|
1121
1302
|
' linear_team (list)',
|
|
@@ -1134,6 +1315,8 @@ export default async function piLinearToolsExtension(pi) {
|
|
|
1134
1315
|
' /linear-tools-config --api-key <key>',
|
|
1135
1316
|
' /linear-tools-config --default-team <team-key>',
|
|
1136
1317
|
' /linear-tools-config --team <team-key> --project <project-name-or-id>',
|
|
1318
|
+
' /linear-tools-config --rate-limit-debug true|false',
|
|
1319
|
+
' /linear-tools-config --allow-overwrite-files true|false',
|
|
1137
1320
|
' /linear-tools-help',
|
|
1138
1321
|
' /linear-tools-reload',
|
|
1139
1322
|
'',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fink-andreas/pi-linear-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Pi extension with Linear SDK tools and configuration commands",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
29
|
"start": "node index.js",
|
|
30
|
-
"test": "node tests/test-package-manifest.js && node tests/test-extension-registration.js && node tests/test-settings.js && node tests/test-api-usage-caching.js && node tests/test-assignee-update.js && node tests/test-rate-limit-fallback-update.js && node tests/test-full-assignee-flow.js && node tests/test-branch-param.js && node tests/test-team-filter.js && node tests/test-project-crud.js && node tests/test-project-lifecycle.js && node tests/test-sync-doc.js && node tests/test-issue-activity.js",
|
|
30
|
+
"test": "node tests/test-package-manifest.js && node tests/test-extension-registration.js && node tests/test-issue-comment-result.js && node tests/test-issue-priority.js && node tests/test-settings.js && node tests/test-issue-download.js && node tests/test-api-usage-caching.js && node tests/test-assignee-update.js && node tests/test-rate-limit-fallback-update.js && node tests/test-full-assignee-flow.js && node tests/test-branch-param.js && node tests/test-team-filter.js && node tests/test-project-crud.js && node tests/test-project-lifecycle.js && node tests/test-sync-doc.js && node tests/test-issue-activity.js",
|
|
31
31
|
"dev:sync-local-extension": "node scripts/dev-sync-local-extension.mjs",
|
|
32
32
|
"release:check": "npm test && npm pack --dry-run"
|
|
33
33
|
},
|
package/settings.json.example
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"schemaVersion":
|
|
3
|
-
"
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"authMethod": "api-key",
|
|
4
|
+
"apiKey": "lin_xxx",
|
|
4
5
|
"defaultTeam": "ENG",
|
|
6
|
+
"defaultWorkspace": null,
|
|
7
|
+
"rateLimitDebug": false,
|
|
8
|
+
"allow_overwrite_files": false,
|
|
5
9
|
"projects": {
|
|
6
10
|
"97ec7cae-e252-493d-94d3-6910aa28cacf": {
|
|
7
11
|
"scope": {
|