@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 CHANGED
@@ -1,20 +1,37 @@
1
1
  # Changelog
2
2
 
3
- ## v0.4.3 (2026-04-02)
3
+ ## v0.5.0 (2026-04-17)
4
4
 
5
- Sync-doc parity and tool prompt metadata polish release.
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 first-run `sync-doc` batch parity so `check` and `run` produce aligned project index planning
9
- - Preserved existing managed marker location when `position` is `top`
10
- - Resolved archived projects more reliably during project update flows
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
- - Added `promptSnippet` metadata consistently across all Linear tools for better in-app tool discoverability/context
14
- - Added default activity limit handling for issue activity execution paths
15
-
16
- ### Tests
17
- - Added/updated sync-doc regression coverage for first-run check/run parity and marker placement stability
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. Actions: list, view, activity, create, update, comment, start, delete',
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
- // Re-throw if already formatted with "Linear API error:"
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. Actions: list, view, create, update, delete, archive, unarchive',
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
- // 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 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. Actions: list, view, create, update, archive, unarchive',
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
- const errorType = error?.type || '';
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. Actions: list',
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
- // Comprehensive error handling - catch ALL errors
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. Actions: list, view, create, update, delete',
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
- // Apply milestone-specific hint, then wrap with operation context
1021
- const hintError = withMilestoneScopeHint(error);
1022
- // Comprehensive error handling - catch ALL errors
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fink-andreas/pi-linear-tools",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "Pi extension with Linear SDK tools and configuration commands",
5
5
  "type": "module",
6
6
  "engines": {
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
- const milestoneId = ensureNonEmpty(params.milestone, 'milestone');
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
- const milestoneId = ensureNonEmpty(params.milestone, 'milestone');
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
- const milestoneId = ensureNonEmpty(params.milestone, 'milestone');
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