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

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,42 @@
1
1
  # Changelog
2
2
 
3
- ## v0.4.3 (2026-04-02)
3
+ ## v0.5.1 (2026-04-17)
4
4
 
5
- Sync-doc parity and tool prompt metadata polish release.
5
+ ### Bug Fixes
6
+ - **Prevent pi crashes from Linear tool errors**: Added `executeToolSafely` wrapper that catches errors and returns them as safe tool results instead of throwing, preventing pi from crashing on Linear API errors
7
+
8
+ ## v0.5.0 (2026-04-17)
9
+
10
+ Linear document sync, project lifecycle management, and estimate/story points support.
11
+
12
+ ### New Features
13
+ - **Linear document sync**: Keep Linear documents in sync with local Markdown files via `linear_doc_sync` command
14
+ - **Project lifecycle**: Full project lifecycle support including archive/unarchive, updates, and milestones
15
+ - **Estimate (story points)**: Support for estimate/story points field on issue create/update
16
+ - **Issue activity history**: View complete issue activity with `linear_issue_activity` tool
17
+ - **Batch sync**: `sync-doc --all` for batch syncing all configured document targets
6
18
 
7
19
  ### 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
20
+ - Fixed project name lookup for UUID-like project names
21
+ - Fixed milestone name lookup for view/update/delete operations
22
+ - Replaced 'order' with 'sortOrder' for ProjectMilestone queries
23
+ - Fixed sync-doc first-run check/run parity
24
+ - Fixed sync-doc drift detection and target matching
25
+ - Fixed project archive and update validation
26
+ - Fixed archived project resolution for project updates
27
+ - Fixed sync-doc marker position preservation when position is 'top'
28
+ - Fixed default limit for executeIssueActivity (now 25)
11
29
 
12
30
  ### 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
31
+ - Reduced expensive Linear API issue reads
32
+ - Better CLI help for issue and project workflows
33
+ - Improved sync-doc examples and guidance
34
+ - Added promptSnippet to all Linear tools for consistent LLM context
35
+ - Removed redundant action lists from tool descriptions
36
+
37
+ ### Cleanup
38
+ - Removed planning artifacts and documentation files
39
+ - Stopped tracking plan files in git
18
40
 
19
41
  ## v0.4.2 (2026-03-26)
20
42
 
