@fink-andreas/pi-linear-tools 0.2.0 → 0.3.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,24 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.3.0 (2026-03-22)
4
+
5
+ Rate limit optimization and crash prevention release.
6
+
7
+ ### Bug Fixes
8
+ - Fixed rate limit crashes by eliminating N+1 API queries (root cause)
9
+ - Added global rate limit tracking to prevent repeated API calls
10
+ - Added comprehensive error handling to prevent extension crashes
11
+
12
+ ### Performance
13
+ - Replaced SDK lazy-loading with optimized GraphQL queries
14
+ - API requests per issue listing: ~251 → 1
15
+ - Listings before hitting rate limit: ~50 → ~5000
16
+
17
+ ### Improvements
18
+ - Reduced default pagination limit (50 → 20) to reduce API load
19
+ - Better user-friendly error messages for rate limit errors
20
+ - Rate limit pre-check before making API calls
21
+
3
22
  ## v0.2.0 (2026-02-28)
4
23
 
5
24
  OAuth 2.0 authentication and Markdown rendering release.
@@ -1,5 +1,5 @@
1
1
  import { loadSettings, saveSettings } from '../src/settings.js';
2
- import { createLinearClient } from '../src/linear-client.js';
2
+ import { createLinearClient, isGloballyRateLimited, markRateLimited } from '../src/linear-client.js';
3
3
  import { setQuietMode } from '../src/logger.js';
