@fink-andreas/pi-linear-tools 0.4.3 → 0.5.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 +27 -10
- package/extensions/pi-linear-tools.js +67 -99
- package/package.json +1 -1
- package/src/handlers.js +63 -3
- package/src/linear.js +985 -184
package/CHANGELOG.md
CHANGED
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## v0.
|
|
3
|
+
## v0.5.0 (2026-04-17)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Linear document sync, project lifecycle management, and estimate/story points support.
|
|
6
|
+
|
|
7
|
+
### New Features
|
|
8
|
+
- **Linear document sync**: Keep Linear documents in sync with local Markdown files via `linear_doc_sync` command
|
|
9
|
+
- **Project lifecycle**: Full project lifecycle support including archive/unarchive, updates, and milestones
|
|
10
|
+
- **Estimate (story points)**: Support for estimate/story points field on issue create/update
|
|
11
|
+
- **Issue activity history**: View complete issue activity with `linear_issue_activity` tool
|
|
12
|
+
- **Batch sync**: `sync-doc --all` for batch syncing all configured document targets
|
|
6
13
|
|
|
7
14
|
### Bug Fixes
|
|
8
|
-
- Fixed
|
|
9
|
-
-
|
|
10
|
-
-
|
|
15
|
+
- Fixed project name lookup for UUID-like project names
|
|
16
|
+
- Fixed milestone name lookup for view/update/delete operations
|
|
17
|
+
- Replaced 'order' with 'sortOrder' for ProjectMilestone queries
|
|
18
|
+
- Fixed sync-doc first-run check/run parity
|
|
19
|
+
- Fixed sync-doc drift detection and target matching
|
|
20
|
+
- Fixed project archive and update validation
|
|
21
|
+
- Fixed archived project resolution for project updates
|
|
22
|
+
- Fixed sync-doc marker position preservation when position is 'top'
|
|
23
|
+
- Fixed default limit for executeIssueActivity (now 25)
|
|
11
24
|
|
|
12
25
|
### Improvements
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
-
|
|
26
|
+
- Reduced expensive Linear API issue reads
|
|
27
|
+
- Better CLI help for issue and project workflows
|
|
28
|
+
- Improved sync-doc examples and guidance
|
|
29
|
+
- Added promptSnippet to all Linear tools for consistent LLM context
|
|
30
|
+
- Removed redundant action lists from tool descriptions
|
|
31
|
+
|
|
32
|
+
### Cleanup
|
|
33
|
+
- Removed planning artifacts and documentation files
|
|
34
|
+
- Stopped tracking plan files in git
|
|
18
35
|
|
|
19
36
|
## v0.4.2 (2026-03-26)
|
|
20
37
|
|
|
@@ -452,6 +452,51 @@ async function shouldExposeMilestoneTool() {
|
|
|
452
452
|
/**
|
|
453
453
|
* Render tool result as markdown
|
|
454
454
|
*/
|
|
455
|
+
function toToolTextResult(text, details = {}) {
|
|
456
|
+
return {
|
|
457
|
+
content: [{ type: 'text', text }],
|
|
458
|
+
details,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function buildRateLimitToolResult(error, options = {}) {
|
|
463
|
+
const { cached = false } = options;
|
|
464
|
+
const resetTimestamp = error?.requestsResetAt || Date.now() + 3600000;
|
|
465
|
+
const resetTime = new Date(resetTimestamp).toLocaleTimeString();
|
|
466
|
+
|
|
467
|
+
markRateLimited(resetTimestamp);
|
|
468
|
+
|
|
469
|
+
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.`,
|
|
473
|
+
{
|
|
474
|
+
error: true,
|
|
475
|
+
errorType: 'Ratelimited',
|
|
476
|
+
rateLimited: true,
|
|
477
|
+
cached,
|
|
478
|
+
requestsResetAt: resetTimestamp,
|
|
479
|
+
resetTime,
|
|
480
|
+
}
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function handleToolExecutionError(error, operationLabel, options = {}) {
|
|
485
|
+
const transformedError = options.transformError ? options.transformError(error) : error;
|
|
486
|
+
const errorType = error?.type || transformedError?.type || '';
|
|
487
|
+
const errorMessage = String(transformedError?.message || transformedError || 'Unknown error');
|
|
488
|
+
|
|
489
|
+
if (errorType === 'Ratelimited' || errorMessage.toLowerCase().includes('rate limit')) {
|
|
490
|
+
return buildRateLimitToolResult(error);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (errorMessage.includes('Linear API error:')) {
|
|
494
|
+
throw transformedError;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
throw new Error(`${operationLabel}: ${errorMessage}`);
|
|
498
|
+
}
|
|
499
|
+
|
|
455
500
|
function renderMarkdownResult(result, _options, _theme) {
|
|
456
501
|
const text = result.content?.[0]?.text || '';
|
|
457
502
|
|
|
@@ -491,7 +536,7 @@ async function registerLinearTools(pi) {
|
|
|
491
536
|
pi.registerTool({
|
|
492
537
|
name: 'linear_issue',
|
|
493
538
|
label: 'Linear Issue',
|
|
494
|
-
description: 'Interact with Linear issues.
|
|
539
|
+
description: 'Interact with Linear issues.',
|
|
495
540
|
promptSnippet: 'Interact with Linear issues (list, view, activity, create, update, comment, start, delete)',
|
|
496
541
|
parameters: {
|
|
497
542
|
type: 'object',
|
|
@@ -586,6 +631,10 @@ async function registerLinearTools(pi) {
|
|
|
586
631
|
type: 'string',
|
|
587
632
|
description: 'For update: mark this issue as duplicate of the given issue key/ID.',
|
|
588
633
|
},
|
|
634
|
+
estimate: {
|
|
635
|
+
type: 'integer',
|
|
636
|
+
description: 'Estimate/story points for the issue (non-negative integer, for create/update)',
|
|
637
|
+
},
|
|
589
638
|
team: {
|
|
590
639
|
type: 'string',
|
|
591
640
|
description: 'Team key (e.g. ENG) or name (optional if default team configured)',
|
|
@@ -617,17 +666,13 @@ async function registerLinearTools(pi) {
|
|
|
617
666
|
},
|
|
618
667
|
renderResult: renderMarkdownResult,
|
|
619
668
|
async execute(_toolCallId, params) {
|
|
620
|
-
// Pre-check: skip API calls if we know we're rate limited
|
|
621
|
-
const { isRateLimited, resetAt } = checkAndClearRateLimit();
|
|
622
|
-
if (isRateLimited) {
|
|
623
|
-
throw new Error(
|
|
624
|
-
`Linear API rate limit exceeded (cached).\n\n` +
|
|
625
|
-
`The rate limit resets at: ${resetAt.toLocaleTimeString()}\n\n` +
|
|
626
|
-
`Please wait before making more requests.`
|
|
627
|
-
);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
669
|
try {
|
|
670
|
+
// Pre-check: skip API calls if we know we're rate limited
|
|
671
|
+
const { isRateLimited, resetAt } = checkAndClearRateLimit();
|
|
672
|
+
if (isRateLimited) {
|
|
673
|
+
return buildRateLimitToolResult({ requestsResetAt: resetAt.getTime(), type: 'Ratelimited' }, { cached: true });
|
|
674
|
+
}
|
|
675
|
+
|
|
631
676
|
const client = await createAuthenticatedClient();
|
|
632
677
|
|
|
633
678
|
return await withRequestUsageLogging(client, 'linear_issue', params.action, async () => {
|
|
@@ -661,18 +706,6 @@ async function registerLinearTools(pi) {
|
|
|
661
706
|
const errorType = error?.type || '';
|
|
662
707
|
const errorMessage = String(error?.message || error || 'Unknown error');
|
|
663
708
|
|
|
664
|
-
// Rate limit error - provide clear reset time and mark globally
|
|
665
|
-
if (errorType === 'Ratelimited' || errorMessage.toLowerCase().includes('rate limit')) {
|
|
666
|
-
const resetTimestamp = error?.requestsResetAt || (Date.now() + 3600000);
|
|
667
|
-
const resetTime = new Date(resetTimestamp).toLocaleTimeString();
|
|
668
|
-
markRateLimited(resetTimestamp);
|
|
669
|
-
throw new Error(
|
|
670
|
-
`Linear API rate limit exceeded.\n\n` +
|
|
671
|
-
`The rate limit resets at: ${resetTime}\n\n` +
|
|
672
|
-
`Please wait before making more requests, or reduce the frequency of API calls.`
|
|
673
|
-
);
|
|
674
|
-
}
|
|
675
|
-
|
|
676
709
|
// Authentication/Forbidden errors (handles SDK's ForbiddenLinearError)
|
|
677
710
|
if (errorType === 'Forbidden' || errorType === 'AuthenticationError' ||
|
|
678
711
|
errorMessage.toLowerCase().includes('forbidden') || errorMessage.toLowerCase().includes('unauthorized')) {
|
|
@@ -698,13 +731,7 @@ async function registerLinearTools(pi) {
|
|
|
698
731
|
);
|
|
699
732
|
}
|
|
700
733
|
|
|
701
|
-
|
|
702
|
-
if (errorMessage.includes('Linear API error:')) {
|
|
703
|
-
throw error;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
// Wrap unexpected errors with context - NEVER let raw errors propagate
|
|
707
|
-
throw new Error(`Linear issue operation failed: ${errorMessage}`);
|
|
734
|
+
return handleToolExecutionError(error, 'Linear issue operation failed');
|
|
708
735
|
}
|
|
709
736
|
},
|
|
710
737
|
});
|
|
@@ -712,7 +739,7 @@ async function registerLinearTools(pi) {
|
|
|
712
739
|
pi.registerTool({
|
|
713
740
|
name: 'linear_project',
|
|
714
741
|
label: 'Linear Project',
|
|
715
|
-
description: 'Interact with Linear projects.
|
|
742
|
+
description: 'Interact with Linear projects.',
|
|
716
743
|
promptSnippet: 'Interact with Linear projects (list, view, create, update, delete, archive, unarchive)',
|
|
717
744
|
parameters: {
|
|
718
745
|
type: 'object',
|
|
@@ -795,22 +822,7 @@ async function registerLinearTools(pi) {
|
|
|
795
822
|
}
|
|
796
823
|
});
|
|
797
824
|
} catch (error) {
|
|
798
|
-
|
|
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 project operation failed: ${errorMessage}`);
|
|
825
|
+
return handleToolExecutionError(error, 'Linear project operation failed');
|
|
814
826
|
}
|
|
815
827
|
},
|
|
816
828
|
});
|
|
@@ -818,7 +830,7 @@ async function registerLinearTools(pi) {
|
|
|
818
830
|
pi.registerTool({
|
|
819
831
|
name: 'linear_project_update',
|
|
820
832
|
label: 'Linear Project Update',
|
|
821
|
-
description: 'Interact with Linear project updates.
|
|
833
|
+
description: 'Interact with Linear project updates.',
|
|
822
834
|
promptSnippet: 'Interact with Linear project updates (list, view, create, update, archive, unarchive)',
|
|
823
835
|
parameters: {
|
|
824
836
|
type: 'object',
|
|
@@ -887,21 +899,7 @@ async function registerLinearTools(pi) {
|
|
|
887
899
|
}
|
|
888
900
|
});
|
|
889
901
|
} catch (error) {
|
|
890
|
-
|
|
891
|
-
const errorMessage = String(error?.message || error || 'Unknown error');
|
|
892
|
-
|
|
893
|
-
if (errorType === 'Ratelimited' || errorMessage.toLowerCase().includes('rate limit')) {
|
|
894
|
-
const resetAt = error?.requestsResetAt
|
|
895
|
-
? new Date(error.requestsResetAt).toLocaleTimeString()
|
|
896
|
-
: 'approximately 1 hour from now';
|
|
897
|
-
throw new Error(`Linear API rate limit exceeded. Resets at: ${resetAt}. Please wait before retrying.`);
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
if (errorMessage.includes('Linear API error:')) {
|
|
901
|
-
throw error;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
throw new Error(`Linear project update operation failed: ${errorMessage}`);
|
|
902
|
+
return handleToolExecutionError(error, 'Linear project update operation failed');
|
|
905
903
|
}
|
|
906
904
|
},
|
|
907
905
|
});
|
|
@@ -909,7 +907,7 @@ async function registerLinearTools(pi) {
|
|
|
909
907
|
pi.registerTool({
|
|
910
908
|
name: 'linear_team',
|
|
911
909
|
label: 'Linear Team',
|
|
912
|
-
description: 'Interact with Linear teams.
|
|
910
|
+
description: 'Interact with Linear teams.',
|
|
913
911
|
promptSnippet: 'Interact with Linear teams (list)',
|
|
914
912
|
parameters: {
|
|
915
913
|
type: 'object',
|
|
@@ -937,22 +935,7 @@ async function registerLinearTools(pi) {
|
|
|
937
935
|
}
|
|
938
936
|
});
|
|
939
937
|
} catch (error) {
|
|
940
|
-
|
|
941
|
-
const errorType = error?.type || '';
|
|
942
|
-
const errorMessage = String(error?.message || error || 'Unknown error');
|
|
943
|
-
|
|
944
|
-
if (errorType === 'Ratelimited' || errorMessage.toLowerCase().includes('rate limit')) {
|
|
945
|
-
const resetAt = error?.requestsResetAt
|
|
946
|
-
? new Date(error.requestsResetAt).toLocaleTimeString()
|
|
947
|
-
: 'approximately 1 hour from now';
|
|
948
|
-
throw new Error(`Linear API rate limit exceeded. Resets at: ${resetAt}. Please wait before retrying.`);
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
if (errorMessage.includes('Linear API error:')) {
|
|
952
|
-
throw error;
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
throw new Error(`Linear team operation failed: ${errorMessage}`);
|
|
938
|
+
return handleToolExecutionError(error, 'Linear team operation failed');
|
|
956
939
|
}
|
|
957
940
|
},
|
|
958
941
|
});
|
|
@@ -961,7 +944,7 @@ async function registerLinearTools(pi) {
|
|
|
961
944
|
pi.registerTool({
|
|
962
945
|
name: 'linear_milestone',
|
|
963
946
|
label: 'Linear Milestone',
|
|
964
|
-
description: 'Interact with Linear project milestones.
|
|
947
|
+
description: 'Interact with Linear project milestones.',
|
|
965
948
|
promptSnippet: 'Interact with Linear milestones (list, view, create, update, delete)',
|
|
966
949
|
parameters: {
|
|
967
950
|
type: 'object',
|
|
@@ -1017,24 +1000,9 @@ async function registerLinearTools(pi) {
|
|
|
1017
1000
|
}
|
|
1018
1001
|
});
|
|
1019
1002
|
} catch (error) {
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
const errorType = error?.type || '';
|
|
1024
|
-
const errorMessage = String(hintError?.message || hintError || 'Unknown error');
|
|
1025
|
-
|
|
1026
|
-
if (errorType === 'Ratelimited' || errorMessage.toLowerCase().includes('rate limit')) {
|
|
1027
|
-
const resetAt = error?.requestsResetAt
|
|
1028
|
-
? new Date(error.requestsResetAt).toLocaleTimeString()
|
|
1029
|
-
: 'approximately 1 hour from now';
|
|
1030
|
-
throw new Error(`Linear API rate limit exceeded. Resets at: ${resetAt}. Please wait before retrying.`);
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
if (errorMessage.includes('Linear API error:')) {
|
|
1034
|
-
throw hintError;
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
throw new Error(`Linear milestone operation failed: ${errorMessage}`);
|
|
1003
|
+
return handleToolExecutionError(error, 'Linear milestone operation failed', {
|
|
1004
|
+
transformError: withMilestoneScopeHint,
|
|
1005
|
+
});
|
|
1038
1006
|
}
|
|
1039
1007
|
},
|
|
1040
1008
|
});
|
package/package.json
CHANGED
package/src/handlers.js
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
fetchTeams,
|
|
18
18
|
resolveProjectRef,
|
|
19
19
|
resolveTeamRef,
|
|
20
|
+
resolveMilestoneRef,
|
|
20
21
|
getTeamWorkflowStates,
|
|
21
22
|
fetchIssueDetails,
|
|
22
23
|
fetchIssueActivity,
|
|
@@ -319,6 +320,10 @@ export async function executeIssueCreate(client, params, options = {}) {
|
|
|
319
320
|
createInput.priority = params.priority;
|
|
320
321
|
}
|
|
321
322
|
|
|
323
|
+
if (params.estimate !== undefined && params.estimate !== null) {
|
|
324
|
+
createInput.estimate = params.estimate;
|
|
325
|
+
}
|
|
326
|
+
|
|
322
327
|
if (params.parentId) {
|
|
323
328
|
createInput.parentId = params.parentId;
|
|
324
329
|
}
|
|
@@ -401,6 +406,7 @@ export async function executeIssueUpdate(client, params) {
|
|
|
401
406
|
title: params.title,
|
|
402
407
|
description: params.description,
|
|
403
408
|
priority: params.priority,
|
|
409
|
+
estimate: params.estimate,
|
|
404
410
|
state: params.state,
|
|
405
411
|
milestone: params.milestone,
|
|
406
412
|
projectMilestoneId: params.projectMilestoneId,
|
|
@@ -1041,7 +1047,24 @@ export async function executeMilestoneList(client, params) {
|
|
|
1041
1047
|
* View milestone details
|
|
1042
1048
|
*/
|
|
1043
1049
|
export async function executeMilestoneView(client, params) {
|
|
1044
|
-
|
|
1050
|
+
let projectId = null;
|
|
1051
|
+
let milestoneId = params.milestone;
|
|
1052
|
+
|
|
1053
|
+
// If milestone is not a Linear ID, resolve it with project context
|
|
1054
|
+
const ref = String(milestoneId || '').trim();
|
|
1055
|
+
const isId = typeof ref === 'string' && /^[0-9a-fA-F-]{16,}$/.test(ref);
|
|
1056
|
+
|
|
1057
|
+
if (!isId) {
|
|
1058
|
+
// Need project context to resolve milestone name
|
|
1059
|
+
let projectRef = params.project;
|
|
1060
|
+
if (!projectRef) {
|
|
1061
|
+
projectRef = path.basename(process.cwd());
|
|
1062
|
+
}
|
|
1063
|
+
const resolvedProject = await resolveProjectRef(client, projectRef);
|
|
1064
|
+
projectId = resolvedProject.id;
|
|
1065
|
+
const resolvedMilestone = await resolveMilestoneRef(client, ref, projectId);
|
|
1066
|
+
milestoneId = resolvedMilestone.id;
|
|
1067
|
+
}
|
|
1045
1068
|
|
|
1046
1069
|
const milestoneData = await fetchMilestoneDetails(client, milestoneId);
|
|
1047
1070
|
|
|
@@ -1163,7 +1186,25 @@ export async function executeMilestoneCreate(client, params) {
|
|
|
1163
1186
|
* Update a milestone
|
|
1164
1187
|
*/
|
|
1165
1188
|
export async function executeMilestoneUpdate(client, params) {
|
|
1166
|
-
|
|
1189
|
+
let projectId = null;
|
|
1190
|
+
let milestoneId = params.milestone;
|
|
1191
|
+
|
|
1192
|
+
// If milestone is not a Linear ID, resolve it with project context
|
|
1193
|
+
const ref = String(milestoneId || '').trim();
|
|
1194
|
+
const isId = typeof ref === 'string' && /^[0-9a-fA-F-]{16,}$/.test(ref);
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
if (!isId) {
|
|
1198
|
+
// Need project context to resolve milestone name
|
|
1199
|
+
let projectRef = params.project;
|
|
1200
|
+
if (!projectRef) {
|
|
1201
|
+
projectRef = path.basename(process.cwd());
|
|
1202
|
+
}
|
|
1203
|
+
const resolvedProject = await resolveProjectRef(client, projectRef);
|
|
1204
|
+
projectId = resolvedProject.id;
|
|
1205
|
+
const resolvedMilestone = await resolveMilestoneRef(client, ref, projectId);
|
|
1206
|
+
milestoneId = resolvedMilestone.id;
|
|
1207
|
+
}
|
|
1167
1208
|
|
|
1168
1209
|
// Note: status is not included as it's a computed/read-only field in Linear's API
|
|
1169
1210
|
const result = await updateProjectMilestone(client, milestoneId, {
|
|
@@ -1202,7 +1243,26 @@ export async function executeMilestoneUpdate(client, params) {
|
|
|
1202
1243
|
* Delete a milestone
|
|
1203
1244
|
*/
|
|
1204
1245
|
export async function executeMilestoneDelete(client, params) {
|
|
1205
|
-
|
|
1246
|
+
let projectId = null;
|
|
1247
|
+
let milestoneId = params.milestone;
|
|
1248
|
+
|
|
1249
|
+
// If milestone is not a Linear ID, resolve it with project context
|
|
1250
|
+
const ref = String(milestoneId || '').trim();
|
|
1251
|
+
const isId = typeof ref === 'string' && /^[0-9a-fA-F-]{16,}$/.test(ref);
|
|
1252
|
+
|
|
1253
|
+
|
|
1254
|
+
if (!isId) {
|
|
1255
|
+
// Need project context to resolve milestone name
|
|
1256
|
+
let projectRef = params.project;
|
|
1257
|
+
if (!projectRef) {
|
|
1258
|
+
projectRef = path.basename(process.cwd());
|
|
1259
|
+
}
|
|
1260
|
+
const resolvedProject = await resolveProjectRef(client, projectRef);
|
|
1261
|
+
projectId = resolvedProject.id;
|
|
1262
|
+
const resolvedMilestone = await resolveMilestoneRef(client, ref, projectId);
|
|
1263
|
+
milestoneId = resolvedMilestone.id;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1206
1266
|
const result = await deleteProjectMilestone(client, milestoneId);
|
|
1207
1267
|
|
|
1208
1268
|
const label = result.name
|