@@ -452,6 +452,85 @@ 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
+
500
+ function buildGenericToolErrorResult(error, operationLabel) {
501
+ const errorType = error?.type || error?.name || 'Error';
502
+ const errorMessage = String(error?.message || error || 'Unknown error');
503
+
504
+ return toToolTextResult(`${operationLabel}: ${errorMessage}`, {
505
+ error: true,
506
+ errorType,
507
+ rateLimited: false,
508
+ });
509
+ }
510
+
511
+ async function executeToolSafely(operationLabel, operation, options = {}) {
512
+ try {
513
+ return await operation();
514
+ } catch (error) {
515
+ debug('[pi-linear-tools] tool execution failed', {
516
+ operationLabel,
517
+ errorType: error?.type || error?.name || null,
518
+ error: String(error?.message || error || 'unknown'),
519
+ });
520
+
521
+ try {
522
+ return handleToolExecutionError(error, operationLabel, options);
523
+ } catch (handledError) {
524
+ debug('[pi-linear-tools] returning generic safe tool error result', {
525
+ operationLabel,
526
+ errorType: handledError?.type || handledError?.name || null,
527
+ error: String(handledError?.message || handledError || 'unknown'),
528
+ });
529
+ return buildGenericToolErrorResult(handledError, operationLabel);
530
+ }
531
+ }
532
+ }
533
+
455
534
  function renderMarkdownResult(result, _options, _theme) {
456
535
  const text = result.content?.[0]?.text || '';
457
536
 
@@ -491,7 +570,7 @@ async function registerLinearTools(pi) {
491
570
  pi.registerTool({
492
571
  name: 'linear_issue',
493
572
  label: 'Linear Issue',
494
- description: 'Interact with Linear issues. Actions: list, view, activity, create, update, comment, start, delete',
573
+ description: 'Interact with Linear issues.',
495
574
  promptSnippet: 'Interact with Linear issues (list, view, activity, create, update, comment, start, delete)',
496
575
  parameters: {
497
576
  type: 'object',
@@ -586,6 +665,10 @@ async function registerLinearTools(pi) {
586
665
  type: 'string',
587
666
  description: 'For update: mark this issue as duplicate of the given issue key/ID.',
588
667
  },
668
+ estimate: {
669
+ type: 'integer',
670
+ description: 'Estimate/story points for the issue (non-negative integer, for create/update)',
671
+ },
589
672
  team: {
590
673
  type: 'string',
591
674
  description: 'Team key (e.g. ENG) or name (optional if default team configured)',
@@ -617,17 +700,13 @@ async function registerLinearTools(pi) {
617
700
  },
618
701
  renderResult: renderMarkdownResult,
619
702
  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
- }
703
+ return executeToolSafely('Linear issue operation failed', async () => {
704
+ // Pre-check: skip API calls if we know we're rate limited
705
+ const { isRateLimited, resetAt } = checkAndClearRateLimit();
706
+ if (isRateLimited) {
707
+ return buildRateLimitToolResult({ requestsResetAt: resetAt.getTime(), type: 'Ratelimited' }, { cached: true });
708
+ }
629
709
 
630
- try {
631
710
  const client = await createAuthenticatedClient();
632
711
 
633
712
  return await withRequestUsageLogging(client, 'linear_issue', params.action, async () => {
@@ -656,63 +735,14 @@ async function registerLinearTools(pi) {
656
735
  throw new Error(`Unknown action: ${params.action}`);
657
736
  }
658
737
  });
659
- } catch (error) {
660
- // Comprehensive error handling - catch ALL errors including SDK's RatelimitedLinearError
661
- const errorType = error?.type || '';
662
- const errorMessage = String(error?.message || error || 'Unknown error');
663
-
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
- // Authentication/Forbidden errors (handles SDK's ForbiddenLinearError)
677
- if (errorType === 'Forbidden' || errorType === 'AuthenticationError' ||
678
- errorMessage.toLowerCase().includes('forbidden') || errorMessage.toLowerCase().includes('unauthorized')) {
679
- throw new Error(
680
- `Linear API authentication failed: ${errorMessage}\n\n` +
681
- `Please check your API key or OAuth token permissions.`
682
- );
683
- }
684
-
685
- // Network errors (handles SDK's NetworkError)
686
- if (errorType === 'NetworkError' || errorMessage.toLowerCase().includes('network')) {
687
- throw new Error(
688
- `Network error communicating with Linear API.\n\n` +
689
- `Please check your internet connection and try again.`
690
- );
691
- }
692
-
693
- // Internal server errors (handles SDK's InternalError)
694
- if (errorType === 'InternalError' || (error?.status >= 500 && error?.status < 600)) {
695
- throw new Error(
696
- `Linear API server error (${error?.status || 'unknown'}).\n\n` +
697
- `Linear may be experiencing issues. Please try again later.`
698
- );
699
- }
700
-
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}`);
708
- }
738
+ });
709
739
  },
710
740
  });
711
741
 
712
742
  pi.registerTool({
713
743
  name: 'linear_project',
714
744
  label: 'Linear Project',
715
- description: 'Interact with Linear projects. Actions: list, view, create, update, delete, archive, unarchive',
745
+ description: 'Interact with Linear projects.',
716
746
  promptSnippet: 'Interact with Linear projects (list, view, create, update, delete, archive, unarchive)',
717
747
  parameters: {
718
748
  type: 'object',
@@ -771,7 +801,7 @@ async function registerLinearTools(pi) {
771
801
  },
772
802
  renderResult: renderMarkdownResult,
773
803
  async execute(_toolCallId, params) {
774
- try {
804
+ return executeToolSafely('Linear project operation failed', async () => {
775
805
  const client = await createAuthenticatedClient();
776
806
 
777
807
  return await withRequestUsageLogging(client, 'linear_project', params.action, async () => {
@@ -794,31 +824,14 @@ async function registerLinearTools(pi) {
794
824
  throw new Error(`Unknown action: ${params.action}`);
795
825
  }
796
826
  });
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 project operation failed: ${errorMessage}`);
814
- }
827
+ });
815
828
  },
816
829
  });
