@fink-andreas/pi-linear-tools 0.4.1 → 0.4.3

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.
@@ -1,6 +1,6 @@
1
1
  import { loadSettings, saveSettings } from '../src/settings.js';
2
- import { createLinearClient, checkAndClearRateLimit, markRateLimited } from '../src/linear-client.js';
3
- import { setQuietMode } from '../src/logger.js';
2
+ import { createLinearClient, checkAndClearRateLimit, markRateLimited, getClientRequestMetrics } from '../src/linear-client.js';
3
+ import { setQuietMode, debug } from '../src/logger.js';
4
4
  import {
5
5
  resolveProjectRef,
6
6
  fetchTeams,
@@ -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,
@@ -65,6 +78,7 @@ import { authenticate, getAccessToken, logout } from '../src/auth/index.js';
65
78
  import { withMilestoneScopeHint } from '../src/error-hints.js';
66
79
 
67
80
  let cachedApiKey = null;
81
+ const INCLUDE_USAGE_SUMMARY = String(process.env.PI_LINEAR_TOOLS_USAGE_SUMMARY || '').toLowerCase() === 'true';
68
82
 
69
83
  async function getLinearAuth() {
70
84
  const envKey = process.env.LINEAR_API_KEY;
@@ -106,6 +120,65 @@ async function createAuthenticatedClient() {
106
120
  return createLinearClient(await getLinearAuth());
107
121
  }
108
122
 
123
+ async function withRequestUsageLogging(client, toolName, action, operation) {
124
+ const before = getClientRequestMetrics(client);
125
+
126
+ try {
127
+ const result = await operation();
128
+ const after = getClientRequestMetrics(client);
129
+
130
+ const usageDelta = {
131
+ tool: toolName,
132
+ action,
133
+ requestsDelta: after.total - before.total,
134
+ successDelta: after.success - before.success,
135
+ failedDelta: after.failed - before.failed,
136
+ rateLimitedDelta: after.rateLimited - before.rateLimited,
137
+ summary: `Linear API usage: ${after.total - before.total} req (${after.success - before.success} ok, ${after.failed - before.failed} failed, ${after.rateLimited - before.rateLimited} rate-limited)`,
138
+ };
139
+
140
+ debug('[pi-linear-tools] API usage per command', usageDelta);
141
+
142
+ if (!INCLUDE_USAGE_SUMMARY || !result || typeof result !== 'object') {
143
+ return result;
144
+ }
145
+
146
+ const details = (result.details && typeof result.details === 'object') ? result.details : {};
147
+ const content = Array.isArray(result.content)
148
+ ? result.content.map((item, idx) => {
149
+ if (idx !== 0 || item?.type !== 'text' || typeof item.text !== 'string') return item;
150
+ return {
151
+ ...item,
152
+ text: `${item.text}\n\n_${usageDelta.summary}_`,
153
+ };
154
+ })
155
+ : result.content;
156
+
157
+ return {
158
+ ...result,
159
+ content,
160
+ details: {
161
+ ...details,
162
+ apiUsage: usageDelta,
163
+ },
164
+ };
165
+ } catch (error) {
166
+ const after = getClientRequestMetrics(client);
167
+
168
+ debug('[pi-linear-tools] API usage per command (error)', {
169
+ tool: toolName,
170
+ action,
171
+ requestsDelta: after.total - before.total,
172
+ successDelta: after.success - before.success,
173
+ failedDelta: after.failed - before.failed,
174
+ rateLimitedDelta: after.rateLimited - before.rateLimited,
175
+ error: String(error?.message || error || 'unknown'),
176
+ });
177
+
178
+ throw error;
179
+ }
180
+ }
181
+
109
182
  async function resolveDefaultTeam(projectId) {
110
183
  const settings = await loadSettings();
111
184
 
@@ -418,18 +491,19 @@ async function registerLinearTools(pi) {
418
491
  pi.registerTool({
419
492
  name: 'linear_issue',
420
493
  label: 'Linear Issue',
421
- description: 'Interact with Linear issues. Actions: list, view, create, update, comment, start, delete',
494
+ description: 'Interact with Linear issues. Actions: list, view, activity, create, update, comment, start, delete',
495
+ promptSnippet: 'Interact with Linear issues (list, view, activity, create, update, comment, start, delete)',
422
496
  parameters: {
423
497
  type: 'object',
424
498
  properties: {
425
499
  action: {
426
500
  type: 'string',
427
- enum: ['list', 'view', 'create', 'update', 'comment', 'start', 'delete'],
501
+ enum: ['list', 'view', 'activity', 'create', 'update', 'comment', 'start', 'delete'],
428
502
  description: 'Action to perform on issue(s)',
429
503
  },
430
504
  issue: {
431
505
  type: 'string',
432
- description: 'Issue key (ABC-123) or Linear issue ID (for view, update, comment, start, delete)',
506
+ description: 'Issue key (ABC-123) or Linear issue ID (for view, activity, update, comment, start, delete)',
433
507
  },
434
508
  project: {
435
509
  type: 'string',
@@ -449,8 +523,8 @@ async function registerLinearTools(pi) {
449
523
  description: 'Optional explicit assignee ID alias for update/create debugging/compatibility.',
450
524
  },
451
525
  limit: {
452
- type: 'number',
453
- description: 'Maximum number of issues to list (default: 50)',
526
+ type: 'integer',
527
+ description: 'Maximum number of issues or activity entries to list',
454
528
  },
455
529
  includeComments: {
456
530
  type: 'boolean',
@@ -465,9 +539,13 @@ async function registerLinearTools(pi) {
465
539
  description: 'Issue description in markdown (for create, update)',
466
540
  },
467
541
  priority: {
468
- type: 'number',
542
+ type: 'integer',
469
543
  description: 'Priority 0..4 (for create, update)',
470
544
  },
545
+ includeArchived: {
546
+ type: 'boolean',
547
+ description: 'Include archived resources when listing activity or project updates',
548
+ },
471
549
  state: {
472
550
  type: 'string',
473
551
  description: 'Target state name or ID (for create, update)',
@@ -552,28 +630,32 @@ async function registerLinearTools(pi) {
552
630
  try {
553
631
  const client = await createAuthenticatedClient();
554
632
 
555
- switch (params.action) {
556
- case 'list':
557
- return await executeIssueList(client, params);
558
- case 'view':
559
- return await executeIssueView(client, params);
560
- case 'create':
561
- return await executeIssueCreate(client, params, { resolveDefaultTeam });
562
- case 'update':
563
- return await executeIssueUpdate(client, params);
564
- case 'comment':
565
- return await executeIssueComment(client, params);
566
- case 'start':
567
- return await executeIssueStart(client, params, {
568
- gitExecutor: async (branchName, fromRef, onBranchExists) => {
569
- return startGitBranchForIssue(pi, branchName, fromRef, onBranchExists);
570
- },
571
- });
572
- case 'delete':
573
- return await executeIssueDelete(client, params);
574
- default:
575
- throw new Error(`Unknown action: ${params.action}`);
576
- }
633
+ return await withRequestUsageLogging(client, 'linear_issue', params.action, async () => {
634
+ switch (params.action) {
635
+ case 'list':
636
+ return await executeIssueList(client, params);
637
+ case 'view':
638
+ return await executeIssueView(client, params);
639
+ case 'activity':
640
+ return await executeIssueActivity(client, params);
641
+ case 'create':
642
+ return await executeIssueCreate(client, params, { resolveDefaultTeam });
643
+ case 'update':
644
+ return await executeIssueUpdate(client, params);
645
+ case 'comment':
646
+ return await executeIssueComment(client, params);
647
+ case 'start':
648
+ return await executeIssueStart(client, params, {
649
+ gitExecutor: async (branchName, fromRef, onBranchExists) => {
650
+ return startGitBranchForIssue(pi, branchName, fromRef, onBranchExists);
651
+ },
652
+ });
653
+ case 'delete':
654
+ return await executeIssueDelete(client, params);
655
+ default:
656
+ throw new Error(`Unknown action: ${params.action}`);
657
+ }
658
+ });
577
659
  } catch (error) {
578
660
  // Comprehensive error handling - catch ALL errors including SDK's RatelimitedLinearError
579
661
  const errorType = error?.type || '';
@@ -630,15 +712,59 @@ async function registerLinearTools(pi) {
630
712
  pi.registerTool({
631
713
  name: 'linear_project',
632
714
  label: 'Linear Project',
633
- description: 'Interact with Linear projects. Actions: list',
715
+ description: 'Interact with Linear projects. Actions: list, view, create, update, delete, archive, unarchive',
716
+ promptSnippet: 'Interact with Linear projects (list, view, create, update, delete, archive, unarchive)',
634
717
  parameters: {
635
718
  type: 'object',
636
719
  properties: {
637
720
  action: {
638
721
  type: 'string',
639
- enum: ['list'],
722
+ enum: ['list', 'view', 'create', 'update', 'delete', 'archive', 'unarchive'],
640
723
  description: 'Action to perform on project(s)',
641
724
  },
725
+ project: {
726
+ type: 'string',
727
+ description: 'Project name or ID (for view, update, delete)',
728
+ },
729
+ name: {
730
+ type: 'string',
731
+ description: 'Project name (required for create, optional for update)',
732
+ },
733
+ teams: {
734
+ type: 'string',
735
+ description: 'Comma-separated team keys or IDs (required for create, optional for update)',
736
+ },
737
+ description: {
738
+ type: 'string',
739
+ description: 'Project description in markdown',
740
+ },
741
+ lead: {
742
+ type: 'string',
743
+ description: 'Project lead user ID, "me", or "none" when updating',
744
+ },
745
+ priority: {
746
+ type: 'integer',
747
+ description: 'Priority 0-4',
748
+ minimum: 0,
749
+ maximum: 4,
750
+ multipleOf: 1,
751
+ },
752
+ color: {
753
+ type: 'string',
754
+ description: 'Project color (hex)',
755
+ },
756
+ icon: {
757
+ type: 'string',
758
+ description: 'Project icon',
759
+ },
760
+ startDate: {
761
+ type: 'string',
762
+ description: 'Planned start date (YYYY-MM-DD)',
763
+ },
764
+ targetDate: {
765
+ type: 'string',
766
+ description: 'Planned target date (YYYY-MM-DD)',
767
+ },
642
768
  },
643
769
  required: ['action'],
644
770
  additionalProperties: false,
@@ -648,12 +774,26 @@ async function registerLinearTools(pi) {
648
774
  try {
649
775
  const client = await createAuthenticatedClient();
650
776
 
651
- switch (params.action) {
652
- case 'list':
653
- return await executeProjectList(client);
654
- default:
655
- throw new Error(`Unknown action: ${params.action}`);
656
- }
777
+ return await withRequestUsageLogging(client, 'linear_project', params.action, async () => {
778
+ switch (params.action) {
779
+ case 'list':
780
+ return await executeProjectList(client);
781
+ case 'view':
782
+ return await executeProjectView(client, params);
783
+ case 'create':
784
+ return await executeProjectCreate(client, params);
785
+ case 'update':
786
+ return await executeProjectUpdate(client, params);
787
+ case 'delete':
788
+ return await executeProjectDelete(client, params);
789
+ case 'archive':
790
+ return await executeProjectArchive(client, params);
791
+ case 'unarchive':
792
+ return await executeProjectUnarchive(client, params);
793
+ default:
794
+ throw new Error(`Unknown action: ${params.action}`);
795
+ }
796
+ });
657
797
  } catch (error) {
658
798
  // Comprehensive error handling - catch ALL errors
659
799
  const errorType = error?.type || '';
@@ -675,10 +815,102 @@ async function registerLinearTools(pi) {
675
815
  },
676
816
  });
677
817
 
818
+ pi.registerTool({
819
+ name: 'linear_project_update',
820
+ label: 'Linear Project Update',
821
+ description: 'Interact with Linear project updates. Actions: list, view, create, update, archive, unarchive',
822
+ promptSnippet: 'Interact with Linear project updates (list, view, create, update, archive, unarchive)',
823
+ parameters: {
824
+ type: 'object',
825
+ properties: {
826
+ action: {
827
+ type: 'string',
828
+ enum: ['list', 'view', 'create', 'update', 'archive', 'unarchive'],
829
+ description: 'Action to perform on project update(s)',
830
+ },
831
+ project: {
832
+ type: 'string',
833
+ description: 'Project name or ID (for list, create)',
834
+ },
835
+ projectUpdate: {
836
+ type: 'string',
837
+ description: 'Project update ID (for view, update, archive, unarchive)',
838
+ },
839
+ body: {
840
+ type: 'string',
841
+ description: 'Project update body in markdown',
842
+ },
843
+ health: {
844
+ type: 'string',
845
+ description: 'Project update health: onTrack, atRisk, or offTrack',
846
+ enum: ['onTrack', 'atRisk', 'offTrack'],
847
+ },
848
+ isDiffHidden: {
849
+ type: 'boolean',
850
+ description: 'Whether to hide the diff on the update',
851
+ },
852
+ limit: {
853
+ type: 'integer',
854
+ description: 'Max updates to list',
855
+ minimum: 1,
856
+ multipleOf: 1,
857
+ },
858
+ includeArchived: {
859
+ type: 'boolean',
860
+ description: 'Whether archived updates should be included when listing',
861
+ },
862
+ },
863
+ required: ['action'],
864
+ additionalProperties: false,
865
+ },
866
+ renderResult: renderMarkdownResult,
867
+ async execute(_toolCallId, params) {
868
+ try {
869
+ const client = await createAuthenticatedClient();
870
+
871
+ return await withRequestUsageLogging(client, 'linear_project_update', params.action, async () => {
872
+ switch (params.action) {
873
+ case 'list':
874
+ return await executeProjectUpdateList(client, params);
875
+ case 'view':
876
+ return await executeProjectUpdateView(client, params);
877
+ case 'create':
878
+ return await executeProjectUpdateCreate(client, params);
879
+ case 'update':
880
+ return await executeProjectUpdateUpdate(client, params);
881
+ case 'archive':
882
+ return await executeProjectUpdateArchive(client, params);
883
+ case 'unarchive':
884
+ return await executeProjectUpdateUnarchive(client, params);
885
+ default:
886
+ throw new Error(`Unknown action: ${params.action}`);
887
+ }
888
+ });
889
+ } 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}`);
905
+ }
906
+ },
907
+ });
908
+
678
909
  pi.registerTool({
679
910
  name: 'linear_team',
680
911
  label: 'Linear Team',
681
912
  description: 'Interact with Linear teams. Actions: list',
913
+ promptSnippet: 'Interact with Linear teams (list)',
682
914
  parameters: {
683
915
  type: 'object',
684
916
  properties: {
@@ -696,12 +928,14 @@ async function registerLinearTools(pi) {
696
928
  try {
697
929
  const client = await createAuthenticatedClient();
698
930
 
699
- switch (params.action) {
700
- case 'list':
701
- return await executeTeamList(client);
702
- default:
703
- throw new Error(`Unknown action: ${params.action}`);
704
- }
931
+ return await withRequestUsageLogging(client, 'linear_team', params.action, async () => {
932
+ switch (params.action) {
933
+ case 'list':
934
+ return await executeTeamList(client);
935
+ default:
936
+ throw new Error(`Unknown action: ${params.action}`);
937
+ }
938
+ });
705
939
  } catch (error) {
706
940
  // Comprehensive error handling - catch ALL errors
707
941
  const errorType = error?.type || '';
@@ -728,6 +962,7 @@ async function registerLinearTools(pi) {
728
962
  name: 'linear_milestone',
729
963
  label: 'Linear Milestone',
730
964
  description: 'Interact with Linear project milestones. Actions: list, view, create, update, delete',
965
+ promptSnippet: 'Interact with Linear milestones (list, view, create, update, delete)',
731
966
  parameters: {
732
967
  type: 'object',
733
968
  properties: {
@@ -765,20 +1000,22 @@ async function registerLinearTools(pi) {
765
1000
  try {
766
1001
  const client = await createAuthenticatedClient();
767
1002
 
768
- switch (params.action) {
769
- case 'list':
770
- return await executeMilestoneList(client, params);
771
- case 'view':
772
- return await executeMilestoneView(client, params);
773
- case 'create':
774
- return await executeMilestoneCreate(client, params);
775
- case 'update':
776
- return await executeMilestoneUpdate(client, params);
777
- case 'delete':
778
- return await executeMilestoneDelete(client, params);
779
- default:
780
- throw new Error(`Unknown action: ${params.action}`);
781
- }
1003
+ return await withRequestUsageLogging(client, 'linear_milestone', params.action, async () => {
1004
+ switch (params.action) {
1005
+ case 'list':
1006
+ return await executeMilestoneList(client, params);
1007
+ case 'view':
1008
+ return await executeMilestoneView(client, params);
1009
+ case 'create':
1010
+ return await executeMilestoneCreate(client, params);
1011
+ case 'update':
1012
+ return await executeMilestoneUpdate(client, params);
1013
+ case 'delete':
1014
+ return await executeMilestoneDelete(client, params);
1015
+ default:
1016
+ throw new Error(`Unknown action: ${params.action}`);
1017
+ }
1018
+ });
782
1019
  } catch (error) {
783
1020
  // Apply milestone-specific hint, then wrap with operation context
784
1021
  const hintError = withMilestoneScopeHint(error);
@@ -915,8 +1152,9 @@ export default async function piLinearToolsExtension(pi) {
915
1152
  const showMilestoneTool = await shouldExposeMilestoneTool();
916
1153
  const toolLines = [
917
1154
  'LLM-callable tools:',
918
- ' linear_issue (list/view/create/update/comment/start/delete)',
919
- ' linear_project (list)',
1155
+ ' linear_issue (list/view/activity/create/update/comment/start/delete)',
1156
+ ' linear_project (list/view/create/update/delete/archive/unarchive)',
1157
+ ' linear_project_update (list/view/create/update/archive/unarchive)',
920
1158
  ' linear_team (list)',
921
1159
  ];
922
1160