4
4
  import {
5
5
  resolveProjectRef,
@@ -502,7 +502,10 @@ function renderMarkdownResult(result, _options, _theme) {
502
502
  }
503
503
 
504
504
  async function registerLinearTools(pi) {
505
- if (typeof pi.registerTool !== 'function') return;
505
+ if (typeof pi.registerTool !== 'function') {
506
+ console.warn('[pi-linear-tools] pi.registerTool not available');
507
+ return;
508
+ }
506
509
 
507
510
  pi.registerTool({
508
511
  name: 'linear_issue',
@@ -613,10 +616,6 @@ async function registerLinearTools(pi) {
613
616
  type: 'string',
614
617
  description: 'Parent comment ID for reply (for comment)',
615
618
  },
616
- branch: {
617
- type: 'string',
618
- description: 'Custom branch name override (for start)',
619
- },
620
619
  fromRef: {
621
620
  type: 'string',
622
621
  description: 'Git ref to branch from (default: HEAD, for start)',
@@ -632,29 +631,90 @@ async function registerLinearTools(pi) {
632
631
  },
633
632
  renderResult: renderMarkdownResult,
634
633
  async execute(_toolCallId, params) {
635
- const client = await createAuthenticatedClient();
636
-
637
- switch (params.action) {
638
- case 'list':
639
- return executeIssueList(client, params);
640
- case 'view':
641
- return executeIssueView(client, params);
642
- case 'create':
643
- return executeIssueCreate(client, params, { resolveDefaultTeam });
644
- case 'update':
645
- return executeIssueUpdate(client, params);
646
- case 'comment':
647
- return executeIssueComment(client, params);
648
- case 'start':
649
- return executeIssueStart(client, params, {
650
- gitExecutor: async (branchName, fromRef, onBranchExists) => {
651
- return startGitBranchForIssue(pi, branchName, fromRef, onBranchExists);
652
- },
653
- });
654
- case 'delete':
655
- return executeIssueDelete(client, params);
656
- default:
657
- throw new Error(`Unknown action: ${params.action}`);
634
+ // Pre-check: skip API calls if we know we're rate limited
635
+ const { isRateLimited, resetAt } = isGloballyRateLimited();
636
+ if (isRateLimited) {
637
+ throw new Error(
638
+ `Linear API rate limit exceeded (cached).\n\n` +
639
+ `The rate limit resets at: ${resetAt.toLocaleTimeString()}\n\n` +
640
+ `Please wait before making more requests.`
641
+ );
642
+ }
643
+
644
+ try {
645
+ const client = await createAuthenticatedClient();
646
+
647
+ switch (params.action) {
648
+ case 'list':
649
+ return await executeIssueList(client, params);
650
+ case 'view':
651
+ return await executeIssueView(client, params);
652
+ case 'create':
653
+ return await executeIssueCreate(client, params, { resolveDefaultTeam });
654
+ case 'update':
655
+ return await executeIssueUpdate(client, params);
656
+ case 'comment':
657
+ return await executeIssueComment(client, params);
658
+ case 'start':
659
+ return await executeIssueStart(client, params, {
660
+ gitExecutor: async (branchName, fromRef, onBranchExists) => {
661
+ return startGitBranchForIssue(pi, branchName, fromRef, onBranchExists);
662
+ },
663
+ });
664
+ case 'delete':
665
+ return await executeIssueDelete(client, params);
666
+ default:
667
+ throw new Error(`Unknown action: ${params.action}`);
668
+ }
669
+ } catch (error) {
670
+ // Comprehensive error handling - catch ALL errors including SDK's RatelimitedLinearError
671
+ const errorType = error?.type || '';
672
+ const errorMessage = String(error?.message || error || 'Unknown error');
673
+
674
+ // Rate limit error - provide clear reset time and mark globally
675
+ if (errorType === 'Ratelimited' || errorMessage.toLowerCase().includes('rate limit')) {
676
+ const resetTimestamp = error?.requestsResetAt || (Date.now() + 3600000);
677
+ const resetTime = new Date(resetTimestamp).toLocaleTimeString();
678
+ markRateLimited(resetTimestamp);
679
+ throw new Error(
680
+ `Linear API rate limit exceeded.\n\n` +
681
+ `The rate limit resets at: ${resetTime}\n\n` +
682
+ `Please wait before making more requests, or reduce the frequency of API calls.`
683
+ );
684
+ }
685
+
686
+ // Authentication/Forbidden errors (handles SDK's ForbiddenLinearError)
687
+ if (errorType === 'Forbidden' || errorType === 'AuthenticationError' ||
688
+ errorMessage.toLowerCase().includes('forbidden') || errorMessage.toLowerCase().includes('unauthorized')) {
689
+ throw new Error(
690
+ `Linear API authentication failed: ${errorMessage}\n\n` +
691
+ `Please check your API key or OAuth token permissions.`
692
+ );
693
+ }
694
+
695
+ // Network errors (handles SDK's NetworkError)
696
+ if (errorType === 'NetworkError' || errorMessage.toLowerCase().includes('network')) {
697
+ throw new Error(
698
+ `Network error communicating with Linear API.\n\n` +
699
+ `Please check your internet connection and try again.`
700
+ );
701
+ }
702
+
703
+ // Internal server errors (handles SDK's InternalError)
704
+ if (errorType === 'InternalError' || (error?.status >= 500 && error?.status < 600)) {
705
+ throw new Error(
706
+ `Linear API server error (${error?.status || 'unknown'}).\n\n` +
707
+ `Linear may be experiencing issues. Please try again later.`
708
+ );
709
+ }
710
+
711
+ // Re-throw if already formatted with "Linear API error:"
712
+ if (errorMessage.includes('Linear API error:')) {
713
+ throw error;
714
+ }
715
+
716
+ // Wrap unexpected errors with context - NEVER let raw errors propagate
717
+ throw new Error(`Linear issue operation failed: ${errorMessage}`);
658
718
  }
659
719
  },
660
720
  });
@@ -677,13 +737,32 @@ async function registerLinearTools(pi) {
677
737
  },
678
738
  renderResult: renderMarkdownResult,
679
739
  async execute(_toolCallId, params) {
680
- const client = await createAuthenticatedClient();
740
+ try {
741
+ const client = await createAuthenticatedClient();
681
742
 
682
- switch (params.action) {
683
- case 'list':
684
- return executeProjectList(client);
685
- default:
686
- throw new Error(`Unknown action: ${params.action}`);
743
+ switch (params.action) {
744
+ case 'list':
745
+ return await executeProjectList(client);
746
+ default:
747
+ throw new Error(`Unknown action: ${params.action}`);
748
+ }
749
+ } catch (error) {
750
+ // Comprehensive error handling - catch ALL errors
751
+ const errorType = error?.type || '';
752
+ const errorMessage = String(error?.message || error || 'Unknown error');
753
+
754
+ if (errorType === 'Ratelimited' || errorMessage.toLowerCase().includes('rate limit')) {
755
+ const resetAt = error?.requestsResetAt
756
+ ? new Date(error.requestsResetAt).toLocaleTimeString()
757
+ : 'approximately 1 hour from now';
758
+ throw new Error(`Linear API rate limit exceeded. Resets at: ${resetAt}. Please wait before retrying.`);
759
+ }
760
+
761
+ if (errorMessage.includes('Linear API error:')) {
762
+ throw error;
763
+ }
764
+
765
+ throw new Error(`Linear project operation failed: ${errorMessage}`);
687
766
  }
688
767
  },
689
768
  });
@@ -706,13 +785,32 @@ async function registerLinearTools(pi) {
706
785
  },
707
786
  renderResult: renderMarkdownResult,
708
787
  async execute(_toolCallId, params) {
709
- const client = await createAuthenticatedClient();
788
+ try {
789
+ const client = await createAuthenticatedClient();
710
790
 
711
- switch (params.action) {
712
- case 'list':
713
- return executeTeamList(client);
714
- default:
715
- throw new Error(`Unknown action: ${params.action}`);
791
+ switch (params.action) {
792
+ case 'list':
793
+ return await executeTeamList(client);
794
+ default:
795
+ throw new Error(`Unknown action: ${params.action}`);
796
+ }
797
+ } catch (error) {
798
+ // Comprehensive error handling - catch ALL errors
799
+ const errorType = error?.type || '';
800
+ const errorMessage = String(error?.message || error || 'Unknown error');
801
+
802
+ if (errorType === 'Ratelimited' || errorMessage.toLowerCase().includes('rate limit')) {
803
+ const resetAt = error?.requestsResetAt
804
+ ? new Date(error.requestsResetAt).toLocaleTimeString()
805
+ : 'approximately 1 hour from now';
806
+ throw new Error(`Linear API rate limit exceeded. Resets at: ${resetAt}. Please wait before retrying.`);
807
+ }
808
+
809
+ if (errorMessage.includes('Linear API error:')) {
810
+ throw error;
811
+ }
812
+
813
+ throw new Error(`Linear team operation failed: ${errorMessage}`);
716
814
  }
717
815
  },
718
816
  });
@@ -750,20 +848,15 @@ async function registerLinearTools(pi) {
750
848
  type: 'string',
751
849
  description: 'Target completion date (ISO 8601 date)',
752
850
  },
753
- status: {
754
- type: 'string',
755
- enum: ['backlogged', 'planned', 'inProgress', 'paused', 'completed', 'done', 'cancelled'],
756
- description: 'Milestone status',
757
- },
758
851
  },
759
852
  required: ['action'],
760
853
  additionalProperties: false,
761
854
  },
