@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.
- package/CHANGELOG.md +37 -0
- package/README.md +139 -13
- package/extensions/pi-linear-tools.js +298 -60
- package/index.js +1 -951
- package/package.json +6 -2
- package/src/cli.js +703 -7
- package/src/handlers.js +429 -4
- package/src/linear-client.js +7 -2
- package/src/linear.js +1502 -152
- package/src/sync-doc.js +1208 -0
- package/FUNCTIONALITY.md +0 -57
- package/POST_RELEASE_CHECKLIST.md +0 -30
- package/RELEASE.md +0 -50
package/src/handlers.js
CHANGED
|
@@ -13,20 +13,35 @@ import {
|
|
|
13
13
|
updateIssue,
|
|
14
14
|
createIssue,
|
|
15
15
|
fetchProjects,
|
|
16
|
+
fetchProjectDetails,
|
|
16
17
|
fetchTeams,
|
|
17
18
|
resolveProjectRef,
|
|
18
19
|
resolveTeamRef,
|
|
19
20
|
getTeamWorkflowStates,
|
|
20
21
|
fetchIssueDetails,
|
|
22
|
+
fetchIssueActivity,
|
|
21
23
|
formatIssueAsMarkdown,
|
|
24
|
+
formatIssueActivityAsMarkdown,
|
|
22
25
|
fetchIssuesByProject,
|
|
23
26
|
fetchProjectMilestones,
|
|
24
27
|
fetchMilestoneDetails,
|
|
25
28
|
createProjectMilestone,
|
|
26
29
|
updateProjectMilestone,
|
|
27
30
|
deleteProjectMilestone,
|
|
31
|
+
createProject,
|
|
32
|
+
updateProject,
|
|
33
|
+
deleteProject,
|
|
34
|
+
archiveProject,
|
|
35
|
+
unarchiveProject,
|
|
36
|
+
fetchProjectUpdates,
|
|
37
|
+
fetchProjectUpdateDetails,
|
|
38
|
+
createProjectUpdate,
|
|
39
|
+
updateProjectUpdate,
|
|
40
|
+
archiveProjectUpdate,
|
|
41
|
+
unarchiveProjectUpdate,
|
|
28
42
|
deleteIssue,
|
|
29
43
|
withHandlerErrorHandling,
|
|
44
|
+
getViewer,
|
|
30
45
|
} from './linear.js';
|
|
31
46
|
import { debug } from './logger.js';
|
|
32
47
|
|
|
@@ -43,6 +58,17 @@ function ensureNonEmpty(value, fieldName) {
|
|
|
43
58
|
return text;
|
|
44
59
|
}
|
|
45
60
|
|
|
61
|
+
function parseRefList(value) {
|
|
62
|
+
if (value === undefined || value === null) return [];
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
return value.map((item) => String(item || '').trim()).filter(Boolean);
|
|
65
|
+
}
|
|
66
|
+
return String(value)
|
|
67
|
+
.split(',')
|
|
68
|
+
.map((item) => item.trim())
|
|
69
|
+
.filter(Boolean);
|
|
70
|
+
}
|
|
71
|
+
|
|
46
72
|
// ===== GIT OPERATIONS (for issue start) =====
|
|
47
73
|
|
|
48
74
|
/**
|
|
@@ -130,6 +156,14 @@ async function startGitBranch(branchName, fromRef = 'HEAD', onBranchExists = 'sw
|
|
|
130
156
|
|
|
131
157
|
/**
|
|
132
158
|
* List issues in a project
|
|
159
|
+
* @param {LinearClient} client - Linear SDK client
|
|
160
|
+
* @param {Object} params - Parameters
|
|
161
|
+
* @param {string} [params.project] - Project name or ID
|
|
162
|
+
* @param {string[]} [params.states] - State names to filter by
|
|
163
|
+
* @param {string} [params.assignee] - "me" or "all" for assignee filtering
|
|
164
|
+
* @param {string} [params.team] - Team key or ID to filter by
|
|
165
|
+
* @param {number} [params.limit] - Maximum results (default: 20)
|
|
166
|
+
* @returns {Promise<{content: Array, details: Object}>}
|
|
133
167
|
*/
|
|
134
168
|
export async function executeIssueList(client, params) {
|
|
135
169
|
return withHandlerErrorHandling(async () => {
|
|
@@ -142,12 +176,20 @@ export async function executeIssueList(client, params) {
|
|
|
142
176
|
|
|
143
177
|
let assigneeId = null;
|
|
144
178
|
if (params.assignee === 'me') {
|
|
145
|
-
const viewer = await client
|
|
179
|
+
const viewer = await getViewer(client);
|
|
146
180
|
assigneeId = viewer.id;
|
|
147
181
|
}
|
|
148
182
|
|
|
183
|
+
// Resolve team if provided
|
|
184
|
+
let teamId = null;
|
|
185
|
+
if (params.team) {
|
|
186
|
+
const team = await resolveTeamRef(client, params.team);
|
|
187
|
+
teamId = team.id;
|
|
188
|
+
}
|
|
189
|
+
|
|
149
190
|
const { issues, truncated } = await fetchIssuesByProject(client, resolved.id, params.states || null, {
|
|
150
191
|
assigneeId,
|
|
192
|
+
teamId,
|
|
151
193
|
limit: params.limit || 20,
|
|
152
194
|
});
|
|
153
195
|
|
|
@@ -209,6 +251,28 @@ export async function executeIssueView(client, params) {
|
|
|
209
251
|
};
|
|
210
252
|
}
|
|
211
253
|
|
|
254
|
+
export async function executeIssueActivity(client, params) {
|
|
255
|
+
const issue = ensureNonEmpty(params.issue, 'issue');
|
|
256
|
+
const activityData = await fetchIssueActivity(client, issue, {
|
|
257
|
+
limit: params.limit || 25,
|
|
258
|
+
includeArchived: params.includeArchived === true,
|
|
259
|
+
});
|
|
260
|
+
const markdown = formatIssueActivityAsMarkdown(activityData, {
|
|
261
|
+
limit: params.limit,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
content: [{ type: 'text', text: markdown }],
|
|
266
|
+
details: {
|
|
267
|
+
issueId: activityData.issue.id,
|
|
268
|
+
identifier: activityData.issue.identifier,
|
|
269
|
+
title: activityData.issue.title,
|
|
270
|
+
activityCount: activityData.activity.length,
|
|
271
|
+
url: activityData.issue.url,
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
212
276
|
/**
|
|
213
277
|
* Create a new issue
|
|
214
278
|
*/
|
|
@@ -260,7 +324,7 @@ export async function executeIssueCreate(client, params, options = {}) {
|
|
|
260
324
|
}
|
|
261
325
|
|
|
262
326
|
if (params.assignee === 'me') {
|
|
263
|
-
const viewer = await client
|
|
327
|
+
const viewer = await getViewer(client);
|
|
264
328
|
createInput.assigneeId = viewer.id;
|
|
265
329
|
} else if (params.assignee) {
|
|
266
330
|
createInput.assigneeId = params.assignee;
|
|
@@ -358,7 +422,7 @@ export async function executeIssueUpdate(client, params) {
|
|
|
358
422
|
|
|
359
423
|
// Handle assignee parameter
|
|
360
424
|
if (params.assignee === 'me') {
|
|
361
|
-
const viewer = await client
|
|
425
|
+
const viewer = await getViewer(client);
|
|
362
426
|
updatePatch.assigneeId = viewer.id;
|
|
363
427
|
} else if (params.assignee) {
|
|
364
428
|
updatePatch.assigneeId = params.assignee;
|
|
@@ -413,8 +477,12 @@ export async function executeIssueUpdate(client, params) {
|
|
|
413
477
|
? ` (${changeSummaryParts.join(', ')})`
|
|
414
478
|
: '';
|
|
415
479
|
|
|
480
|
+
const rateLimitNote = result.usedRateLimitFallback
|
|
481
|
+
? '\n\n_Note: update succeeded, but detailed issue refresh was rate-limited. Some returned fields may be partial until rate limit resets._'
|
|
482
|
+
: '';
|
|
483
|
+
|
|
416
484
|
return toTextResult(
|
|
417
|
-
`Updated issue ${result.issue.identifier}${suffix}`,
|
|
485
|
+
`Updated issue ${result.issue.identifier}${suffix}${rateLimitNote}`,
|
|
418
486
|
{
|
|
419
487
|
issueId: result.issue.id,
|
|
420
488
|
identifier: result.issue.identifier,
|
|
@@ -422,6 +490,7 @@ export async function executeIssueUpdate(client, params) {
|
|
|
422
490
|
state: result.issue.state,
|
|
423
491
|
priority: result.issue.priority,
|
|
424
492
|
projectMilestone: result.issue.projectMilestone,
|
|
493
|
+
usedRateLimitFallback: !!result.usedRateLimitFallback,
|
|
425
494
|
}
|
|
426
495
|
);
|
|
427
496
|
}
|
|
@@ -529,6 +598,362 @@ export async function executeProjectList(client) {
|
|
|
529
598
|
});
|
|
530
599
|
}
|
|
531
600
|
|
|
601
|
+
export async function executeProjectView(client, params) {
|
|
602
|
+
return withHandlerErrorHandling(async () => {
|
|
603
|
+
const projectRef = ensureNonEmpty(params.project, 'project');
|
|
604
|
+
const project = await fetchProjectDetails(client, projectRef);
|
|
605
|
+
|
|
606
|
+
const lines = [`# Project: ${project.name}`];
|
|
607
|
+
const metaParts = [];
|
|
608
|
+
|
|
609
|
+
if (project.status?.name) metaParts.push(`**Status:** ${project.status.name}`);
|
|
610
|
+
if (project.health) metaParts.push(`**Health:** ${project.health}`);
|
|
611
|
+
if (project.progress !== undefined && project.progress !== null) metaParts.push(`**Progress:** ${project.progress}%`);
|
|
612
|
+
if (project.priority !== undefined && project.priority !== null) metaParts.push(`**Priority:** ${project.priority}`);
|
|
613
|
+
if (project.startDate) metaParts.push(`**Start:** ${project.startDate}`);
|
|
614
|
+
if (project.targetDate) metaParts.push(`**Target:** ${project.targetDate}`);
|
|
615
|
+
|
|
616
|
+
if (metaParts.length > 0) {
|
|
617
|
+
lines.push('');
|
|
618
|
+
lines.push(metaParts.join(' | '));
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const teamLabel = project.teams.length > 0
|
|
622
|
+
? project.teams.map((team) => `\`${team.key}\``).join(', ')
|
|
623
|
+
: 'None';
|
|
624
|
+
const leadLabel = project.lead?.displayName || project.lead?.name || 'Unassigned';
|
|
625
|
+
|
|
626
|
+
lines.push('');
|
|
627
|
+
lines.push(`**Teams:** ${teamLabel}`);
|
|
628
|
+
lines.push(`**Lead:** ${leadLabel}`);
|
|
629
|
+
|
|
630
|
+
if (project.url) {
|
|
631
|
+
lines.push(`**URL:** ${project.url}`);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (project.description) {
|
|
635
|
+
lines.push('');
|
|
636
|
+
lines.push(project.description);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (project.content) {
|
|
640
|
+
lines.push('');
|
|
641
|
+
if (project.description) {
|
|
642
|
+
lines.push('## Content');
|
|
643
|
+
lines.push('');
|
|
644
|
+
}
|
|
645
|
+
lines.push(project.content);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (project.projectMilestones.length > 0) {
|
|
649
|
+
lines.push('');
|
|
650
|
+
lines.push(`## Milestones (${project.projectMilestones.length})`);
|
|
651
|
+
lines.push('');
|
|
652
|
+
|
|
653
|
+
for (const milestone of project.projectMilestones) {
|
|
654
|
+
const progressLabel = milestone.progress !== undefined && milestone.progress !== null
|
|
655
|
+
? `${milestone.progress}%`
|
|
656
|
+
: 'N/A';
|
|
657
|
+
const targetLabel = milestone.targetDate ? ` → ${milestone.targetDate}` : '';
|
|
658
|
+
lines.push(`- **${milestone.name}** _[${milestone.status}]_ (${progressLabel})${targetLabel}`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return toTextResult(lines.join('\n'), {
|
|
663
|
+
projectId: project.id,
|
|
664
|
+
name: project.name,
|
|
665
|
+
status: project.status,
|
|
666
|
+
teamCount: project.teams.length,
|
|
667
|
+
milestoneCount: project.projectMilestones.length,
|
|
668
|
+
url: project.url,
|
|
669
|
+
});
|
|
670
|
+
}, 'executeProjectView');
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export async function executeProjectCreate(client, params) {
|
|
674
|
+
return withHandlerErrorHandling(async () => {
|
|
675
|
+
const name = ensureNonEmpty(params.name, 'name');
|
|
676
|
+
const teamRefs = parseRefList(params.teams ?? params.team);
|
|
677
|
+
|
|
678
|
+
if (teamRefs.length === 0) {
|
|
679
|
+
throw new Error('Missing required field: teams');
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const teams = await Promise.all(teamRefs.map((teamRef) => resolveTeamRef(client, teamRef)));
|
|
683
|
+
|
|
684
|
+
let leadId;
|
|
685
|
+
if (params.lead === 'me') {
|
|
686
|
+
const viewer = await getViewer(client);
|
|
687
|
+
leadId = viewer.id;
|
|
688
|
+
} else if (params.lead) {
|
|
689
|
+
leadId = params.lead;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const project = await createProject(client, {
|
|
693
|
+
name,
|
|
694
|
+
teamIds: teams.map((team) => team.id),
|
|
695
|
+
description: params.description,
|
|
696
|
+
leadId,
|
|
697
|
+
priority: params.priority,
|
|
698
|
+
color: params.color,
|
|
699
|
+
icon: params.icon,
|
|
700
|
+
startDate: params.startDate,
|
|
701
|
+
targetDate: params.targetDate,
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
return toTextResult(
|
|
705
|
+
`Created project **${project.name}** for ${teams.map((team) => team.key).join(', ')}`,
|
|
706
|
+
{
|
|
707
|
+
projectId: project.id,
|
|
708
|
+
name: project.name,
|
|
709
|
+
teams: project.teams,
|
|
710
|
+
status: project.status,
|
|
711
|
+
url: project.url,
|
|
712
|
+
}
|
|
713
|
+
);
|
|
714
|
+
}, 'executeProjectCreate');
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
export async function executeProjectUpdate(client, params) {
|
|
718
|
+
return withHandlerErrorHandling(async () => {
|
|
719
|
+
const projectRef = ensureNonEmpty(params.project, 'project');
|
|
720
|
+
const patch = {
|
|
721
|
+
name: params.name,
|
|
722
|
+
description: params.description,
|
|
723
|
+
priority: params.priority,
|
|
724
|
+
color: params.color,
|
|
725
|
+
icon: params.icon,
|
|
726
|
+
startDate: params.startDate,
|
|
727
|
+
targetDate: params.targetDate,
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
if (params.lead === 'me') {
|
|
731
|
+
const viewer = await getViewer(client);
|
|
732
|
+
patch.leadId = viewer.id;
|
|
733
|
+
} else if (params.lead === 'none') {
|
|
734
|
+
patch.leadId = null;
|
|
735
|
+
} else if (params.lead !== undefined) {
|
|
736
|
+
patch.leadId = params.lead;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (params.teams !== undefined || params.team !== undefined) {
|
|
740
|
+
const teamRefs = parseRefList(params.teams ?? params.team);
|
|
741
|
+
if (teamRefs.length === 0) {
|
|
742
|
+
throw new Error('At least one team is required when updating teams');
|
|
743
|
+
}
|
|
744
|
+
const teams = await Promise.all(teamRefs.map((teamRef) => resolveTeamRef(client, teamRef)));
|
|
745
|
+
patch.teamIds = teams.map((team) => team.id);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const result = await updateProject(client, projectRef, patch);
|
|
749
|
+
|
|
750
|
+
return toTextResult(
|
|
751
|
+
`Updated project **${result.project.name}** (${result.changed.join(', ')})`,
|
|
752
|
+
{
|
|
753
|
+
projectId: result.project.id,
|
|
754
|
+
name: result.project.name,
|
|
755
|
+
changed: result.changed,
|
|
756
|
+
status: result.project.status,
|
|
757
|
+
}
|
|
758
|
+
);
|
|
759
|
+
}, 'executeProjectUpdate');
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
export async function executeProjectDelete(client, params) {
|
|
763
|
+
return withHandlerErrorHandling(async () => {
|
|
764
|
+
const projectRef = ensureNonEmpty(params.project, 'project');
|
|
765
|
+
const result = await deleteProject(client, projectRef);
|
|
766
|
+
|
|
767
|
+
return toTextResult(
|
|
768
|
+
`Deleted project **${result.name}** \`${result.projectId}\``,
|
|
769
|
+
{
|
|
770
|
+
projectId: result.projectId,
|
|
771
|
+
name: result.name,
|
|
772
|
+
success: result.success,
|
|
773
|
+
}
|
|
774
|
+
);
|
|
775
|
+
}, 'executeProjectDelete');
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
export async function executeProjectArchive(client, params) {
|
|
779
|
+
return withHandlerErrorHandling(async () => {
|
|
780
|
+
const projectRef = ensureNonEmpty(params.project, 'project');
|
|
781
|
+
const result = await archiveProject(client, projectRef);
|
|
782
|
+
|
|
783
|
+
return toTextResult(
|
|
784
|
+
`Archived project **${result.name || result.entity?.name || result.projectId}**`,
|
|
785
|
+
{
|
|
786
|
+
projectId: result.projectId,
|
|
787
|
+
name: result.name || result.entity?.name || null,
|
|
788
|
+
success: result.success,
|
|
789
|
+
}
|
|
790
|
+
);
|
|
791
|
+
}, 'executeProjectArchive');
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
export async function executeProjectUnarchive(client, params) {
|
|
795
|
+
return withHandlerErrorHandling(async () => {
|
|
796
|
+
const projectRef = ensureNonEmpty(params.project, 'project');
|
|
797
|
+
const result = await unarchiveProject(client, projectRef);
|
|
798
|
+
|
|
799
|
+
return toTextResult(
|
|
800
|
+
`Unarchived project **${result.project.name}**`,
|
|
801
|
+
{
|
|
802
|
+
projectId: result.project.id,
|
|
803
|
+
name: result.project.name,
|
|
804
|
+
success: result.success,
|
|
805
|
+
}
|
|
806
|
+
);
|
|
807
|
+
}, 'executeProjectUnarchive');
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// ===== PROJECT UPDATE HANDLERS =====
|
|
811
|
+
|
|
812
|
+
export async function executeProjectUpdateList(client, params) {
|
|
813
|
+
return withHandlerErrorHandling(async () => {
|
|
814
|
+
const projectRef = ensureNonEmpty(params.project, 'project');
|
|
815
|
+
const { project, updates } = await fetchProjectUpdates(client, projectRef, {
|
|
816
|
+
limit: params.limit ?? 10,
|
|
817
|
+
includeArchived: params.includeArchived === true,
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
if (updates.length === 0) {
|
|
821
|
+
return toTextResult(`No project updates found for "${project.name}"`, {
|
|
822
|
+
projectId: project.id,
|
|
823
|
+
projectName: project.name,
|
|
824
|
+
updateCount: 0,
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const lines = [`## Project updates for "${project.name}" (${updates.length})`, ''];
|
|
829
|
+
for (const update of updates) {
|
|
830
|
+
const author = update.user?.displayName || update.user?.name || 'Unknown';
|
|
831
|
+
const createdAt = update.createdAt ? String(update.createdAt).slice(0, 10) : 'unknown date';
|
|
832
|
+
const healthLabel = update.health ? ` [${update.health}]` : '';
|
|
833
|
+
const archivedLabel = update.archivedAt ? ' [archived]' : '';
|
|
834
|
+
const preview = update.body.replace(/\s+/g, ' ').trim().slice(0, 120);
|
|
835
|
+
lines.push(`- **${update.id}**${healthLabel}${archivedLabel} by ${author} on ${createdAt}`);
|
|
836
|
+
if (preview) {
|
|
837
|
+
lines.push(` ${preview}${update.body.length > 120 ? '...' : ''}`);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
return toTextResult(lines.join('\n'), {
|
|
842
|
+
projectId: project.id,
|
|
843
|
+
projectName: project.name,
|
|
844
|
+
updateCount: updates.length,
|
|
845
|
+
});
|
|
846
|
+
}, 'executeProjectUpdateList');
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
export async function executeProjectUpdateView(client, params) {
|
|
850
|
+
return withHandlerErrorHandling(async () => {
|
|
851
|
+
const projectUpdateId = ensureNonEmpty(params.projectUpdate, 'projectUpdate');
|
|
852
|
+
const projectUpdate = await fetchProjectUpdateDetails(client, projectUpdateId);
|
|
853
|
+
|
|
854
|
+
const lines = [`# Project update ${projectUpdate.id}`];
|
|
855
|
+
const meta = [];
|
|
856
|
+
if (projectUpdate.project?.name) meta.push(`**Project:** ${projectUpdate.project.name}`);
|
|
857
|
+
if (projectUpdate.health) meta.push(`**Health:** ${projectUpdate.health}`);
|
|
858
|
+
if (projectUpdate.user?.displayName || projectUpdate.user?.name) meta.push(`**Author:** ${projectUpdate.user?.displayName || projectUpdate.user?.name}`);
|
|
859
|
+
if (projectUpdate.createdAt) meta.push(`**Created:** ${String(projectUpdate.createdAt).slice(0, 10)}`);
|
|
860
|
+
if (projectUpdate.archivedAt) meta.push(`**Archived:** ${String(projectUpdate.archivedAt).slice(0, 10)}`);
|
|
861
|
+
if (meta.length > 0) {
|
|
862
|
+
lines.push('');
|
|
863
|
+
lines.push(meta.join(' | '));
|
|
864
|
+
}
|
|
865
|
+
if (projectUpdate.url) {
|
|
866
|
+
lines.push('');
|
|
867
|
+
lines.push(`**URL:** ${projectUpdate.url}`);
|
|
868
|
+
}
|
|
869
|
+
if (projectUpdate.body) {
|
|
870
|
+
lines.push('');
|
|
871
|
+
lines.push(projectUpdate.body);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
return toTextResult(lines.join('\n'), {
|
|
875
|
+
projectUpdateId: projectUpdate.id,
|
|
876
|
+
projectId: projectUpdate.project?.id || null,
|
|
877
|
+
projectName: projectUpdate.project?.name || null,
|
|
878
|
+
health: projectUpdate.health,
|
|
879
|
+
archivedAt: projectUpdate.archivedAt,
|
|
880
|
+
});
|
|
881
|
+
}, 'executeProjectUpdateView');
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
export async function executeProjectUpdateCreate(client, params) {
|
|
885
|
+
return withHandlerErrorHandling(async () => {
|
|
886
|
+
const projectRef = ensureNonEmpty(params.project, 'project');
|
|
887
|
+
const resolved = await resolveProjectRef(client, projectRef);
|
|
888
|
+
const projectUpdate = await createProjectUpdate(client, {
|
|
889
|
+
projectId: resolved.id,
|
|
890
|
+
body: params.body,
|
|
891
|
+
health: params.health,
|
|
892
|
+
isDiffHidden: params.isDiffHidden,
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
return toTextResult(
|
|
896
|
+
`Created project update **${projectUpdate.id}** for "${resolved.name}"`,
|
|
897
|
+
{
|
|
898
|
+
projectUpdateId: projectUpdate.id,
|
|
899
|
+
projectId: resolved.id,
|
|
900
|
+
projectName: resolved.name,
|
|
901
|
+
health: projectUpdate.health,
|
|
902
|
+
}
|
|
903
|
+
);
|
|
904
|
+
}, 'executeProjectUpdateCreate');
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
export async function executeProjectUpdateUpdate(client, params) {
|
|
908
|
+
return withHandlerErrorHandling(async () => {
|
|
909
|
+
const projectUpdateId = ensureNonEmpty(params.projectUpdate, 'projectUpdate');
|
|
910
|
+
const result = await updateProjectUpdate(client, projectUpdateId, {
|
|
911
|
+
body: params.body,
|
|
912
|
+
health: params.health,
|
|
913
|
+
isDiffHidden: params.isDiffHidden,
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
return toTextResult(
|
|
917
|
+
`Updated project update **${result.projectUpdate.id}** (${result.changed.join(', ')})`,
|
|
918
|
+
{
|
|
919
|
+
projectUpdateId: result.projectUpdate.id,
|
|
920
|
+
changed: result.changed,
|
|
921
|
+
health: result.projectUpdate.health,
|
|
922
|
+
}
|
|
923
|
+
);
|
|
924
|
+
}, 'executeProjectUpdateUpdate');
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
export async function executeProjectUpdateArchive(client, params) {
|
|
928
|
+
return withHandlerErrorHandling(async () => {
|
|
929
|
+
const projectUpdateId = ensureNonEmpty(params.projectUpdate, 'projectUpdate');
|
|
930
|
+
const result = await archiveProjectUpdate(client, projectUpdateId);
|
|
931
|
+
|
|
932
|
+
return toTextResult(
|
|
933
|
+
`Archived project update **${projectUpdateId}**`,
|
|
934
|
+
{
|
|
935
|
+
projectUpdateId,
|
|
936
|
+
success: result.success,
|
|
937
|
+
}
|
|
938
|
+
);
|
|
939
|
+
}, 'executeProjectUpdateArchive');
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
export async function executeProjectUpdateUnarchive(client, params) {
|
|
943
|
+
return withHandlerErrorHandling(async () => {
|
|
944
|
+
const projectUpdateId = ensureNonEmpty(params.projectUpdate, 'projectUpdate');
|
|
945
|
+
const result = await unarchiveProjectUpdate(client, projectUpdateId);
|
|
946
|
+
|
|
947
|
+
return toTextResult(
|
|
948
|
+
`Unarchived project update **${result.projectUpdate.id}**`,
|
|
949
|
+
{
|
|
950
|
+
projectUpdateId: result.projectUpdate.id,
|
|
951
|
+
success: result.success,
|
|
952
|
+
}
|
|
953
|
+
);
|
|
954
|
+
}, 'executeProjectUpdateUnarchive');
|
|
955
|
+
}
|
|
956
|
+
|
|
532
957
|
// ===== TEAM HANDLERS =====
|
|
533
958
|
|
|
534
959
|
/**
|
package/src/linear-client.js
CHANGED
|
@@ -28,6 +28,10 @@ function getTrackerKey(apiKey) {
|
|
|
28
28
|
return apiKey || 'default';
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function getTrackerKeyFromClient(client) {
|
|
32
|
+
return client?.__piLinearTrackerKey || client?.apiKey || 'default';
|
|
33
|
+
}
|
|
34
|
+
|
|
31
35
|
function getRequestMetric(trackerKey) {
|
|
32
36
|
let metric = requestMetrics.get(trackerKey);
|
|
33
37
|
if (!metric) {
|
|
@@ -124,7 +128,7 @@ export function markRateLimited(resetAt) {
|
|
|
124
128
|
* @returns {{remaining: number|null, resetAt: number|null, resetTime: string|null}}
|
|
125
129
|
*/
|
|
126
130
|
export function getClientRateLimit(client) {
|
|
127
|
-
const trackerData = rateLimitTracker.get(client
|
|
131
|
+
const trackerData = rateLimitTracker.get(getTrackerKeyFromClient(client));
|
|
128
132
|
if (trackerData) {
|
|
129
133
|
return {
|
|
130
134
|
remaining: trackerData.remaining,
|
|
@@ -140,7 +144,7 @@ export function getClientRateLimit(client) {
|
|
|
140
144
|
* Expose per-client request counters for diagnostics
|
|
141
145
|
*/
|
|
142
146
|
export function getClientRequestMetrics(client) {
|
|
143
|
-
const key = client
|
|
147
|
+
const key = getTrackerKeyFromClient(client);
|
|
144
148
|
const metric = requestMetrics.get(key);
|
|
145
149
|
if (!metric) {
|
|
146
150
|
return {
|
|
@@ -227,6 +231,7 @@ export function createLinearClient(auth) {
|
|
|
227
231
|
const client = new LinearClient(clientConfig);
|
|
228
232
|
|
|
229
233
|
const trackerKey = getTrackerKey(apiKey);
|
|
234
|
+
client.__piLinearTrackerKey = trackerKey;
|
|
230
235
|
if (!rateLimitTracker.has(trackerKey)) {
|
|
231
236
|
rateLimitTracker.set(trackerKey, { remaining: 5000, resetAt: Date.now() + 3600000 });
|
|
232
237
|
}
|