817
830
 
818
831
  pi.registerTool({
819
832
  name: 'linear_project_update',
820
833
  label: 'Linear Project Update',
821
- description: 'Interact with Linear project updates. Actions: list, view, create, update, archive, unarchive',
834
+ description: 'Interact with Linear project updates.',
822
835
  promptSnippet: 'Interact with Linear project updates (list, view, create, update, archive, unarchive)',
823
836
  parameters: {
824
837
  type: 'object',
@@ -865,7 +878,7 @@ async function registerLinearTools(pi) {
865
878
  },
866
879
  renderResult: renderMarkdownResult,
867
880
  async execute(_toolCallId, params) {
868
- try {
881
+ return executeToolSafely('Linear project update operation failed', async () => {
869
882
  const client = await createAuthenticatedClient();
870
883
 
871
884
  return await withRequestUsageLogging(client, 'linear_project_update', params.action, async () => {
@@ -886,30 +899,14 @@ async function registerLinearTools(pi) {
886
899
  throw new Error(`Unknown action: ${params.action}`);
887
900
  }
888
901
  });
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
- }
902
+ });
906
903
  },
907
904
  });
908
905
 
909
906
  pi.registerTool({
910
907
  name: 'linear_team',
911
908
  label: 'Linear Team',
912
- description: 'Interact with Linear teams. Actions: list',
909
+ description: 'Interact with Linear teams.',
913
910
  promptSnippet: 'Interact with Linear teams (list)',
914
911
  parameters: {
915
912
  type: 'object',
@@ -925,7 +922,7 @@ async function registerLinearTools(pi) {
925
922
  },
926
923
  renderResult: renderMarkdownResult,
927
924
  async execute(_toolCallId, params) {
928
- try {
925
+ return executeToolSafely('Linear team operation failed', async () => {
929
926
  const client = await createAuthenticatedClient();
930
927
 
931
928
  return await withRequestUsageLogging(client, 'linear_team', params.action, async () => {
@@ -936,24 +933,7 @@ async function registerLinearTools(pi) {
936
933
  throw new Error(`Unknown action: ${params.action}`);
937
934
  }
938
935
  });
939
- } 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}`);
956
- }
936
+ });
957
937
  },
958
938
  });
959
939
 
@@ -961,7 +941,7 @@ async function registerLinearTools(pi) {
961
941
  pi.registerTool({
962
942
  name: 'linear_milestone',
963
943
  label: 'Linear Milestone',
964
- description: 'Interact with Linear project milestones. Actions: list, view, create, update, delete',
944
+ description: 'Interact with Linear project milestones.',
965
945
  promptSnippet: 'Interact with Linear milestones (list, view, create, update, delete)',
966
946
  parameters: {
967
947
  type: 'object',
@@ -997,7 +977,7 @@ async function registerLinearTools(pi) {
997
977
  },
998
978
  renderResult: renderMarkdownResult,
999
979
  async execute(_toolCallId, params) {
1000
- try {
980
+ return executeToolSafely('Linear milestone operation failed', async () => {
1001
981
  const client = await createAuthenticatedClient();
1002
982
 
1003
983
  return await withRequestUsageLogging(client, 'linear_milestone', params.action, async () => {
@@ -1016,26 +996,9 @@ async function registerLinearTools(pi) {
1016
996
  throw new Error(`Unknown action: ${params.action}`);
1017
997
  }
1018
998
  });
1019
- } 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}`);
1038
- }
999
+ }, {
1000
+ transformError: withMilestoneScopeHint,
1001
+ });
1039
1002
  },
1040
1003
  });
1041
1004
  }
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.1",
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