762
855
  renderResult: renderMarkdownResult,
763
856
  async execute(_toolCallId, params) {
764
- const client = await createAuthenticatedClient();
765
-
766
857
  try {
858
+ const client = await createAuthenticatedClient();
859
+
767
860
  switch (params.action) {
768
861
  case 'list':
769
862
  return await executeMilestoneList(client, params);
@@ -779,7 +872,24 @@ async function registerLinearTools(pi) {
779
872
  throw new Error(`Unknown action: ${params.action}`);
780
873
  }
781
874
  } catch (error) {
782
- throw withMilestoneScopeHint(error);
875
+ // Apply milestone-specific hint, then wrap with operation context
876
+ const hintError = withMilestoneScopeHint(error);
877
+ // Comprehensive error handling - catch ALL errors
878
+ const errorType = error?.type || '';
879
+ const errorMessage = String(hintError?.message || hintError || 'Unknown error');
880
+
881
+ if (errorType === 'Ratelimited' || errorMessage.toLowerCase().includes('rate limit')) {
882
+ const resetAt = error?.requestsResetAt
883
+ ? new Date(error.requestsResetAt).toLocaleTimeString()
884
+ : 'approximately 1 hour from now';
885
+ throw new Error(`Linear API rate limit exceeded. Resets at: ${resetAt}. Please wait before retrying.`);
886
+ }
887
+
888
+ if (errorMessage.includes('Linear API error:')) {
889
+ throw hintError;
890
+ }
891
+
892
+ throw new Error(`Linear milestone operation failed: ${errorMessage}`);
783
893
  }
784
894
  },
785
895
  });
@@ -787,6 +897,8 @@ async function registerLinearTools(pi) {
787
897
  }
788
898
 
789
899
  export default async function piLinearToolsExtension(pi) {
900
+ // Safety wrapper: never let extension errors crash pi
901
+ try {
790
902
  pi.registerCommand('linear-tools-config', {
791
903
  description: 'Configure pi-linear-tools settings (API key and default team mappings)',
792
904
  handler: async (argsText, ctx) => {
@@ -924,4 +1036,8 @@ export default async function piLinearToolsExtension(pi) {
924
1036
  });
925
1037
 
926
1038
  await registerLinearTools(pi);
1039
+ } catch (error) {
1040
+ // Safety: never let extension initialization crash pi
1041
+ console.error('[pi-linear-tools] Extension initialization failed:', error?.message || error);
1042
+ }
927
1043
  }