@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.
package/src/handlers.js CHANGED
@@ -13,18 +13,33 @@ import {
13
13
  updateIssue,
14
14
  createIssue,
15
15
  fetchProjects,
16
+ fetchProjectDetails,
16
17
  fetchTeams,
17
18
  resolveProjectRef,
18
19
  resolveTeamRef,
20
+ resolveMilestoneRef,
19
21
  getTeamWorkflowStates,
20
22
  fetchIssueDetails,
23
+ fetchIssueActivity,
21
24
  formatIssueAsMarkdown,
25
+ formatIssueActivityAsMarkdown,
22
26
  fetchIssuesByProject,
23
27
  fetchProjectMilestones,
24
28
  fetchMilestoneDetails,
25
29
  createProjectMilestone,
26
30
  updateProjectMilestone,
27
31
  deleteProjectMilestone,
32
+ createProject,
33
+ updateProject,
34
+ deleteProject,
35
+ archiveProject,
36
+ unarchiveProject,
37
+ fetchProjectUpdates,
38
+ fetchProjectUpdateDetails,
39
+ createProjectUpdate,
40
+ updateProjectUpdate,
41
+ archiveProjectUpdate,
42
+ unarchiveProjectUpdate,
28
43
  deleteIssue,
29
44
  withHandlerErrorHandling,
30
45
  getViewer,
@@ -44,6 +59,17 @@ function ensureNonEmpty(value, fieldName) {
44
59
  return text;
45
60
  }
46
61
 
62
+ function parseRefList(value) {
63
+ if (value === undefined || value === null) return [];
64
+ if (Array.isArray(value)) {
65
+ return value.map((item) => String(item || '').trim()).filter(Boolean);
66
+ }
67
+ return String(value)
68
+ .split(',')
69
+ .map((item) => item.trim())
70
+ .filter(Boolean);
71
+ }
72
+
47
73
  // ===== GIT OPERATIONS (for issue start) =====
48
74
 
49
75
  /**
@@ -226,6 +252,28 @@ export async function executeIssueView(client, params) {
226
252
  };
227
253
  }
228
254
 
255
+ export async function executeIssueActivity(client, params) {
256
+ const issue = ensureNonEmpty(params.issue, 'issue');
257
+ const activityData = await fetchIssueActivity(client, issue, {
258
+ limit: params.limit || 25,
259
+ includeArchived: params.includeArchived === true,
260
+ });
261
+ const markdown = formatIssueActivityAsMarkdown(activityData, {
262
+ limit: params.limit,
263
+ });
264
+
265
+ return {
266
+ content: [{ type: 'text', text: markdown }],
267
+ details: {
268
+ issueId: activityData.issue.id,
269
+ identifier: activityData.issue.identifier,
270
+ title: activityData.issue.title,
271
+ activityCount: activityData.activity.length,
272
+ url: activityData.issue.url,
273
+ },
274
+ };
275
+ }
276
+
229
277
  /**
230
278
  * Create a new issue
231
279
  */
@@ -272,6 +320,10 @@ export async function executeIssueCreate(client, params, options = {}) {
272
320
  createInput.priority = params.priority;
273
321
  }
274
322
 
323
+ if (params.estimate !== undefined && params.estimate !== null) {
324
+ createInput.estimate = params.estimate;
325
+ }
326
+
275
327
  if (params.parentId) {
276
328
  createInput.parentId = params.parentId;
277
329
  }
@@ -354,6 +406,7 @@ export async function executeIssueUpdate(client, params) {
354
406
  title: params.title,
355
407
  description: params.description,
356
408
  priority: params.priority,
409
+ estimate: params.estimate,
357
410
  state: params.state,
358
411
  milestone: params.milestone,
359
412
  projectMilestoneId: params.projectMilestoneId,
@@ -551,6 +604,362 @@ export async function executeProjectList(client) {
551
604
  });
552
605
  }
553
606
 
