@fink-andreas/pi-linear-tools 0.4.2 → 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.
@@ -48,12 +48,25 @@ try {
48
48
  import {
49
49
  executeIssueList,
50
50
  executeIssueView,
51
+ executeIssueActivity,
51
52
  executeIssueCreate,
52
53
  executeIssueUpdate,
53
54
  executeIssueComment,
54
55
  executeIssueStart,
55
56
  executeIssueDelete,
56
57
  executeProjectList,
58
+ executeProjectView,
59
+ executeProjectCreate,
60
+ executeProjectUpdate,
61
+ executeProjectDelete,
62
+ executeProjectArchive,
63
+ executeProjectUnarchive,
64
+ executeProjectUpdateList,
65
+ executeProjectUpdateView,
66
+ executeProjectUpdateCreate,
67
+ executeProjectUpdateUpdate,
68
+ executeProjectUpdateArchive,
69
+ executeProjectUpdateUnarchive,
57
70
  executeTeamList,
58
71
  executeMilestoneList,
59
72
  executeMilestoneView,
@@ -439,6 +452,51 @@ async function shouldExposeMilestoneTool() {
439
452
  /**
440
453
  * Render tool result as markdown
441
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
+
442
500
  function renderMarkdownResult(result, _options, _theme) {
443
501
  const text = result.content?.[0]?.text || '';
444
502
 
@@ -478,18 +536,19 @@ async function registerLinearTools(pi) {
478
536
  pi.registerTool({
479
537
  name: 'linear_issue',
480
538
  label: 'Linear Issue',
481
- description: 'Interact with Linear issues. Actions: list, view, create, update, comment, start, delete',
539
+ description: 'Interact with Linear issues.',
540
+ promptSnippet: 'Interact with Linear issues (list, view, activity, create, update, comment, start, delete)',
482
541
  parameters: {
483
542
  type: 'object',
484
543
  properties: {
485
544
  action: {
486
545
  type: 'string',
487
- enum: ['list', 'view', 'create', 'update', 'comment', 'start', 'delete'],
546
+ enum: ['list', 'view', 'activity', 'create', 'update', 'comment', 'start', 'delete'],
488
547
  description: 'Action to perform on issue(s)',
489
548
  },
490
549
  issue: {
491
550
  type: 'string',
492
- description: 'Issue key (ABC-123) or Linear issue ID (for view, update, comment, start, delete)',
551
+ description: 'Issue key (ABC-123) or Linear issue ID (for view, activity, update, comment, start, delete)',
493
552
  },
494
553
  project: {
495
554
  type: 'string',
@@ -509,8 +568,8 @@ async function registerLinearTools(pi) {
509
568
  description: 'Optional explicit assignee ID alias for update/create debugging/compatibility.',
510
569
  },
511
570
  limit: {
512
- type: 'number',
513
- description: 'Maximum number of issues to list (default: 50)',
571
+ type: 'integer',
572
+ description: 'Maximum number of issues or activity entries to list',
514
573
  },
515
574
  includeComments: {
516
575
  type: 'boolean',
@@ -525,9 +584,13 @@ async function registerLinearTools(pi) {
525
584
  description: 'Issue description in markdown (for create, update)',
526
585
  },
527
586
  priority: {
528
- type: 'number',
587
+ type: 'integer',
529
588
  description: 'Priority 0..4 (for create, update)',
530
589
  },
590
+ includeArchived: {
591
+ type: 'boolean',
592
+ description: 'Include archived resources when listing activity or project updates',
593
+ },
531
594
  state: {
532
595
  type: 'string',
533
596
  description: 'Target state name or ID (for create, update)',
@@ -568,6 +631,10 @@ async function registerLinearTools(pi) {
568
631
  type: 'string',
569
632
  description: 'For update: mark this issue as duplicate of the given issue key/ID.',
570
633
  },
634
+ estimate: {
635
+ type: 'integer',
636
+ description: 'Estimate/story points for the issue (non-negative integer, for create/update)',
637
+ },
571
638
  team: {
572
639
  type: 'string',
573
640
  description: 'Team key (e.g. ENG) or name (optional if default team configured)',
@@ -599,17 +666,13 @@ async function registerLinearTools(pi) {
599
666
  },
600
667
  renderResult: renderMarkdownResult,
601
668
  async execute(_toolCallId, params) {
602
- // Pre-check: skip API calls if we know we're rate limited
603
- const { isRateLimited, resetAt } = checkAndClearRateLimit();
604
- if (isRateLimited) {
605
- throw new Error(
606
- `Linear API rate limit exceeded (cached).\n\n` +
607
- `The rate limit resets at: ${resetAt.toLocaleTimeString()}\n\n` +
608
- `Please wait before making more requests.`
609
- );
610
- }
611
-
612
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
+
613
676
  const client = await createAuthenticatedClient();
614
677
 
615
678
  return await withRequestUsageLogging(client, 'linear_issue', params.action, async () => {
@@ -618,6 +681,8 @@ async function registerLinearTools(pi) {
618
681
  return await executeIssueList(client, params);
619
682
  case 'view':
620
683
  return await executeIssueView(client, params);
684
+ case 'activity':
685
+ return await executeIssueActivity(client, params);
621
686
  case 'create':
622
687
  return await executeIssueCreate(client, params, { resolveDefaultTeam });
623
688
  case 'update':
@@ -641,18 +706,6 @@ async function registerLinearTools(pi) {
641
706
  const errorType = error?.type || '';
642
707
  const errorMessage = String(error?.message || error || 'Unknown error');
643
708
 
644
- // Rate limit error - provide clear reset time and mark globally
645
- if (errorType === 'Ratelimited' || errorMessage.toLowerCase().includes('rate limit')) {
646
- const resetTimestamp = error?.requestsResetAt || (Date.now() + 3600000);
647
- const resetTime = new Date(resetTimestamp).toLocaleTimeString();
648
- markRateLimited(resetTimestamp);
649
- throw new Error(
650
- `Linear API rate limit exceeded.\n\n` +
651
- `The rate limit resets at: ${resetTime}\n\n` +
652
- `Please wait before making more requests, or reduce the frequency of API calls.`
653
- );
654
- }
655
-
656
709
  // Authentication/Forbidden errors (handles SDK's ForbiddenLinearError)
657
710
  if (errorType === 'Forbidden' || errorType === 'AuthenticationError' ||
658
711
  errorMessage.toLowerCase().includes('forbidden') || errorMessage.toLowerCase().includes('unauthorized')) {
@@ -678,13 +731,7 @@ async function registerLinearTools(pi) {
678
731
  );
679
732
  }
680
733
 
681
- // Re-throw if already formatted with "Linear API error:"
682
- if (errorMessage.includes('Linear API error:')) {
683
- throw error;
684
- }
685
-
686
- // Wrap unexpected errors with context - NEVER let raw errors propagate
687
- throw new Error(`Linear issue operation failed: ${errorMessage}`);
734
+ return handleToolExecutionError(error, 'Linear issue operation failed');
688
735
  }
689
736
  },
690
737
  });
@@ -692,15 +739,59 @@ async function registerLinearTools(pi) {
692
739
  pi.registerTool({
693
740
  name: 'linear_project',
694
741
  label: 'Linear Project',
695
- description: 'Interact with Linear projects. Actions: list',
742
+ description: 'Interact with Linear projects.',
743
+ promptSnippet: 'Interact with Linear projects (list, view, create, update, delete, archive, unarchive)',
696
744
  parameters: {
697
745
  type: 'object',
698
746
  properties: {
699
747
  action: {
700
748
  type: 'string',
701
- enum: ['list'],
749
+ enum: ['list', 'view', 'create', 'update', 'delete', 'archive', 'unarchive'],
702
750
  description: 'Action to perform on project(s)',
703
751
  },
752
+ project: {
753
+ type: 'string',
754
+ description: 'Project name or ID (for view, update, delete)',
755
+ },
756
+ name: {
757
+ type: 'string',
758
+ description: 'Project name (required for create, optional for update)',
759
+ },
760
+ teams: {
761
+ type: 'string',
762
+ description: 'Comma-separated team keys or IDs (required for create, optional for update)',
763
+ },
764
+ description: {
765
+ type: 'string',
766
+ description: 'Project description in markdown',
767
+ },
768
+ lead: {
769
+ type: 'string',
770
+ description: 'Project lead user ID, "me", or "none" when updating',
771
+ },
772
+ priority: {
773
+ type: 'integer',
774
+ description: 'Priority 0-4',
775
+ minimum: 0,
776
+ maximum: 4,
777
+ multipleOf: 1,
778
+ },
779
+ color: {
780
+ type: 'string',
781
+ description: 'Project color (hex)',
782
+ },
783
+ icon: {
784
+ type: 'string',
785
+ description: 'Project icon',
786
+ },
787
+ startDate: {
788
+ type: 'string',
789
+ description: 'Planned start date (YYYY-MM-DD)',
790
+ },
791
+ targetDate: {
792
+ type: 'string',
793
+ description: 'Planned target date (YYYY-MM-DD)',
794
+ },
704
795
  },
705
796
  required: ['action'],
706
797
  additionalProperties: false,
@@ -714,27 +805,101 @@ async function registerLinearTools(pi) {
714
805
  switch (params.action) {
715
806
  case 'list':
716
807
  return await executeProjectList(client);
808
+ case 'view':
809
+ return await executeProjectView(client, params);
810
+ case 'create':
811
+ return await executeProjectCreate(client, params);
812
+ case 'update':
813
+ return await executeProjectUpdate(client, params);
814
+ case 'delete':
815
+ return await executeProjectDelete(client, params);
816
+ case 'archive':
817
+ return await executeProjectArchive(client, params);
818
+ case 'unarchive':
819
+ return await executeProjectUnarchive(client, params);
717
820
  default:
718
821
  throw new Error(`Unknown action: ${params.action}`);
719
822
  }
720
823
  });
721
824
  } catch (error) {
722
- // Comprehensive error handling - catch ALL errors
723
- const errorType = error?.type || '';
724
- const errorMessage = String(error?.message || error || 'Unknown error');
725
-
726
- if (errorType === 'Ratelimited' || errorMessage.toLowerCase().includes('rate limit')) {
727
- const resetAt = error?.requestsResetAt
728
- ? new Date(error.requestsResetAt).toLocaleTimeString()
729
- : 'approximately 1 hour from now';
730
- throw new Error(`Linear API rate limit exceeded. Resets at: ${resetAt}. Please wait before retrying.`);
731
- }
825
+ return handleToolExecutionError(error, 'Linear project operation failed');
826
+ }
827
+ },
828
+ });
732
829
 
733
- if (errorMessage.includes('Linear API error:')) {
734
- throw error;
735
- }
830
+ pi.registerTool({
831
+ name: 'linear_project_update',
832
+ label: 'Linear Project Update',
833
+ description: 'Interact with Linear project updates.',
834
+ promptSnippet: 'Interact with Linear project updates (list, view, create, update, archive, unarchive)',
835
+ parameters: {
836
+ type: 'object',
837
+ properties: {
838
+ action: {
839
+ type: 'string',
840
+ enum: ['list', 'view', 'create', 'update', 'archive', 'unarchive'],
841
+ description: 'Action to perform on project update(s)',
842
+ },
843
+ project: {
844
+ type: 'string',
845
+ description: 'Project name or ID (for list, create)',
846
+ },
847
+ projectUpdate: {
848
+ type: 'string',
849
+ description: 'Project update ID (for view, update, archive, unarchive)',
850
+ },
851
+ body: {
852
+ type: 'string',
853
+ description: 'Project update body in markdown',
854
+ },
855
+ health: {
856
+ type: 'string',
857
+ description: 'Project update health: onTrack, atRisk, or offTrack',
858
+ enum: ['onTrack', 'atRisk', 'offTrack'],
859
+ },
860
+ isDiffHidden: {
861
+ type: 'boolean',
862
+ description: 'Whether to hide the diff on the update',
863
+ },
864
+ limit: {
865
+ type: 'integer',
866
+ description: 'Max updates to list',
867
+ minimum: 1,
868
+ multipleOf: 1,
869
+ },
870
+ includeArchived: {
871
+ type: 'boolean',
872
+ description: 'Whether archived updates should be included when listing',
873
+ },
874
+ },
875
+ required: ['action'],
876
+ additionalProperties: false,
877
+ },
878
+ renderResult: renderMarkdownResult,
879
+ async execute(_toolCallId, params) {
880
+ try {
881
+ const client = await createAuthenticatedClient();
736
882
 
737
- throw new Error(`Linear project operation failed: ${errorMessage}`);
883
+ return await withRequestUsageLogging(client, 'linear_project_update', params.action, async () => {
884
+ switch (params.action) {
885
+ case 'list':
886
+ return await executeProjectUpdateList(client, params);
887
+ case 'view':
888
+ return await executeProjectUpdateView(client, params);
889
+ case 'create':
890
+ return await executeProjectUpdateCreate(client, params);
891
+ case 'update':
892
+ return await executeProjectUpdateUpdate(client, params);
893
+ case 'archive':
894
+ return await executeProjectUpdateArchive(client, params);
895
+ case 'unarchive':
896
+ return await executeProjectUpdateUnarchive(client, params);
897
+ default:
898
+ throw new Error(`Unknown action: ${params.action}`);
899
+ }
900
+ });
901
+ } catch (error) {
902
+ return handleToolExecutionError(error, 'Linear project update operation failed');
738
903
  }
739
904
  },
740
905
  });
@@ -742,7 +907,8 @@ async function registerLinearTools(pi) {
742
907
  pi.registerTool({
743
908
  name: 'linear_team',
744
909
  label: 'Linear Team',
745
- description: 'Interact with Linear teams. Actions: list',
910
+ description: 'Interact with Linear teams.',
911
+ promptSnippet: 'Interact with Linear teams (list)',
746
912
  parameters: {
747
913
  type: 'object',
748
914
  properties: {
@@ -769,22 +935,7 @@ async function registerLinearTools(pi) {
769
935
  }
770
936
  });
771
937
  } catch (error) {
772
- // Comprehensive error handling - catch ALL errors
773
- const errorType = error?.type || '';
774
- const errorMessage = String(error?.message || error || 'Unknown error');
775
-
776
- if (errorType === 'Ratelimited' || errorMessage.toLowerCase().includes('rate limit')) {
777
- const resetAt = error?.requestsResetAt
778
- ? new Date(error.requestsResetAt).toLocaleTimeString()
779
- : 'approximately 1 hour from now';
780
- throw new Error(`Linear API rate limit exceeded. Resets at: ${resetAt}. Please wait before retrying.`);
781
- }
782
-
783
- if (errorMessage.includes('Linear API error:')) {
784
- throw error;
785
- }
786
-
787
- throw new Error(`Linear team operation failed: ${errorMessage}`);
938
+ return handleToolExecutionError(error, 'Linear team operation failed');
788
939
  }
789
940
  },
790
941
  });
@@ -793,7 +944,8 @@ async function registerLinearTools(pi) {
793
944
  pi.registerTool({
794
945
  name: 'linear_milestone',
795
946
  label: 'Linear Milestone',
796
- description: 'Interact with Linear project milestones. Actions: list, view, create, update, delete',
947
+ description: 'Interact with Linear project milestones.',
948
+ promptSnippet: 'Interact with Linear milestones (list, view, create, update, delete)',
797
949
  parameters: {
798
950
  type: 'object',
799
951
  properties: {
@@ -848,24 +1000,9 @@ async function registerLinearTools(pi) {
848
1000
  }
849
1001
  });
850
1002
  } catch (error) {
851
- // Apply milestone-specific hint, then wrap with operation context
852
- const hintError = withMilestoneScopeHint(error);
853
- // Comprehensive error handling - catch ALL errors
854
- const errorType = error?.type || '';
855
- const errorMessage = String(hintError?.message || hintError || 'Unknown error');
856
-
857
- if (errorType === 'Ratelimited' || errorMessage.toLowerCase().includes('rate limit')) {
858
- const resetAt = error?.requestsResetAt
859
- ? new Date(error.requestsResetAt).toLocaleTimeString()
860
- : 'approximately 1 hour from now';
861
- throw new Error(`Linear API rate limit exceeded. Resets at: ${resetAt}. Please wait before retrying.`);
862
- }
863
-
864
- if (errorMessage.includes('Linear API error:')) {
865
- throw hintError;
866
- }
867
-
868
- throw new Error(`Linear milestone operation failed: ${errorMessage}`);
1003
+ return handleToolExecutionError(error, 'Linear milestone operation failed', {
1004
+ transformError: withMilestoneScopeHint,
1005
+ });
869
1006
  }
870
1007
  },
871
1008
  });
@@ -983,8 +1120,9 @@ export default async function piLinearToolsExtension(pi) {
983
1120
  const showMilestoneTool = await shouldExposeMilestoneTool();
984
1121
  const toolLines = [
985
1122
  'LLM-callable tools:',
986
- ' linear_issue (list/view/create/update/comment/start/delete)',
987
- ' linear_project (list)',
1123
+ ' linear_issue (list/view/activity/create/update/comment/start/delete)',
1124
+ ' linear_project (list/view/create/update/delete/archive/unarchive)',
1125
+ ' linear_project_update (list/view/create/update/archive/unarchive)',
988
1126
  ' linear_team (list)',
989
1127
  ];
990
1128