@fink-andreas/pi-linear-tools 0.6.0 → 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 +21 -0
- package/README.md +20 -2
- package/extensions/pi-linear-tools.js +104 -15
- 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 +72 -17
- package/src/linear.js +196 -21
- package/src/settings.js +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
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
|
+
|
|
3
24
|
## v0.6.0 (2026-04-23)
|
|
4
25
|
|
|
5
26
|
Rate-limit resilience for milestone operations.
|
package/README.md
CHANGED
|
@@ -41,6 +41,7 @@ Optional non-interactive commands:
|
|
|
41
41
|
/linear-tools-config --api-key lin_xxx
|
|
42
42
|
/linear-tools-config --default-team ENG
|
|
43
43
|
/linear-tools-config --team ENG --project "My Project"
|
|
44
|
+
/linear-tools-config --allow-overwrite-files true|false
|
|
44
45
|
```
|
|
45
46
|
|
|
46
47
|
## Extension commands
|
|
@@ -67,7 +68,11 @@ Reference conventions:
|
|
|
67
68
|
## LLM-callable tools
|
|
68
69
|
|
|
69
70
|
### `linear_issue`
|
|
70
|
-
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`.
|
|
71
76
|
|
|
72
77
|
### `linear_project`
|
|
73
78
|
Actions: `list`, `view`, `create`, `update`, `delete`, `archive`, `unarchive`
|
|
@@ -94,11 +99,12 @@ pi-linear-tools config
|
|
|
94
99
|
pi-linear-tools config --api-key lin_xxx
|
|
95
100
|
pi-linear-tools config --default-team ENG
|
|
96
101
|
pi-linear-tools config --team ENG --project "My Project"
|
|
102
|
+
pi-linear-tools config --allow-overwrite-files true|false
|
|
97
103
|
```
|
|
98
104
|
|
|
99
105
|
### Issue commands
|
|
100
106
|
|
|
101
|
-
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.
|
|
102
108
|
|
|
103
109
|
```bash
|
|
104
110
|
# List issues
|
|
@@ -110,10 +116,18 @@ pi-linear-tools issue list --project "My Project" --assignee me
|
|
|
110
116
|
pi-linear-tools issue view ENG-123
|
|
111
117
|
pi-linear-tools issue view ENG-123 --no-comments
|
|
112
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
|
+
|
|
113
123
|
# Read Activity timeline
|
|
114
124
|
pi-linear-tools issue activity ENG-123 --limit 20
|
|
115
125
|
pi-linear-tools issue activity https://linear.app/workspace/issue/ENG-123/example --limit 20
|
|
116
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
|
+
|
|
117
131
|
# Create issue
|
|
118
132
|
pi-linear-tools issue create --title "Fix login bug" --team ENG
|
|
119
133
|
pi-linear-tools issue create --title "New feature" --team ENG --project "My Project" --priority 2 --assignee me
|
|
@@ -124,6 +138,10 @@ pi-linear-tools issue update ENG-123 --title "Updated title" --assignee me
|
|
|
124
138
|
pi-linear-tools issue update ENG-123 --milestone "Sprint 1"
|
|
125
139
|
pi-linear-tools issue update ENG-123 --sub-issue-of ENG-100
|
|
126
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
|
+
|
|
127
145
|
# Comment on issue
|
|
128
146
|
pi-linear-tools issue comment ENG-123 --body "This is fixed in PR #456"
|
|
129
147
|
pi-linear-tools issue comment ENG-123 --body "Blocked on API review"
|
|
@@ -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,
|
|
@@ -144,7 +146,9 @@ async function withRequestUsageLogging(client, toolName, action, operation, rate
|
|
|
144
146
|
debug('[pi-linear-tools] Rate limit status', {
|
|
145
147
|
tool: toolName,
|
|
146
148
|
action,
|
|
149
|
+
requestsDelta: usageDelta.requestsDelta,
|
|
147
150
|
rateLimitUsed: rateLimitInfo.used,
|
|
151
|
+
rateLimitTotal: rateLimitInfo.total,
|
|
148
152
|
rateLimitRemaining: rateLimitInfo.remaining,
|
|
149
153
|
rateLimitPercent: rateLimitInfo.usagePercent,
|
|
150
154
|
rateLimitResetAt: rateLimitInfo.resetTime,
|
|
@@ -161,7 +165,10 @@ async function withRequestUsageLogging(client, toolName, action, operation, rate
|
|
|
161
165
|
|
|
162
166
|
// Add rate limit info to details if debug is enabled
|
|
163
167
|
if (rateLimitDebug && rateLimitInfo) {
|
|
164
|
-
details.rateLimit =
|
|
168
|
+
details.rateLimit = {
|
|
169
|
+
...rateLimitInfo,
|
|
170
|
+
requestsDelta: usageDelta.requestsDelta,
|
|
171
|
+
};
|
|
165
172
|
}
|
|
166
173
|
|
|
167
174
|
if (!INCLUDE_USAGE_SUMMARY && !rateLimitDebug) {
|
|
@@ -181,8 +188,12 @@ async function withRequestUsageLogging(client, toolName, action, operation, rate
|
|
|
181
188
|
}
|
|
182
189
|
|
|
183
190
|
if (rateLimitDebug && rateLimitInfo) {
|
|
184
|
-
const percent = rateLimitInfo.usagePercent
|
|
185
|
-
|
|
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}`;
|
|
186
197
|
}
|
|
187
198
|
|
|
188
199
|
return {
|
|
@@ -213,7 +224,9 @@ async function withRequestUsageLogging(client, toolName, action, operation, rate
|
|
|
213
224
|
rateLimitedDelta: after.rateLimited - before.rateLimited,
|
|
214
225
|
error: String(error?.message || error || 'unknown'),
|
|
215
226
|
...(rateLimitDebug && rateLimitInfo ? {
|
|
227
|
+
requestsDelta: after.total - before.total,
|
|
216
228
|
rateLimitUsed: rateLimitInfo.used,
|
|
229
|
+
rateLimitTotal: rateLimitInfo.total,
|
|
217
230
|
rateLimitRemaining: rateLimitInfo.remaining,
|
|
218
231
|
rateLimitPercent: rateLimitInfo.usagePercent,
|
|
219
232
|
} : {}),
|
|
@@ -629,18 +642,18 @@ async function registerLinearTools(pi) {
|
|
|
629
642
|
name: 'linear_issue',
|
|
630
643
|
label: 'Linear Issue',
|
|
631
644
|
description: 'Interact with Linear issues.',
|
|
632
|
-
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)',
|
|
633
646
|
parameters: {
|
|
634
647
|
type: 'object',
|
|
635
648
|
properties: {
|
|
636
649
|
action: {
|
|
637
650
|
type: 'string',
|
|
638
|
-
enum: ['list', 'view', 'activity', 'create', 'update', 'comment', 'start', 'delete'],
|
|
651
|
+
enum: ['list', 'view', 'images', 'download', 'activity', 'create', 'update', 'comment', 'start', 'delete'],
|
|
639
652
|
description: 'Action to perform on issue(s)',
|
|
640
653
|
},
|
|
641
654
|
issue: {
|
|
642
655
|
type: 'string',
|
|
643
|
-
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)',
|
|
644
657
|
},
|
|
645
658
|
project: {
|
|
646
659
|
type: 'string',
|
|
@@ -661,11 +674,46 @@ async function registerLinearTools(pi) {
|
|
|
661
674
|
},
|
|
662
675
|
limit: {
|
|
663
676
|
type: 'integer',
|
|
664
|
-
description: 'Maximum number of issues
|
|
677
|
+
description: 'Maximum number of issues, activity entries, or images to fetch',
|
|
665
678
|
},
|
|
666
679
|
includeComments: {
|
|
667
680
|
type: 'boolean',
|
|
668
|
-
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,
|
|
669
717
|
},
|
|
670
718
|
title: {
|
|
671
719
|
type: 'string',
|
|
@@ -676,8 +724,19 @@ async function registerLinearTools(pi) {
|
|
|
676
724
|
description: 'Issue description in markdown (for create, update)',
|
|
677
725
|
},
|
|
678
726
|
priority: {
|
|
679
|
-
|
|
680
|
-
|
|
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
|
+
],
|
|
681
740
|
},
|
|
682
741
|
includeArchived: {
|
|
683
742
|
type: 'boolean',
|
|
@@ -726,6 +785,7 @@ async function registerLinearTools(pi) {
|
|
|
726
785
|
estimate: {
|
|
727
786
|
type: 'integer',
|
|
728
787
|
description: 'Estimate/story points for the issue (non-negative integer, for create/update)',
|
|
788
|
+
minimum: 0,
|
|
729
789
|
},
|
|
730
790
|
team: {
|
|
731
791
|
type: 'string',
|
|
@@ -775,6 +835,10 @@ async function registerLinearTools(pi) {
|
|
|
775
835
|
return await executeIssueList(client, params);
|
|
776
836
|
case 'view':
|
|
777
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 });
|
|
778
842
|
case 'activity':
|
|
779
843
|
return await executeIssueActivity(client, params);
|
|
780
844
|
case 'create':
|
|
@@ -862,6 +926,12 @@ async function registerLinearTools(pi) {
|
|
|
862
926
|
renderResult: renderMarkdownResult,
|
|
863
927
|
async execute(_toolCallId, params) {
|
|
864
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
|
+
|
|
865
935
|
const settings = await loadSettings();
|
|
866
936
|
const rateLimitDebug = settings.rateLimitDebug || false;
|
|
867
937
|
const client = await createAuthenticatedClient();
|
|
@@ -941,6 +1011,12 @@ async function registerLinearTools(pi) {
|
|
|
941
1011
|
renderResult: renderMarkdownResult,
|
|
942
1012
|
async execute(_toolCallId, params) {
|
|
943
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
|
+
|
|
944
1020
|
const settings = await loadSettings();
|
|
945
1021
|
const rateLimitDebug = settings.rateLimitDebug || false;
|
|
946
1022
|
const client = await createAuthenticatedClient();
|
|
@@ -1083,7 +1159,7 @@ export default async function piLinearToolsExtension(pi) {
|
|
|
1083
1159
|
// Safety wrapper: never let extension errors crash pi
|
|
1084
1160
|
try {
|
|
1085
1161
|
pi.registerCommand('linear-tools-config', {
|
|
1086
|
-
description: 'Configure pi-linear-tools settings (API key, default team, rate limit debug)',
|
|
1162
|
+
description: 'Configure pi-linear-tools settings (API key, default team, rate limit debug, file overwrite guard)',
|
|
1087
1163
|
handler: async (argsText, ctx) => {
|
|
1088
1164
|
const args = parseArgs(argsText);
|
|
1089
1165
|
const apiKey = readFlag(args, '--api-key');
|
|
@@ -1091,6 +1167,7 @@ export default async function piLinearToolsExtension(pi) {
|
|
|
1091
1167
|
const projectTeam = readFlag(args, '--team');
|
|
1092
1168
|
const projectName = readFlag(args, '--project');
|
|
1093
1169
|
const rateLimitDebug = readFlag(args, '--rate-limit-debug');
|
|
1170
|
+
const allowOverwriteFiles = readFlag(args, '--allow-overwrite-files');
|
|
1094
1171
|
|
|
1095
1172
|
if (apiKey) {
|
|
1096
1173
|
const settings = await loadSettings();
|
|
@@ -1122,6 +1199,17 @@ export default async function piLinearToolsExtension(pi) {
|
|
|
1122
1199
|
return;
|
|
1123
1200
|
}
|
|
1124
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
|
+
|
|
1125
1213
|
if (defaultTeam) {
|
|
1126
1214
|
const settings = await loadSettings();
|
|
1127
1215
|
settings.defaultTeam = defaultTeam;
|
|
@@ -1165,13 +1253,13 @@ export default async function piLinearToolsExtension(pi) {
|
|
|
1165
1253
|
return;
|
|
1166
1254
|
}
|
|
1167
1255
|
|
|
1168
|
-
if (!apiKey && !defaultTeam && !projectTeam && !projectName && !rateLimitDebug && ctx?.hasUI && ctx?.ui) {
|
|
1256
|
+
if (!apiKey && !defaultTeam && !projectTeam && !projectName && !rateLimitDebug && !allowOverwriteFiles && ctx?.hasUI && ctx?.ui) {
|
|
1169
1257
|
await runInteractiveConfigFlow(ctx, pi);
|
|
1170
1258
|
return;
|
|
1171
1259
|
}
|
|
1172
1260
|
|
|
1173
1261
|
// Show current settings if no valid action was specified
|
|
1174
|
-
if (apiKey || defaultTeam || projectTeam || projectName || rateLimitDebug) {
|
|
1262
|
+
if (apiKey || defaultTeam || projectTeam || projectName || rateLimitDebug || allowOverwriteFiles) {
|
|
1175
1263
|
// Actions above already handled and returned
|
|
1176
1264
|
return;
|
|
1177
1265
|
}
|
|
@@ -1182,7 +1270,7 @@ export default async function piLinearToolsExtension(pi) {
|
|
|
1182
1270
|
|
|
1183
1271
|
pi.sendMessage({
|
|
1184
1272
|
customType: 'pi-linear-tools',
|
|
1185
|
-
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 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\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.`,
|
|
1186
1274
|
display: true,
|
|
1187
1275
|
});
|
|
1188
1276
|
},
|
|
@@ -1208,7 +1296,7 @@ export default async function piLinearToolsExtension(pi) {
|
|
|
1208
1296
|
const showMilestoneTool = await shouldExposeMilestoneTool();
|
|
1209
1297
|
const toolLines = [
|
|
1210
1298
|
'LLM-callable tools:',
|
|
1211
|
-
' linear_issue (list/view/activity/create/update/comment/start/delete)',
|
|
1299
|
+
' linear_issue (list/view/images/download/activity/create/update/comment/start/delete)',
|
|
1212
1300
|
' linear_project (list/view/create/update/delete/archive/unarchive)',
|
|
1213
1301
|
' linear_project_update (list/view/create/update/archive/unarchive)',
|
|
1214
1302
|
' linear_team (list)',
|
|
@@ -1228,6 +1316,7 @@ export default async function piLinearToolsExtension(pi) {
|
|
|
1228
1316
|
' /linear-tools-config --default-team <team-key>',
|
|
1229
1317
|
' /linear-tools-config --team <team-key> --project <project-name-or-id>',
|
|
1230
1318
|
' /linear-tools-config --rate-limit-debug true|false',
|
|
1319
|
+
' /linear-tools-config --allow-overwrite-files true|false',
|
|
1231
1320
|
' /linear-tools-help',
|
|
1232
1321
|
' /linear-tools-reload',
|
|
1233
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": {
|
package/src/cli.js
CHANGED
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
import {
|
|
19
19
|
executeIssueList,
|
|
20
20
|
executeIssueView,
|
|
21
|
+
executeIssueImages,
|
|
22
|
+
executeIssueDownload,
|
|
21
23
|
executeIssueActivity,
|
|
22
24
|
executeIssueCreate,
|
|
23
25
|
executeIssueUpdate,
|
|
@@ -164,6 +166,8 @@ Other commands:
|
|
|
164
166
|
config Show current configuration
|
|
165
167
|
config --api-key <key> Set Linear API key (legacy)
|
|
166
168
|
config --default-team <key> Set default team
|
|
169
|
+
config --allow-overwrite-files true|false
|
|
170
|
+
Allow issue download overwrites when requested
|
|
167
171
|
|
|
168
172
|
Auth Actions:
|
|
169
173
|
login Authenticate with Linear via OAuth 2.0
|
|
@@ -173,9 +177,12 @@ Auth Actions:
|
|
|
173
177
|
Issue Actions:
|
|
174
178
|
list [--project X] [--states X,Y] [--assignee me|all] [--team X] [--limit N]
|
|
175
179
|
view <issue> [--no-comments]
|
|
180
|
+
images <issue> [--no-comments] [--limit N] [--max-bytes N]
|
|
181
|
+
download <issue> --directory DIR [--attachment-id ID|--attachment-title TITLE|--attachment-url URL|--attachment-index N]
|
|
182
|
+
[--filename NAME] [--overwrite true|false] [--max-bytes N]
|
|
176
183
|
activity <issue> [--limit N] [--include-archived true|false]
|
|
177
|
-
create --title X [--team X] [--project X] [--description X] [--priority 0-4] [--assignee me|ID]
|
|
178
|
-
update <issue> [--title X] [--description X] [--state X] [--priority 0-4]
|
|
184
|
+
create --title X [--team X] [--project X] [--description X] [--priority 0-4|name] [--assignee me|ID]
|
|
185
|
+
update <issue> [--title X] [--description X] [--state X] [--priority 0-4|name]
|
|
179
186
|
[--assignee me|ID] [--milestone X] [--sub-issue-of X]
|
|
180
187
|
comment <issue> --body X
|
|
181
188
|
start <issue> [--from-ref X] [--on-branch-exists switch|suffix]
|
|
@@ -242,7 +249,7 @@ Common Flags:
|
|
|
242
249
|
--project Project name or ID
|
|
243
250
|
--team Team key (e.g., ENG)
|
|
244
251
|
--assignee "me" or assignee ID
|
|
245
|
-
--priority
|
|
252
|
+
--priority Issue priority: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low; or none, urgent, high, medium, low
|
|
246
253
|
--state State name or ID
|
|
247
254
|
--limit Max results (default: 50)
|
|
248
255
|
|
|
@@ -286,11 +293,14 @@ Guidance:
|
|
|
286
293
|
Use "update" to change the issue itself: title, description, state, assignee, milestone, or parent.
|
|
287
294
|
Use "comment" to add a discussion entry.
|
|
288
295
|
Use "activity" to read the Activity timeline shown in Linear.
|
|
296
|
+
Use "images" to fetch image attachments embedded in issue markdown/comments.
|
|
289
297
|
|
|
290
298
|
Actions:
|
|
291
299
|
list List issues in a project
|
|
292
300
|
view View issue details
|
|
301
|
+
images Fetch image attachments embedded in issue markdown/comments
|
|
293
302
|
activity View issue activity/history
|
|
303
|
+
download Download a Linear issue attachment
|
|
294
304
|
create Create a new issue
|
|
295
305
|
update Update an existing issue
|
|
296
306
|
comment Add a comment to an issue
|
|
@@ -308,6 +318,23 @@ View Options:
|
|
|
308
318
|
<issue> Issue key (e.g., ENG-123), ID, or issue URL
|
|
309
319
|
--no-comments Exclude comments from output
|
|
310
320
|
|
|
321
|
+
Images Options:
|
|
322
|
+
<issue> Issue key (e.g., ENG-123), ID, or issue URL
|
|
323
|
+
--no-comments Exclude images from comments
|
|
324
|
+
--limit N Max images to fetch (default: 10)
|
|
325
|
+
--max-bytes N Max bytes per image (default/max: 10485760)
|
|
326
|
+
|
|
327
|
+
Download Options:
|
|
328
|
+
<issue> Issue key, ID, or issue URL
|
|
329
|
+
--directory DIR Relative destination directory (required)
|
|
330
|
+
--attachment-id ID Attachment ID selector
|
|
331
|
+
--attachment-title X Attachment title selector (must be unique)
|
|
332
|
+
--attachment-url URL Attachment URL selector
|
|
333
|
+
--attachment-index N 1-based attachment index selector
|
|
334
|
+
--filename NAME Optional output filename
|
|
335
|
+
--overwrite X true or false (default: false; requires config guard)
|
|
336
|
+
--max-bytes N Max download bytes (default/max: 52428800)
|
|
337
|
+
|
|
311
338
|
Activity Options:
|
|
312
339
|
<issue> Issue key, ID, or issue URL
|
|
313
340
|
--limit N Max activity entries to fetch (default: 20)
|
|
@@ -318,7 +345,7 @@ Create Options:
|
|
|
318
345
|
--team X Team key, e.g., ENG (required if no default team)
|
|
319
346
|
--project X Project name or ID
|
|
320
347
|
--description X Issue description (markdown)
|
|
321
|
-
--priority N
|
|
348
|
+
--priority N Issue priority: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low; or none, urgent, high, medium, low
|
|
322
349
|
--assignee X "me" or assignee ID
|
|
323
350
|
--parent-id X Parent issue ID for sub-issues
|
|
324
351
|
|
|
@@ -327,7 +354,7 @@ Update Options:
|
|
|
327
354
|
--title X New title
|
|
328
355
|
--description X New description
|
|
329
356
|
--state X New state name or ID
|
|
330
|
-
--priority N New priority 0
|
|
357
|
+
--priority N New issue priority: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low; or none, urgent, high, medium, low
|
|
331
358
|
--assignee X "me" or assignee ID
|
|
332
359
|
--milestone X Milestone name/ID, or "none" to clear
|
|
333
360
|
--sub-issue-of X Parent issue key/ID, or "none" to clear
|
|
@@ -351,6 +378,7 @@ Examples:
|
|
|
351
378
|
pi-linear-tools issue comment ENG-123 --body "Ready for review"
|
|
352
379
|
pi-linear-tools issue activity ENG-123 --limit 20
|
|
353
380
|
pi-linear-tools issue activity https://linear.app/workspace/issue/ENG-123/example --limit 20
|
|
381
|
+
pi-linear-tools issue download ENG-123 --attachment-index 1 --directory downloads
|
|
354
382
|
`);
|
|
355
383
|
}
|
|
356
384
|
|
|
@@ -729,6 +757,7 @@ async function handleConfig(args) {
|
|
|
729
757
|
const defaultTeam = readFlag(args, '--default-team');
|
|
730
758
|
const projectTeam = readFlag(args, '--team');
|
|
731
759
|
const projectName = readFlag(args, '--project');
|
|
760
|
+
const allowOverwriteFiles = readFlag(args, '--allow-overwrite-files');
|
|
732
761
|
|
|
733
762
|
if (apiKey) {
|
|
734
763
|
const settings = await loadSettings();
|
|
@@ -748,6 +777,19 @@ async function handleConfig(args) {
|
|
|
748
777
|
return;
|
|
749
778
|
}
|
|
750
779
|
|
|
780
|
+
if (allowOverwriteFiles !== undefined) {
|
|
781
|
+
const enabled = parseBoolean(allowOverwriteFiles);
|
|
782
|
+
if (enabled === undefined) {
|
|
783
|
+
throw new Error('Invalid value for --allow-overwrite-files. Use true or false.');
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const settings = await loadSettings();
|
|
787
|
+
settings.allow_overwrite_files = enabled;
|
|
788
|
+
await saveSettings(settings);
|
|
789
|
+
console.log(`File overwrite guard ${enabled ? 'allows overwrites' : 'blocks overwrites'}`);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
|
|
751
793
|
if (projectTeam) {
|
|
752
794
|
if (!projectName) {
|
|
753
795
|
throw new Error('Missing required flag: --project when using --team');
|
|
@@ -777,12 +819,14 @@ async function handleConfig(args) {
|
|
|
777
819
|
console.log(`Configuration:
|
|
778
820
|
LINEAR_API_KEY: ${hasKey ? 'configured' : 'not set'} (source: ${keySource})
|
|
779
821
|
Default team: ${settings.defaultTeam || 'not set'}
|
|
822
|
+
Allow overwrite files: ${settings.allow_overwrite_files ? 'enabled' : 'disabled'}
|
|
780
823
|
Project team mappings: ${Object.keys(settings.projects || {}).length}
|
|
781
824
|
|
|
782
825
|
Commands:
|
|
783
826
|
pi-linear-tools config --api-key lin_xxx
|
|
784
827
|
pi-linear-tools config --default-team ENG
|
|
785
|
-
pi-linear-tools config --team ENG --project MyProject
|
|
828
|
+
pi-linear-tools config --team ENG --project MyProject
|
|
829
|
+
pi-linear-tools config --allow-overwrite-files true|false`);
|
|
786
830
|
}
|
|
787
831
|
|
|
788
832
|
// ===== ISSUE HANDLERS =====
|
|
@@ -819,6 +863,25 @@ async function handleIssueView(args) {
|
|
|
819
863
|
console.log(result.content[0].text);
|
|
820
864
|
}
|
|
821
865
|
|
|
866
|
+
async function handleIssueImages(args) {
|
|
867
|
+
const client = await createAuthenticatedClient();
|
|
868
|
+
|
|
869
|
+
const positional = args.filter((a) => !a.startsWith('-'));
|
|
870
|
+
if (positional.length === 0) {
|
|
871
|
+
throw new Error('Missing required argument: issue key or ID');
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const params = {
|
|
875
|
+
issue: positional[0],
|
|
876
|
+
includeComments: !hasFlag(args, '--no-comments'),
|
|
877
|
+
limit: parseNumber(readFlag(args, '--limit')),
|
|
878
|
+
maxBytes: parseNumber(readFlag(args, '--max-bytes')),
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
const result = await executeIssueImages(client, params);
|
|
882
|
+
console.log(result.content[0].text);
|
|
883
|
+
}
|
|
884
|
+
|
|
822
885
|
async function handleIssueActivity(args) {
|
|
823
886
|
const client = await createAuthenticatedClient();
|
|
824
887
|
|
|
@@ -837,6 +900,32 @@ async function handleIssueActivity(args) {
|
|
|
837
900
|
console.log(result.content[0].text);
|
|
838
901
|
}
|
|
839
902
|
|
|
903
|
+
async function handleIssueDownload(args) {
|
|
904
|
+
const client = await createAuthenticatedClient();
|
|
905
|
+
const settings = await loadSettings();
|
|
906
|
+
|
|
907
|
+
const positional = args.filter((a) => !a.startsWith('-'));
|
|
908
|
+
if (positional.length === 0) {
|
|
909
|
+
throw new Error('Missing required argument: issue key or ID');
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const overwrite = parseBoolean(readFlag(args, '--overwrite'));
|
|
913
|
+
const params = {
|
|
914
|
+
issue: positional[0],
|
|
915
|
+
attachmentId: readFlag(args, '--attachment-id'),
|
|
916
|
+
attachmentTitle: readFlag(args, '--attachment-title'),
|
|
917
|
+
attachmentUrl: readFlag(args, '--attachment-url'),
|
|
918
|
+
attachmentIndex: parseNumber(readFlag(args, '--attachment-index')),
|
|
919
|
+
directory: readFlag(args, '--directory'),
|
|
920
|
+
filename: readFlag(args, '--filename'),
|
|
921
|
+
overwrite: overwrite === undefined ? undefined : overwrite,
|
|
922
|
+
maxBytes: parseNumber(readFlag(args, '--max-bytes')),
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
const result = await executeIssueDownload(client, params, { settings });
|
|
926
|
+
console.log(result.content[0].text);
|
|
927
|
+
}
|
|
928
|
+
|
|
840
929
|
async function handleIssueCreate(args) {
|
|
841
930
|
const client = await createAuthenticatedClient();
|
|
842
931
|
|
|
@@ -845,7 +934,7 @@ async function handleIssueCreate(args) {
|
|
|
845
934
|
team: readFlag(args, '--team'),
|
|
846
935
|
project: readFlag(args, '--project'),
|
|
847
936
|
description: readFlag(args, '--description'),
|
|
848
|
-
priority:
|
|
937
|
+
priority: readFlag(args, '--priority'),
|
|
849
938
|
assignee: readFlag(args, '--assignee'),
|
|
850
939
|
parentId: readFlag(args, '--parent-id'),
|
|
851
940
|
state: readFlag(args, '--state'),
|
|
@@ -872,7 +961,7 @@ async function handleIssueUpdate(args) {
|
|
|
872
961
|
title: readFlag(args, '--title'),
|
|
873
962
|
description: readFlag(args, '--description'),
|
|
874
963
|
state: readFlag(args, '--state'),
|
|
875
|
-
priority:
|
|
964
|
+
priority: readFlag(args, '--priority'),
|
|
876
965
|
assignee: readFlag(args, '--assignee'),
|
|
877
966
|
milestone: readFlag(args, '--milestone'),
|
|
878
967
|
subIssueOf: readFlag(args, '--sub-issue-of'),
|
|
@@ -950,6 +1039,10 @@ async function handleIssue(args) {
|
|
|
950
1039
|
return handleIssueList(rest);
|
|
951
1040
|
case 'view':
|
|
952
1041
|
return handleIssueView(rest);
|
|
1042
|
+
case 'images':
|
|
1043
|
+
return handleIssueImages(rest);
|
|
1044
|
+
case 'download':
|
|
1045
|
+
return handleIssueDownload(rest);
|
|
953
1046
|
case 'activity':
|
|
954
1047
|
return handleIssueActivity(rest);
|
|
955
1048
|
case 'create':
|