607
+ export async function executeProjectView(client, params) {
608
+ return withHandlerErrorHandling(async () => {
609
+ const projectRef = ensureNonEmpty(params.project, 'project');
610
+ const project = await fetchProjectDetails(client, projectRef);
611
+
612
+ const lines = [`# Project: ${project.name}`];
613
+ const metaParts = [];
614
+
615
+ if (project.status?.name) metaParts.push(`**Status:** ${project.status.name}`);
616
+ if (project.health) metaParts.push(`**Health:** ${project.health}`);
617
+ if (project.progress !== undefined && project.progress !== null) metaParts.push(`**Progress:** ${project.progress}%`);
618
+ if (project.priority !== undefined && project.priority !== null) metaParts.push(`**Priority:** ${project.priority}`);
619
+ if (project.startDate) metaParts.push(`**Start:** ${project.startDate}`);
620
+ if (project.targetDate) metaParts.push(`**Target:** ${project.targetDate}`);
621
+
622
+ if (metaParts.length > 0) {
623
+ lines.push('');
624
+ lines.push(metaParts.join(' | '));
625
+ }
626
+
627
+ const teamLabel = project.teams.length > 0
628
+ ? project.teams.map((team) => `\`${team.key}\``).join(', ')
629
+ : 'None';
630
+ const leadLabel = project.lead?.displayName || project.lead?.name || 'Unassigned';
631
+
632
+ lines.push('');
633
+ lines.push(`**Teams:** ${teamLabel}`);
634
+ lines.push(`**Lead:** ${leadLabel}`);
635
+
636
+ if (project.url) {
637
+ lines.push(`**URL:** ${project.url}`);
638
+ }
639
+
640
+ if (project.description) {
641
+ lines.push('');
642
+ lines.push(project.description);
643
+ }
644
+
645
+ if (project.content) {
646
+ lines.push('');
647
+ if (project.description) {
648
+ lines.push('## Content');
649
+ lines.push('');
650
+ }
651
+ lines.push(project.content);
652
+ }
653
+
654
+ if (project.projectMilestones.length > 0) {
655
+ lines.push('');
656
+ lines.push(`## Milestones (${project.projectMilestones.length})`);
657
+ lines.push('');
658
+
659
+ for (const milestone of project.projectMilestones) {
660
+ const progressLabel = milestone.progress !== undefined && milestone.progress !== null
661
+ ? `${milestone.progress}%`
662
+ : 'N/A';
663
+ const targetLabel = milestone.targetDate ? ` → ${milestone.targetDate}` : '';
664
+ lines.push(`- **${milestone.name}** _[${milestone.status}]_ (${progressLabel})${targetLabel}`);
665
+ }
666
+ }
667
+
668
+ return toTextResult(lines.join('\n'), {
669
+ projectId: project.id,
670
+ name: project.name,
671
+ status: project.status,
672
+ teamCount: project.teams.length,
673
+ milestoneCount: project.projectMilestones.length,
674
+ url: project.url,
675
+ });
676
+ }, 'executeProjectView');
677
+ }
678
+
679
+ export async function executeProjectCreate(client, params) {
680
+ return withHandlerErrorHandling(async () => {
681
+ const name = ensureNonEmpty(params.name, 'name');
682
+ const teamRefs = parseRefList(params.teams ?? params.team);
683
+
684
+ if (teamRefs.length === 0) {
685
+ throw new Error('Missing required field: teams');
686
+ }
687
+
688
+ const teams = await Promise.all(teamRefs.map((teamRef) => resolveTeamRef(client, teamRef)));
689
+
690
+ let leadId;
691
+ if (params.lead === 'me') {
692
+ const viewer = await getViewer(client);
693
+ leadId = viewer.id;
694
+ } else if (params.lead) {
695
+ leadId = params.lead;
696
+ }
697
+
698
+ const project = await createProject(client, {
699
+ name,
700
+ teamIds: teams.map((team) => team.id),
701
+ description: params.description,
702
+ leadId,
703
+ priority: params.priority,
704
+ color: params.color,
705
+ icon: params.icon,
706
+ startDate: params.startDate,
707
+ targetDate: params.targetDate,
708
+ });
709
+
710
+ return toTextResult(
711
+ `Created project **${project.name}** for ${teams.map((team) => team.key).join(', ')}`,
712
+ {
713
+ projectId: project.id,
714
+ name: project.name,
715
+ teams: project.teams,
716
+ status: project.status,
717
+ url: project.url,
718
+ }
719
+ );
720
+ }, 'executeProjectCreate');
721
+ }
722
+
723
+ export async function executeProjectUpdate(client, params) {
724
+ return withHandlerErrorHandling(async () => {
725
+ const projectRef = ensureNonEmpty(params.project, 'project');
726
+ const patch = {
727
+ name: params.name,
728
+ description: params.description,
729
+ priority: params.priority,
730
+ color: params.color,
731
+ icon: params.icon,
732
+ startDate: params.startDate,
733
+ targetDate: params.targetDate,
734
+ };
735
+
736
+ if (params.lead === 'me') {
737
+ const viewer = await getViewer(client);
738
+ patch.leadId = viewer.id;
739
+ } else if (params.lead === 'none') {
740
+ patch.leadId = null;
741
+ } else if (params.lead !== undefined) {
742
+ patch.leadId = params.lead;
743
+ }
744
+
745
+ if (params.teams !== undefined || params.team !== undefined) {
746
+ const teamRefs = parseRefList(params.teams ?? params.team);
747
+ if (teamRefs.length === 0) {
748
+ throw new Error('At least one team is required when updating teams');
749
+ }
750
+ const teams = await Promise.all(teamRefs.map((teamRef) => resolveTeamRef(client, teamRef)));
751
+ patch.teamIds = teams.map((team) => team.id);
752
+ }
753
+
754
+ const result = await updateProject(client, projectRef, patch);
755
+
756
+ return toTextResult(
757
+ `Updated project **${result.project.name}** (${result.changed.join(', ')})`,
758
+ {
759
+ projectId: result.project.id,
760
+ name: result.project.name,
761
+ changed: result.changed,
762
+ status: result.project.status,
763
+ }
764
+ );
765
+ }, 'executeProjectUpdate');
766
+ }
767
+
768
+ export async function executeProjectDelete(client, params) {
769
+ return withHandlerErrorHandling(async () => {
770
+ const projectRef = ensureNonEmpty(params.project, 'project');
771
+ const result = await deleteProject(client, projectRef);
772
+
773
+ return toTextResult(
774
+ `Deleted project **${result.name}** \`${result.projectId}\``,
775
+ {
776
+ projectId: result.projectId,
777
+ name: result.name,
778
+ success: result.success,
779
+ }
780
+ );
781
+ }, 'executeProjectDelete');
782
+ }
783
+
784
+ export async function executeProjectArchive(client, params) {
785
+ return withHandlerErrorHandling(async () => {
786
+ const projectRef = ensureNonEmpty(params.project, 'project');
787
+ const result = await archiveProject(client, projectRef);
788
+
789
+ return toTextResult(
790
+ `Archived project **${result.name || result.entity?.name || result.projectId}**`,
791
+ {
792
+ projectId: result.projectId,
793
+ name: result.name || result.entity?.name || null,
794
+ success: result.success,
795
+ }
796
+ );
797
+ }, 'executeProjectArchive');
798
+ }
799
+
800
+ export async function executeProjectUnarchive(client, params) {
801
+ return withHandlerErrorHandling(async () => {
802
+ const projectRef = ensureNonEmpty(params.project, 'project');
803
+ const result = await unarchiveProject(client, projectRef);
804
+
805
+ return toTextResult(
806
+ `Unarchived project **${result.project.name}**`,
807
+ {
808
+ projectId: result.project.id,
809
+ name: result.project.name,
810
+ success: result.success,
811
+ }
812
+ );
813
+ }, 'executeProjectUnarchive');
814
+ }
815
+
816
+ // ===== PROJECT UPDATE HANDLERS =====
817
+
818
+ export async function executeProjectUpdateList(client, params) {
819
+ return withHandlerErrorHandling(async () => {
820
+ const projectRef = ensureNonEmpty(params.project, 'project');
821
+ const { project, updates } = await fetchProjectUpdates(client, projectRef, {
822
+ limit: params.limit ?? 10,
823
+ includeArchived: params.includeArchived === true,
824
+ });
825
+
826
+ if (updates.length === 0) {
827
+ return toTextResult(`No project updates found for "${project.name}"`, {
828
+ projectId: project.id,
829
+ projectName: project.name,
830
+ updateCount: 0,
831
+ });
832
+ }
833
+
834
+ const lines = [`## Project updates for "${project.name}" (${updates.length})`, ''];
835
+ for (const update of updates) {
836
+ const author = update.user?.displayName || update.user?.name || 'Unknown';
837
+ const createdAt = update.createdAt ? String(update.createdAt).slice(0, 10) : 'unknown date';
838
+ const healthLabel = update.health ? ` [${update.health}]` : '';
839
+ const archivedLabel = update.archivedAt ? ' [archived]' : '';
840
+ const preview = update.body.replace(/\s+/g, ' ').trim().slice(0, 120);
841
+ lines.push(`- **${update.id}**${healthLabel}${archivedLabel} by ${author} on ${createdAt}`);
842
+ if (preview) {
843
+ lines.push(` ${preview}${update.body.length > 120 ? '...' : ''}`);
844
+ }
845
+ }
846
+
847
+ return toTextResult(lines.join('\n'), {
848
+ projectId: project.id,
849
+ projectName: project.name,
850
+ updateCount: updates.length,
851
+ });
852
+ }, 'executeProjectUpdateList');
853
+ }
854
+
855
+ export async function executeProjectUpdateView(client, params) {
856
+ return withHandlerErrorHandling(async () => {
857
+ const projectUpdateId = ensureNonEmpty(params.projectUpdate, 'projectUpdate');
858
+ const projectUpdate = await fetchProjectUpdateDetails(client, projectUpdateId);
859
+
860
+ const lines = [`# Project update ${projectUpdate.id}`];
861
+ const meta = [];
862
+ if (projectUpdate.project?.name) meta.push(`**Project:** ${projectUpdate.project.name}`);
863
+ if (projectUpdate.health) meta.push(`**Health:** ${projectUpdate.health}`);
864
+ if (projectUpdate.user?.displayName || projectUpdate.user?.name) meta.push(`**Author:** ${projectUpdate.user?.displayName || projectUpdate.user?.name}`);
865
+ if (projectUpdate.createdAt) meta.push(`**Created:** ${String(projectUpdate.createdAt).slice(0, 10)}`);
866
+ if (projectUpdate.archivedAt) meta.push(`**Archived:** ${String(projectUpdate.archivedAt).slice(0, 10)}`);
867
+ if (meta.length > 0) {
868
+ lines.push('');
869
+ lines.push(meta.join(' | '));
870
+ }
871
+ if (projectUpdate.url) {
872
+ lines.push('');
873
+ lines.push(`**URL:** ${projectUpdate.url}`);
874
+ }
875
+ if (projectUpdate.body) {
876
+ lines.push('');
877
+ lines.push(projectUpdate.body);
878
+ }
879
+
880
+ return toTextResult(lines.join('\n'), {
881
+ projectUpdateId: projectUpdate.id,
882
+ projectId: projectUpdate.project?.id || null,
883
+ projectName: projectUpdate.project?.name || null,
884
+ health: projectUpdate.health,
885
+ archivedAt: projectUpdate.archivedAt,
886
+ });
887
+ }, 'executeProjectUpdateView');
888
+ }
889
+
890
+ export async function executeProjectUpdateCreate(client, params) {
891
+ return withHandlerErrorHandling(async () => {
892
+ const projectRef = ensureNonEmpty(params.project, 'project');
893
+ const resolved = await resolveProjectRef(client, projectRef);
894
+ const projectUpdate = await createProjectUpdate(client, {
895
+ projectId: resolved.id,
896
+ body: params.body,
897
+ health: params.health,
898
+ isDiffHidden: params.isDiffHidden,
899
+ });
900
+
901
+ return toTextResult(
902
+ `Created project update **${projectUpdate.id}** for "${resolved.name}"`,
903
+ {
904
+ projectUpdateId: projectUpdate.id,
905
+ projectId: resolved.id,
906
+ projectName: resolved.name,
907
+ health: projectUpdate.health,
908
+ }
909
+ );
910
+ }, 'executeProjectUpdateCreate');
911
+ }
912
+
913
+ export async function executeProjectUpdateUpdate(client, params) {
914
+ return withHandlerErrorHandling(async () => {
915
+ const projectUpdateId = ensureNonEmpty(params.projectUpdate, 'projectUpdate');
916
+ const result = await updateProjectUpdate(client, projectUpdateId, {
917
+ body: params.body,
918
+ health: params.health,
919
+ isDiffHidden: params.isDiffHidden,
920
+ });
921
+
922
+ return toTextResult(
923
+ `Updated project update **${result.projectUpdate.id}** (${result.changed.join(', ')})`,
924
+ {
925
+ projectUpdateId: result.projectUpdate.id,
926
+ changed: result.changed,
927
+ health: result.projectUpdate.health,
928
+ }
929
+ );
930
+ }, 'executeProjectUpdateUpdate');
931
+ }
932
+
933
+ export async function executeProjectUpdateArchive(client, params) {
934
+ return withHandlerErrorHandling(async () => {
935
+ const projectUpdateId = ensureNonEmpty(params.projectUpdate, 'projectUpdate');
936
+ const result = await archiveProjectUpdate(client, projectUpdateId);
937
+
938
+ return toTextResult(
939
+ `Archived project update **${projectUpdateId}**`,
940
+ {
941
+ projectUpdateId,
942
+ success: result.success,
943
+ }
944
+ );
945
+ }, 'executeProjectUpdateArchive');
946
+ }
947
+
948
+ export async function executeProjectUpdateUnarchive(client, params) {
949
+ return withHandlerErrorHandling(async () => {
950
+ const projectUpdateId = ensureNonEmpty(params.projectUpdate, 'projectUpdate');
951
+ const result = await unarchiveProjectUpdate(client, projectUpdateId);
952
+
953
+ return toTextResult(
954
+ `Unarchived project update **${result.projectUpdate.id}**`,
955
+ {
956
+ projectUpdateId: result.projectUpdate.id,
957
+ success: result.success,
958
+ }
959
+ );
960
+ }, 'executeProjectUpdateUnarchive');
961
+ }
962
+
554
963
  // ===== TEAM HANDLERS =====
555
964
 
556
965
  /**
@@ -638,7 +1047,24 @@ export async function executeMilestoneList(client, params) {
638
1047
  * View milestone details
639
1048
  */
640
1049
  export async function executeMilestoneView(client, params) {
641
- 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
+ }
642
1068
 
643
1069
  const milestoneData = await fetchMilestoneDetails(client, milestoneId);
644
1070
 
@@ -760,7 +1186,25 @@ export async function executeMilestoneCreate(client, params) {
760
1186
  * Update a milestone
761
1187
  */
762
1188
  export async function executeMilestoneUpdate(client, params) {
763
- 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
+ }
764
1208
 
765
1209
  // Note: status is not included as it's a computed/read-only field in Linear's API
766
1210
  const result = await updateProjectMilestone(client, milestoneId, {
@@ -799,7 +1243,26 @@ export async function executeMilestoneUpdate(client, params) {
799
1243
  * Delete a milestone
800
1244
  */
801
1245
  export async function executeMilestoneDelete(client, params) {
802
- 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
+
803
1266
  const result = await deleteProjectMilestone(client, milestoneId);
804
1267
 
805
1268
  const label = result.name