@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 +19 -0
- package/extensions/pi-linear-tools.js +165 -49
- package/index.js +916 -6
- package/package.json +3 -3
- package/src/auth/token-store.js +5 -5
- package/src/cli.js +11 -11
- package/src/handlers.js +56 -50
- package/src/linear-client.js +133 -2
- package/src/linear.js +1071 -641
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')
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
740
|
+
try {
|
|
741
|
+
const client = await createAuthenticatedClient();
|
|
681
742
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
|
|
788
|
+
try {
|
|
789
|
+
const client = await createAuthenticatedClient();
|
|
710
790
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
-
|
|
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
|
}
|