@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 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
- `pi-linear-tools` is a 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 tools and CLI commands.
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
- Useful mental model:
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 (!INCLUDE_USAGE_SUMMARY || !result || typeof result !== 'object') {
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: `${item.text}\n\n_${usageDelta.summary}_`,
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
- cached
471
- ? `Linear API rate limit exceeded (cached).\n\nThe rate limit resets at: ${resetTime}\n\nPlease wait before making more requests.`
472
- : `Linear API rate limit exceeded.\n\nThe rate limit resets at: ${resetTime}\n\nPlease wait before making more requests, or reduce the frequency of API calls.`,
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(error);
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 or activity entries to list',
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
- type: 'integer',
622
- description: 'Priority 0..4 (for create, update)',
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 and default team mappings)',
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.5.1",
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
  },
@@ -1,7 +1,11 @@
1
1
  {
2
- "schemaVersion": 1,
3
- "linearApiKey": "lin_xxx",
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": {