@fink-andreas/pi-linear-tools 0.4.1 → 0.4.2
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 +21 -0
- package/README.md +8 -10
- package/extensions/pi-linear-tools.js +118 -50
- package/index.js +118 -50
- package/package.json +6 -2
- package/src/cli.js +3 -1
- package/src/handlers.js +26 -4
- package/src/linear-client.js +7 -2
- package/src/linear.js +159 -18
- package/FUNCTIONALITY.md +0 -57
- package/POST_RELEASE_CHECKLIST.md +0 -30
- package/RELEASE.md +0 -50
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.4.2 (2026-03-26)
|
|
4
|
+
|
|
5
|
+
Rate-limit resilience and API usage diagnostics release.
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
- Hardened issue update flow when post-mutation refresh is rate-limited
|
|
9
|
+
- Added explicit user-facing note when update succeeded but refresh is partial due to rate limits
|
|
10
|
+
- Fixed per-command API usage metric keying so usage deltas report correctly
|
|
11
|
+
|
|
12
|
+
### Performance / API Usage
|
|
13
|
+
- Added short-lived in-memory caches for viewer, project list, team list, and team workflow states
|
|
14
|
+
- Added direct-ID fast paths for project/team resolution to avoid full list fetches
|
|
15
|
+
|
|
16
|
+
### Diagnostics
|
|
17
|
+
- Added optional per-command usage summary output via `PI_LINEAR_TOOLS_USAGE_SUMMARY=true`
|
|
18
|
+
- Added debug-level per-command request delta logging across issue/project/team/milestone tools
|
|
19
|
+
|
|
20
|
+
### Documentation / Tests
|
|
21
|
+
- Documented new diagnostics flag and cache behavior
|
|
22
|
+
- Added regression tests for rate-limit fallback and API usage caching
|
|
23
|
+
|
|
3
24
|
## v0.4.0 (2026-03-23)
|
|
4
25
|
|
|
5
26
|
Comprehensive error handling and file-first logging release.
|
package/README.md
CHANGED
|
@@ -7,14 +7,13 @@
|
|
|
7
7
|
### As a pi package (recommended)
|
|
8
8
|
```bash
|
|
9
9
|
pi install npm:@fink-andreas/pi-linear-tools
|
|
10
|
-
pi config
|
|
11
10
|
```
|
|
12
|
-
Enable the `pi-linear-tools` extension resource.
|
|
13
11
|
|
|
14
12
|
### As an npm package
|
|
15
13
|
```bash
|
|
16
14
|
npm install -g @fink-andreas/pi-linear-tools
|
|
17
15
|
```
|
|
16
|
+
Use pi-linear-tools as a CLI tool.
|
|
18
17
|
|
|
19
18
|
## Initial configuration
|
|
20
19
|
|
|
@@ -54,10 +53,9 @@ Actions: `list`, `view`, `create`, `update`, `delete`
|
|
|
54
53
|
|
|
55
54
|
## CLI usage
|
|
56
55
|
|
|
57
|
-
If installed globally via npm
|
|
56
|
+
If installed globally via ```npm install -g @fink-andreas/pi-linear-tools```, CLI command ```pi-linear-tools``` is available:
|
|
58
57
|
|
|
59
58
|
```bash
|
|
60
|
-
npm install -g @fink-andreas/pi-linear-tools
|
|
61
59
|
pi-linear-tools --help
|
|
62
60
|
pi-linear-tools config
|
|
63
61
|
pi-linear-tools config --api-key lin_xxx
|
|
@@ -65,12 +63,6 @@ pi-linear-tools config --default-team ENG
|
|
|
65
63
|
pi-linear-tools config --team ENG --project "My Project"
|
|
66
64
|
```
|
|
67
65
|
|
|
68
|
-
If `pi-linear-tools` is not found, run it from the repo:
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
node bin/pi-linear-tools.js --help
|
|
72
|
-
```
|
|
73
|
-
|
|
74
66
|
### Issue commands
|
|
75
67
|
|
|
76
68
|
```bash
|
|
@@ -144,6 +136,12 @@ Settings path:
|
|
|
144
136
|
Environment fallback:
|
|
145
137
|
- `LINEAR_API_KEY` (takes precedence over settings)
|
|
146
138
|
|
|
139
|
+
Debug/diagnostics environment flags:
|
|
140
|
+
- `PI_LINEAR_TOOLS_USAGE_SUMMARY=true` — append per-command Linear API usage summary to tool output markdown and include `details.apiUsage`
|
|
141
|
+
- `LOG_LEVEL=debug` — enable detailed file logging
|
|
142
|
+
- `PI_LINEAR_TOOLS_LOG_TO_CONSOLE=true` — mirror logs to console (normally file-first logging)
|
|
143
|
+
- `PI_LINEAR_TOOLS_LOG_FILE=/custom/path.log` — override log file path
|
|
144
|
+
|
|
147
145
|
## Development
|
|
148
146
|
|
|
149
147
|
```bash
|
|
@@ -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,
|
|
@@ -65,6 +65,7 @@ import { authenticate, getAccessToken, logout } from '../src/auth/index.js';
|
|
|
65
65
|
import { withMilestoneScopeHint } from '../src/error-hints.js';
|
|
66
66
|
|
|
67
67
|
let cachedApiKey = null;
|
|
68
|
+
const INCLUDE_USAGE_SUMMARY = String(process.env.PI_LINEAR_TOOLS_USAGE_SUMMARY || '').toLowerCase() === 'true';
|
|
68
69
|
|
|
69
70
|
async function getLinearAuth() {
|
|
70
71
|
const envKey = process.env.LINEAR_API_KEY;
|
|
@@ -106,6 +107,65 @@ async function createAuthenticatedClient() {
|
|
|
106
107
|
return createLinearClient(await getLinearAuth());
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
async function withRequestUsageLogging(client, toolName, action, operation) {
|
|
111
|
+
const before = getClientRequestMetrics(client);
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const result = await operation();
|
|
115
|
+
const after = getClientRequestMetrics(client);
|
|
116
|
+
|
|
117
|
+
const usageDelta = {
|
|
118
|
+
tool: toolName,
|
|
119
|
+
action,
|
|
120
|
+
requestsDelta: after.total - before.total,
|
|
121
|
+
successDelta: after.success - before.success,
|
|
122
|
+
failedDelta: after.failed - before.failed,
|
|
123
|
+
rateLimitedDelta: after.rateLimited - before.rateLimited,
|
|
124
|
+
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)`,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
debug('[pi-linear-tools] API usage per command', usageDelta);
|
|
128
|
+
|
|
129
|
+
if (!INCLUDE_USAGE_SUMMARY || !result || typeof result !== 'object') {
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const details = (result.details && typeof result.details === 'object') ? result.details : {};
|
|
134
|
+
const content = Array.isArray(result.content)
|
|
135
|
+
? result.content.map((item, idx) => {
|
|
136
|
+
if (idx !== 0 || item?.type !== 'text' || typeof item.text !== 'string') return item;
|
|
137
|
+
return {
|
|
138
|
+
...item,
|
|
139
|
+
text: `${item.text}\n\n_${usageDelta.summary}_`,
|
|
140
|
+
};
|
|
141
|
+
})
|
|
142
|
+
: result.content;
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
...result,
|
|
146
|
+
content,
|
|
147
|
+
details: {
|
|
148
|
+
...details,
|
|
149
|
+
apiUsage: usageDelta,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
} catch (error) {
|
|
153
|
+
const after = getClientRequestMetrics(client);
|
|
154
|
+
|
|
155
|
+
debug('[pi-linear-tools] API usage per command (error)', {
|
|
156
|
+
tool: toolName,
|
|
157
|
+
action,
|
|
158
|
+
requestsDelta: after.total - before.total,
|
|
159
|
+
successDelta: after.success - before.success,
|
|
160
|
+
failedDelta: after.failed - before.failed,
|
|
161
|
+
rateLimitedDelta: after.rateLimited - before.rateLimited,
|
|
162
|
+
error: String(error?.message || error || 'unknown'),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
109
169
|
async function resolveDefaultTeam(projectId) {
|
|
110
170
|
const settings = await loadSettings();
|
|
111
171
|
|
|
@@ -552,28 +612,30 @@ async function registerLinearTools(pi) {
|
|
|
552
612
|
try {
|
|
553
613
|
const client = await createAuthenticatedClient();
|
|
554
614
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
615
|
+
return await withRequestUsageLogging(client, 'linear_issue', params.action, async () => {
|
|
616
|
+
switch (params.action) {
|
|
617
|
+
case 'list':
|
|
618
|
+
return await executeIssueList(client, params);
|
|
619
|
+
case 'view':
|
|
620
|
+
return await executeIssueView(client, params);
|
|
621
|
+
case 'create':
|
|
622
|
+
return await executeIssueCreate(client, params, { resolveDefaultTeam });
|
|
623
|
+
case 'update':
|
|
624
|
+
return await executeIssueUpdate(client, params);
|
|
625
|
+
case 'comment':
|
|
626
|
+
return await executeIssueComment(client, params);
|
|
627
|
+
case 'start':
|
|
628
|
+
return await executeIssueStart(client, params, {
|
|
629
|
+
gitExecutor: async (branchName, fromRef, onBranchExists) => {
|
|
630
|
+
return startGitBranchForIssue(pi, branchName, fromRef, onBranchExists);
|
|
631
|
+
},
|
|
632
|
+
});
|
|
633
|
+
case 'delete':
|
|
634
|
+
return await executeIssueDelete(client, params);
|
|
635
|
+
default:
|
|
636
|
+
throw new Error(`Unknown action: ${params.action}`);
|
|
637
|
+
}
|
|
638
|
+
});
|
|
577
639
|
} catch (error) {
|
|
578
640
|
// Comprehensive error handling - catch ALL errors including SDK's RatelimitedLinearError
|
|
579
641
|
const errorType = error?.type || '';
|
|
@@ -648,12 +710,14 @@ async function registerLinearTools(pi) {
|
|
|
648
710
|
try {
|
|
649
711
|
const client = await createAuthenticatedClient();
|
|
650
712
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
713
|
+
return await withRequestUsageLogging(client, 'linear_project', params.action, async () => {
|
|
714
|
+
switch (params.action) {
|
|
715
|
+
case 'list':
|
|
716
|
+
return await executeProjectList(client);
|
|
717
|
+
default:
|
|
718
|
+
throw new Error(`Unknown action: ${params.action}`);
|
|
719
|
+
}
|
|
720
|
+
});
|
|
657
721
|
} catch (error) {
|
|
658
722
|
// Comprehensive error handling - catch ALL errors
|
|
659
723
|
const errorType = error?.type || '';
|
|
@@ -696,12 +760,14 @@ async function registerLinearTools(pi) {
|
|
|
696
760
|
try {
|
|
697
761
|
const client = await createAuthenticatedClient();
|
|
698
762
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
763
|
+
return await withRequestUsageLogging(client, 'linear_team', params.action, async () => {
|
|
764
|
+
switch (params.action) {
|
|
765
|
+
case 'list':
|
|
766
|
+
return await executeTeamList(client);
|
|
767
|
+
default:
|
|
768
|
+
throw new Error(`Unknown action: ${params.action}`);
|
|
769
|
+
}
|
|
770
|
+
});
|
|
705
771
|
} catch (error) {
|
|
706
772
|
// Comprehensive error handling - catch ALL errors
|
|
707
773
|
const errorType = error?.type || '';
|
|
@@ -765,20 +831,22 @@ async function registerLinearTools(pi) {
|
|
|
765
831
|
try {
|
|
766
832
|
const client = await createAuthenticatedClient();
|
|
767
833
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
834
|
+
return await withRequestUsageLogging(client, 'linear_milestone', params.action, async () => {
|
|
835
|
+
switch (params.action) {
|
|
836
|
+
case 'list':
|
|
837
|
+
return await executeMilestoneList(client, params);
|
|
838
|
+
case 'view':
|
|
839
|
+
return await executeMilestoneView(client, params);
|
|
840
|
+
case 'create':
|
|
841
|
+
return await executeMilestoneCreate(client, params);
|
|
842
|
+
case 'update':
|
|
843
|
+
return await executeMilestoneUpdate(client, params);
|
|
844
|
+
case 'delete':
|
|
845
|
+
return await executeMilestoneDelete(client, params);
|
|
846
|
+
default:
|
|
847
|
+
throw new Error(`Unknown action: ${params.action}`);
|
|
848
|
+
}
|
|
849
|
+
});
|
|
782
850
|
} catch (error) {
|
|
783
851
|
// Apply milestone-specific hint, then wrap with operation context
|
|
784
852
|
const hintError = withMilestoneScopeHint(error);
|
package/index.js
CHANGED
|
@@ -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,
|
|
@@ -65,6 +65,7 @@ import { authenticate, getAccessToken, logout } from './src/auth/index.js';
|
|
|
65
65
|
import { withMilestoneScopeHint } from './src/error-hints.js';
|
|
66
66
|
|
|
67
67
|
let cachedApiKey = null;
|
|
68
|
+
const INCLUDE_USAGE_SUMMARY = String(process.env.PI_LINEAR_TOOLS_USAGE_SUMMARY || '').toLowerCase() === 'true';
|
|
68
69
|
|
|
69
70
|
async function getLinearAuth() {
|
|
70
71
|
const envKey = process.env.LINEAR_API_KEY;
|
|
@@ -106,6 +107,65 @@ async function createAuthenticatedClient() {
|
|
|
106
107
|
return createLinearClient(await getLinearAuth());
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
async function withRequestUsageLogging(client, toolName, action, operation) {
|
|
111
|
+
const before = getClientRequestMetrics(client);
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const result = await operation();
|
|
115
|
+
const after = getClientRequestMetrics(client);
|
|
116
|
+
|
|
117
|
+
const usageDelta = {
|
|
118
|
+
tool: toolName,
|
|
119
|
+
action,
|
|
120
|
+
requestsDelta: after.total - before.total,
|
|
121
|
+
successDelta: after.success - before.success,
|
|
122
|
+
failedDelta: after.failed - before.failed,
|
|
123
|
+
rateLimitedDelta: after.rateLimited - before.rateLimited,
|
|
124
|
+
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)`,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
debug('[pi-linear-tools] API usage per command', usageDelta);
|
|
128
|
+
|
|
129
|
+
if (!INCLUDE_USAGE_SUMMARY || !result || typeof result !== 'object') {
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const details = (result.details && typeof result.details === 'object') ? result.details : {};
|
|
134
|
+
const content = Array.isArray(result.content)
|
|
135
|
+
? result.content.map((item, idx) => {
|
|
136
|
+
if (idx !== 0 || item?.type !== 'text' || typeof item.text !== 'string') return item;
|
|
137
|
+
return {
|
|
138
|
+
...item,
|
|
139
|
+
text: `${item.text}\n\n_${usageDelta.summary}_`,
|
|
140
|
+
};
|
|
141
|
+
})
|
|
142
|
+
: result.content;
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
...result,
|
|
146
|
+
content,
|
|
147
|
+
details: {
|
|
148
|
+
...details,
|
|
149
|
+
apiUsage: usageDelta,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
} catch (error) {
|
|
153
|
+
const after = getClientRequestMetrics(client);
|
|
154
|
+
|
|
155
|
+
debug('[pi-linear-tools] API usage per command (error)', {
|
|
156
|
+
tool: toolName,
|
|
157
|
+
action,
|
|
158
|
+
requestsDelta: after.total - before.total,
|
|
159
|
+
successDelta: after.success - before.success,
|
|
160
|
+
failedDelta: after.failed - before.failed,
|
|
161
|
+
rateLimitedDelta: after.rateLimited - before.rateLimited,
|
|
162
|
+
error: String(error?.message || error || 'unknown'),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
109
169
|
async function resolveDefaultTeam(projectId) {
|
|
110
170
|
const settings = await loadSettings();
|
|
111
171
|
|
|
@@ -552,28 +612,30 @@ async function registerLinearTools(pi) {
|
|
|
552
612
|
try {
|
|
553
613
|
const client = await createAuthenticatedClient();
|
|
554
614
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
615
|
+
return await withRequestUsageLogging(client, 'linear_issue', params.action, async () => {
|
|
616
|
+
switch (params.action) {
|
|
617
|
+
case 'list':
|
|
618
|
+
return await executeIssueList(client, params);
|
|
619
|
+
case 'view':
|
|
620
|
+
return await executeIssueView(client, params);
|
|
621
|
+
case 'create':
|
|
622
|
+
return await executeIssueCreate(client, params, { resolveDefaultTeam });
|
|
623
|
+
case 'update':
|
|
624
|
+
return await executeIssueUpdate(client, params);
|
|
625
|
+
case 'comment':
|
|
626
|
+
return await executeIssueComment(client, params);
|
|
627
|
+
case 'start':
|
|
628
|
+
return await executeIssueStart(client, params, {
|
|
629
|
+
gitExecutor: async (branchName, fromRef, onBranchExists) => {
|
|
630
|
+
return startGitBranchForIssue(pi, branchName, fromRef, onBranchExists);
|
|
631
|
+
},
|
|
632
|
+
});
|
|
633
|
+
case 'delete':
|
|
634
|
+
return await executeIssueDelete(client, params);
|
|
635
|
+
default:
|
|
636
|
+
throw new Error(`Unknown action: ${params.action}`);
|
|
637
|
+
}
|
|
638
|
+
});
|
|
577
639
|
} catch (error) {
|
|
578
640
|
// Comprehensive error handling - catch ALL errors including SDK's RatelimitedLinearError
|
|
579
641
|
const errorType = error?.type || '';
|
|
@@ -648,12 +710,14 @@ async function registerLinearTools(pi) {
|
|
|
648
710
|
try {
|
|
649
711
|
const client = await createAuthenticatedClient();
|
|
650
712
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
713
|
+
return await withRequestUsageLogging(client, 'linear_project', params.action, async () => {
|
|
714
|
+
switch (params.action) {
|
|
715
|
+
case 'list':
|
|
716
|
+
return await executeProjectList(client);
|
|
717
|
+
default:
|
|
718
|
+
throw new Error(`Unknown action: ${params.action}`);
|
|
719
|
+
}
|
|
720
|
+
});
|
|
657
721
|
} catch (error) {
|
|
658
722
|
// Comprehensive error handling - catch ALL errors
|
|
659
723
|
const errorType = error?.type || '';
|
|
@@ -696,12 +760,14 @@ async function registerLinearTools(pi) {
|
|
|
696
760
|
try {
|
|
697
761
|
const client = await createAuthenticatedClient();
|
|
698
762
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
763
|
+
return await withRequestUsageLogging(client, 'linear_team', params.action, async () => {
|
|
764
|
+
switch (params.action) {
|
|
765
|
+
case 'list':
|
|
766
|
+
return await executeTeamList(client);
|
|
767
|
+
default:
|
|
768
|
+
throw new Error(`Unknown action: ${params.action}`);
|
|
769
|
+
}
|
|
770
|
+
});
|
|
705
771
|
} catch (error) {
|
|
706
772
|
// Comprehensive error handling - catch ALL errors
|
|
707
773
|
const errorType = error?.type || '';
|
|
@@ -765,20 +831,22 @@ async function registerLinearTools(pi) {
|
|
|
765
831
|
try {
|
|
766
832
|
const client = await createAuthenticatedClient();
|
|
767
833
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
834
|
+
return await withRequestUsageLogging(client, 'linear_milestone', params.action, async () => {
|
|
835
|
+
switch (params.action) {
|
|
836
|
+
case 'list':
|
|
837
|
+
return await executeMilestoneList(client, params);
|
|
838
|
+
case 'view':
|
|
839
|
+
return await executeMilestoneView(client, params);
|
|
840
|
+
case 'create':
|
|
841
|
+
return await executeMilestoneCreate(client, params);
|
|
842
|
+
case 'update':
|
|
843
|
+
return await executeMilestoneUpdate(client, params);
|
|
844
|
+
case 'delete':
|
|
845
|
+
return await executeMilestoneDelete(client, params);
|
|
846
|
+
default:
|
|
847
|
+
throw new Error(`Unknown action: ${params.action}`);
|
|
848
|
+
}
|
|
849
|
+
});
|
|
782
850
|
} catch (error) {
|
|
783
851
|
// Apply milestone-specific hint, then wrap with operation context
|
|
784
852
|
const hintError = withMilestoneScopeHint(error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fink-andreas/pi-linear-tools",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Pi extension with Linear SDK tools and configuration commands",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
29
|
"start": "node index.js",
|
|
30
|
-
"test": "node tests/test-package-manifest.js && node tests/test-extension-registration.js && node tests/test-settings.js && node tests/test-assignee-update.js && node tests/test-full-assignee-flow.js && node tests/test-branch-param.js",
|
|
30
|
+
"test": "node tests/test-package-manifest.js && node tests/test-extension-registration.js && node tests/test-settings.js && node tests/test-api-usage-caching.js && node tests/test-assignee-update.js && node tests/test-rate-limit-fallback-update.js && node tests/test-full-assignee-flow.js && node tests/test-branch-param.js && node tests/test-team-filter.js",
|
|
31
31
|
"dev:sync-local-extension": "node scripts/dev-sync-local-extension.mjs",
|
|
32
32
|
"release:check": "npm test && npm pack --dry-run"
|
|
33
33
|
},
|
|
@@ -44,6 +44,10 @@
|
|
|
44
44
|
]
|
|
45
45
|
},
|
|
46
46
|
"license": "MIT",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "https://github.com/fink-andreas/pi-linear-tools.git"
|
|
50
|
+
},
|
|
47
51
|
"dependencies": {
|
|
48
52
|
"@linear/sdk": "^75.0.0",
|
|
49
53
|
"@github/keytar": "^7.10.6"
|
package/src/cli.js
CHANGED
|
@@ -149,7 +149,7 @@ Auth Actions:
|
|
|
149
149
|
status Show current authentication status
|
|
150
150
|
|
|
151
151
|
Issue Actions:
|
|
152
|
-
list [--project X] [--states X,Y] [--assignee me|all] [--limit N]
|
|
152
|
+
list [--project X] [--states X,Y] [--assignee me|all] [--team X] [--limit N]
|
|
153
153
|
view <issue> [--no-comments]
|
|
154
154
|
create --title X [--team X] [--project X] [--description X] [--priority 0-4] [--assignee me|ID]
|
|
155
155
|
update <issue> [--title X] [--description X] [--state X] [--priority 0-4]
|
|
@@ -217,6 +217,7 @@ List Options:
|
|
|
217
217
|
--project X Project name or ID (default: current directory name)
|
|
218
218
|
--states X,Y Filter by state names (comma-separated)
|
|
219
219
|
--assignee X Filter by assignee: "me" or "all"
|
|
220
|
+
--team X Filter by team key (e.g., ENG) or ID
|
|
220
221
|
--limit N Max results (default: 50)
|
|
221
222
|
|
|
222
223
|
View Options:
|
|
@@ -531,6 +532,7 @@ async function handleIssueList(args) {
|
|
|
531
532
|
project: readFlag(args, '--project'),
|
|
532
533
|
states: parseArrayValue(readFlag(args, '--states')),
|
|
533
534
|
assignee: readFlag(args, '--assignee'),
|
|
535
|
+
team: readFlag(args, '--team'),
|
|
534
536
|
limit: parseNumber(readFlag(args, '--limit')),
|
|
535
537
|
};
|
|
536
538
|
|
package/src/handlers.js
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
deleteProjectMilestone,
|
|
28
28
|
deleteIssue,
|
|
29
29
|
withHandlerErrorHandling,
|
|
30
|
+
getViewer,
|
|
30
31
|
} from './linear.js';
|
|
31
32
|
import { debug } from './logger.js';
|
|
32
33
|
|
|
@@ -130,6 +131,14 @@ async function startGitBranch(branchName, fromRef = 'HEAD', onBranchExists = 'sw
|
|
|
130
131
|
|
|
131
132
|
/**
|
|
132
133
|
* List issues in a project
|
|
134
|
+
* @param {LinearClient} client - Linear SDK client
|
|
135
|
+
* @param {Object} params - Parameters
|
|
136
|
+
* @param {string} [params.project] - Project name or ID
|
|
137
|
+
* @param {string[]} [params.states] - State names to filter by
|
|
138
|
+
* @param {string} [params.assignee] - "me" or "all" for assignee filtering
|
|
139
|
+
* @param {string} [params.team] - Team key or ID to filter by
|
|
140
|
+
* @param {number} [params.limit] - Maximum results (default: 20)
|
|
141
|
+
* @returns {Promise<{content: Array, details: Object}>}
|
|
133
142
|
*/
|
|
134
143
|
export async function executeIssueList(client, params) {
|
|
135
144
|
return withHandlerErrorHandling(async () => {
|
|
@@ -142,12 +151,20 @@ export async function executeIssueList(client, params) {
|
|
|
142
151
|
|
|
143
152
|
let assigneeId = null;
|
|
144
153
|
if (params.assignee === 'me') {
|
|
145
|
-
const viewer = await client
|
|
154
|
+
const viewer = await getViewer(client);
|
|
146
155
|
assigneeId = viewer.id;
|
|
147
156
|
}
|
|
148
157
|
|
|
158
|
+
// Resolve team if provided
|
|
159
|
+
let teamId = null;
|
|
160
|
+
if (params.team) {
|
|
161
|
+
const team = await resolveTeamRef(client, params.team);
|
|
162
|
+
teamId = team.id;
|
|
163
|
+
}
|
|
164
|
+
|
|
149
165
|
const { issues, truncated } = await fetchIssuesByProject(client, resolved.id, params.states || null, {
|
|
150
166
|
assigneeId,
|
|
167
|
+
teamId,
|
|
151
168
|
limit: params.limit || 20,
|
|
152
169
|
});
|
|
153
170
|
|
|
@@ -260,7 +277,7 @@ export async function executeIssueCreate(client, params, options = {}) {
|
|
|
260
277
|
}
|
|
261
278
|
|
|
262
279
|
if (params.assignee === 'me') {
|
|
263
|
-
const viewer = await client
|
|
280
|
+
const viewer = await getViewer(client);
|
|
264
281
|
createInput.assigneeId = viewer.id;
|
|
265
282
|
} else if (params.assignee) {
|
|
266
283
|
createInput.assigneeId = params.assignee;
|
|
@@ -358,7 +375,7 @@ export async function executeIssueUpdate(client, params) {
|
|
|
358
375
|
|
|
359
376
|
// Handle assignee parameter
|
|
360
377
|
if (params.assignee === 'me') {
|
|
361
|
-
const viewer = await client
|
|
378
|
+
const viewer = await getViewer(client);
|
|
362
379
|
updatePatch.assigneeId = viewer.id;
|
|
363
380
|
} else if (params.assignee) {
|
|
364
381
|
updatePatch.assigneeId = params.assignee;
|
|
@@ -413,8 +430,12 @@ export async function executeIssueUpdate(client, params) {
|
|
|
413
430
|
? ` (${changeSummaryParts.join(', ')})`
|
|
414
431
|
: '';
|
|
415
432
|
|
|
433
|
+
const rateLimitNote = result.usedRateLimitFallback
|
|
434
|
+
? '\n\n_Note: update succeeded, but detailed issue refresh was rate-limited. Some returned fields may be partial until rate limit resets._'
|
|
435
|
+
: '';
|
|
436
|
+
|
|
416
437
|
return toTextResult(
|
|
417
|
-
`Updated issue ${result.issue.identifier}${suffix}`,
|
|
438
|
+
`Updated issue ${result.issue.identifier}${suffix}${rateLimitNote}`,
|
|
418
439
|
{
|
|
419
440
|
issueId: result.issue.id,
|
|
420
441
|
identifier: result.issue.identifier,
|
|
@@ -422,6 +443,7 @@ export async function executeIssueUpdate(client, params) {
|
|
|
422
443
|
state: result.issue.state,
|
|
423
444
|
priority: result.issue.priority,
|
|
424
445
|
projectMilestone: result.issue.projectMilestone,
|
|
446
|
+
usedRateLimitFallback: !!result.usedRateLimitFallback,
|
|
425
447
|
}
|
|
426
448
|
);
|
|
427
449
|
}
|
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
|
}
|
package/src/linear.js
CHANGED
|
@@ -7,6 +7,39 @@
|
|
|
7
7
|
|
|
8
8
|
import { warn, info, debug } from './logger.js';
|
|
9
9
|
|
|
10
|
+
const CACHE_TTL_MS = {
|
|
11
|
+
viewer: 30_000,
|
|
12
|
+
projects: 60_000,
|
|
13
|
+
teams: 60_000,
|
|
14
|
+
teamStates: 60_000,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const viewerCache = new Map();
|
|
18
|
+
const projectsCache = new Map();
|
|
19
|
+
const teamsCache = new Map();
|
|
20
|
+
const teamStatesCache = new Map();
|
|
21
|
+
|
|
22
|
+
function getClientCacheKey(client) {
|
|
23
|
+
return client?.apiKey || 'default';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getCache(map, key) {
|
|
27
|
+
const entry = map.get(key);
|
|
28
|
+
if (!entry) return null;
|
|
29
|
+
if (Date.now() > entry.expiresAt) {
|
|
30
|
+
map.delete(key);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return entry.value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function setCache(map, key, value, ttlMs) {
|
|
37
|
+
map.set(key, {
|
|
38
|
+
value,
|
|
39
|
+
expiresAt: Date.now() + ttlMs,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
10
43
|
// ===== OPTIMIZED GRAPHQL QUERIES =====
|
|
11
44
|
// These queries fetch relations upfront to avoid N+1 API calls
|
|
12
45
|
|
|
@@ -510,15 +543,24 @@ function normalizeIssueRefList(value) {
|
|
|
510
543
|
*/
|
|
511
544
|
export async function fetchViewer(client) {
|
|
512
545
|
return withLinearErrorHandling(async () => {
|
|
546
|
+
const cacheKey = getClientCacheKey(client);
|
|
547
|
+
const cached = getCache(viewerCache, cacheKey);
|
|
548
|
+
if (cached) return cached;
|
|
549
|
+
|
|
513
550
|
const viewer = await client.viewer;
|
|
514
|
-
|
|
551
|
+
const result = {
|
|
515
552
|
id: viewer.id,
|
|
516
553
|
name: viewer.name,
|
|
517
554
|
displayName: viewer.displayName,
|
|
518
555
|
};
|
|
556
|
+
|
|
557
|
+
setCache(viewerCache, cacheKey, result, CACHE_TTL_MS.viewer);
|
|
558
|
+
return result;
|
|
519
559
|
}, 'fetchViewer');
|
|
520
560
|
}
|
|
521
561
|
|
|
562
|
+
export const getViewer = fetchViewer;
|
|
563
|
+
|
|
522
564
|
/**
|
|
523
565
|
* Fetch issues in specific states, optionally filtered by assignee
|
|
524
566
|
* OPTIMIZED: Uses rawRequest with custom GraphQL to fetch all relations in ONE request
|
|
@@ -580,12 +622,13 @@ export async function fetchIssues(client, assigneeId, openStates, limit) {
|
|
|
580
622
|
* @param {Array<string>|null} states - List of state names to include (null = all states)
|
|
581
623
|
* @param {Object} options
|
|
582
624
|
* @param {string|null} options.assigneeId - Assignee ID to filter by (null = all assignees)
|
|
625
|
+
* @param {string|null} options.teamId - Team ID to filter by (null = all teams)
|
|
583
626
|
* @param {number} options.limit - Maximum number of issues to fetch
|
|
584
627
|
* @returns {Promise<{issues: Array, truncated: boolean}>}
|
|
585
628
|
*/
|
|
586
629
|
export async function fetchIssuesByProject(client, projectId, states, options = {}) {
|
|
587
630
|
return withLinearErrorHandling(async () => {
|
|
588
|
-
const { assigneeId = null, limit = 20 } = options;
|
|
631
|
+
const { assigneeId = null, teamId = null, limit = 20 } = options;
|
|
589
632
|
|
|
590
633
|
const filter = {
|
|
591
634
|
project: { id: { eq: projectId } },
|
|
@@ -599,6 +642,10 @@ export async function fetchIssuesByProject(client, projectId, states, options =
|
|
|
599
642
|
filter.assignee = { id: { eq: assigneeId } };
|
|
600
643
|
}
|
|
601
644
|
|
|
645
|
+
if (teamId) {
|
|
646
|
+
filter.team = { id: { eq: teamId } };
|
|
647
|
+
}
|
|
648
|
+
|
|
602
649
|
// Use optimized rawRequest to fetch issues with ALL relations in ONE request
|
|
603
650
|
// This eliminates the N+1 problem where each issue triggered 5 additional API calls
|
|
604
651
|
const { data } = await executeOptimizedQuery(client, ISSUES_WITH_RELATIONS_QUERY, {
|
|
@@ -643,6 +690,10 @@ export async function fetchIssuesByProject(client, projectId, states, options =
|
|
|
643
690
|
*/
|
|
644
691
|
export async function fetchProjects(client) {
|
|
645
692
|
return withLinearErrorHandling(async () => {
|
|
693
|
+
const cacheKey = getClientCacheKey(client);
|
|
694
|
+
const cached = getCache(projectsCache, cacheKey);
|
|
695
|
+
if (cached) return cached;
|
|
696
|
+
|
|
646
697
|
const result = await client.projects();
|
|
647
698
|
const nodes = result.nodes ?? [];
|
|
648
699
|
|
|
@@ -651,7 +702,9 @@ export async function fetchProjects(client) {
|
|
|
651
702
|
projects: nodes.map((p) => ({ id: p.id, name: p.name })),
|
|
652
703
|
});
|
|
653
704
|
|
|
654
|
-
|
|
705
|
+
const projects = nodes.map(p => ({ id: p.id, name: p.name }));
|
|
706
|
+
setCache(projectsCache, cacheKey, projects, CACHE_TTL_MS.projects);
|
|
707
|
+
return projects;
|
|
655
708
|
}, 'fetchProjects');
|
|
656
709
|
}
|
|
657
710
|
|
|
@@ -687,6 +740,10 @@ export async function fetchWorkspaces(client) {
|
|
|
687
740
|
*/
|
|
688
741
|
export async function fetchTeams(client) {
|
|
689
742
|
return withLinearErrorHandling(async () => {
|
|
743
|
+
const cacheKey = getClientCacheKey(client);
|
|
744
|
+
const cached = getCache(teamsCache, cacheKey);
|
|
745
|
+
if (cached) return cached;
|
|
746
|
+
|
|
690
747
|
const result = await client.teams();
|
|
691
748
|
const nodes = result.nodes ?? [];
|
|
692
749
|
|
|
@@ -695,7 +752,9 @@ export async function fetchTeams(client) {
|
|
|
695
752
|
teams: nodes.map((t) => ({ id: t.id, key: t.key, name: t.name })),
|
|
696
753
|
});
|
|
697
754
|
|
|
698
|
-
|
|
755
|
+
const teams = nodes.map(t => ({ id: t.id, key: t.key, name: t.name }));
|
|
756
|
+
setCache(teamsCache, cacheKey, teams, CACHE_TTL_MS.teams);
|
|
757
|
+
return teams;
|
|
699
758
|
}, 'fetchTeams');
|
|
700
759
|
}
|
|
701
760
|
|
|
@@ -711,10 +770,17 @@ export async function resolveTeamRef(client, teamRef) {
|
|
|
711
770
|
throw new Error('Missing team reference');
|
|
712
771
|
}
|
|
713
772
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
// If it looks like a Linear ID (UUID), try direct lookup first
|
|
773
|
+
// If it looks like a Linear ID (UUID), try direct lookup first (cheap)
|
|
717
774
|
if (isLinearId(ref)) {
|
|
775
|
+
try {
|
|
776
|
+
const direct = await client.team(ref);
|
|
777
|
+
if (direct) {
|
|
778
|
+
return { id: direct.id, key: direct.key, name: direct.name };
|
|
779
|
+
}
|
|
780
|
+
} catch {
|
|
781
|
+
// fall back to cached/full-team lookup below
|
|
782
|
+
}
|
|
783
|
+
|
|
718
784
|
const byId = teams.find((t) => t.id === ref);
|
|
719
785
|
if (byId) {
|
|
720
786
|
return byId;
|
|
@@ -722,6 +788,8 @@ export async function resolveTeamRef(client, teamRef) {
|
|
|
722
788
|
throw new Error(`Team not found with ID: ${ref}`);
|
|
723
789
|
}
|
|
724
790
|
|
|
791
|
+
const teams = await fetchTeams(client);
|
|
792
|
+
|
|
725
793
|
// Try exact key match (e.g., "ENG")
|
|
726
794
|
const byKey = teams.find((t) => t.key === ref);
|
|
727
795
|
if (byKey) {
|
|
@@ -778,17 +846,24 @@ export async function resolveIssue(client, issueRef) {
|
|
|
778
846
|
*/
|
|
779
847
|
export async function getTeamWorkflowStates(client, teamRef) {
|
|
780
848
|
return withLinearErrorHandling(async () => {
|
|
849
|
+
const cacheKey = `${getClientCacheKey(client)}::${teamRef}`;
|
|
850
|
+
const cached = getCache(teamStatesCache, cacheKey);
|
|
851
|
+
if (cached) return cached;
|
|
852
|
+
|
|
781
853
|
const team = await client.team(teamRef);
|
|
782
854
|
if (!team) {
|
|
783
855
|
throw new Error(`Team not found: ${teamRef}`);
|
|
784
856
|
}
|
|
785
857
|
|
|
786
858
|
const states = await team.states();
|
|
787
|
-
|
|
859
|
+
const mapped = (states.nodes || []).map(s => ({
|
|
788
860
|
id: s.id,
|
|
789
861
|
name: s.name,
|
|
790
862
|
type: s.type,
|
|
791
863
|
}));
|
|
864
|
+
|
|
865
|
+
setCache(teamStatesCache, cacheKey, mapped, CACHE_TTL_MS.teamStates);
|
|
866
|
+
return mapped;
|
|
792
867
|
}, 'getTeamWorkflowStates');
|
|
793
868
|
}
|
|
794
869
|
|
|
@@ -804,17 +879,27 @@ export async function resolveProjectRef(client, projectRef) {
|
|
|
804
879
|
throw new Error('Missing project reference');
|
|
805
880
|
}
|
|
806
881
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
// If it looks like a Linear ID (UUID), try direct lookup first
|
|
882
|
+
// If it looks like a Linear ID (UUID), try direct lookup first (cheap)
|
|
810
883
|
if (isLinearId(ref)) {
|
|
811
|
-
|
|
884
|
+
try {
|
|
885
|
+
const direct = await client.project(ref);
|
|
886
|
+
if (direct) {
|
|
887
|
+
return { id: direct.id, name: direct.name };
|
|
888
|
+
}
|
|
889
|
+
} catch {
|
|
890
|
+
// fall back to cached/full-project lookup below
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const projectsById = await fetchProjects(client);
|
|
894
|
+
const byId = projectsById.find((p) => p.id === ref);
|
|
812
895
|
if (byId) {
|
|
813
896
|
return byId;
|
|
814
897
|
}
|
|
815
898
|
throw new Error(`Project not found with ID: ${ref}`);
|
|
816
899
|
}
|
|
817
900
|
|
|
901
|
+
const projects = await fetchProjects(client);
|
|
902
|
+
|
|
818
903
|
// Try exact name match
|
|
819
904
|
const exactName = projects.find((p) => p.name === ref);
|
|
820
905
|
if (exactName) {
|
|
@@ -1114,6 +1199,47 @@ export async function addIssueComment(client, issueRef, body, parentCommentId) {
|
|
|
1114
1199
|
* @param {Object} patch - Fields to update
|
|
1115
1200
|
* @returns {Promise<{issue: Object, changed: Array<string>}>}
|
|
1116
1201
|
*/
|
|
1202
|
+
function buildFallbackUpdatedIssue(targetIssue, updateInput) {
|
|
1203
|
+
const fallback = { ...(targetIssue || {}) };
|
|
1204
|
+
|
|
1205
|
+
if (Object.prototype.hasOwnProperty.call(updateInput, 'title')) {
|
|
1206
|
+
fallback.title = updateInput.title;
|
|
1207
|
+
}
|
|
1208
|
+
if (Object.prototype.hasOwnProperty.call(updateInput, 'description')) {
|
|
1209
|
+
fallback.description = updateInput.description;
|
|
1210
|
+
}
|
|
1211
|
+
if (Object.prototype.hasOwnProperty.call(updateInput, 'priority')) {
|
|
1212
|
+
fallback.priority = updateInput.priority;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
if (Object.prototype.hasOwnProperty.call(updateInput, 'assigneeId')) {
|
|
1216
|
+
const assigneeId = updateInput.assigneeId;
|
|
1217
|
+
if (assigneeId === null) {
|
|
1218
|
+
fallback.assignee = null;
|
|
1219
|
+
} else if (typeof assigneeId === 'string' && assigneeId.trim()) {
|
|
1220
|
+
fallback.assignee = {
|
|
1221
|
+
id: assigneeId,
|
|
1222
|
+
name: fallback.assignee?.name || 'Unknown',
|
|
1223
|
+
displayName: fallback.assignee?.displayName || 'Unknown',
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
if (Object.prototype.hasOwnProperty.call(updateInput, 'projectMilestoneId')) {
|
|
1229
|
+
const milestoneId = updateInput.projectMilestoneId;
|
|
1230
|
+
if (milestoneId === null) {
|
|
1231
|
+
fallback.projectMilestone = null;
|
|
1232
|
+
} else if (typeof milestoneId === 'string' && milestoneId.trim()) {
|
|
1233
|
+
fallback.projectMilestone = {
|
|
1234
|
+
id: milestoneId,
|
|
1235
|
+
name: fallback.projectMilestone?.name || 'Unknown',
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
return fallback;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1117
1243
|
export async function updateIssue(client, issueRef, patch = {}) {
|
|
1118
1244
|
return withLinearErrorHandling(async () => {
|
|
1119
1245
|
const targetIssue = await resolveIssue(client, issueRef);
|
|
@@ -1275,14 +1401,28 @@ export async function updateIssue(client, issueRef, patch = {}) {
|
|
|
1275
1401
|
}
|
|
1276
1402
|
|
|
1277
1403
|
// Prefer official data path: refetch the issue after successful mutation.
|
|
1278
|
-
|
|
1404
|
+
// If this extra fetch is rate-limited, keep operation successful and return fallback data.
|
|
1405
|
+
let updatedIssue = null;
|
|
1406
|
+
let usedRateLimitFallback = false;
|
|
1279
1407
|
try {
|
|
1280
|
-
updatedSdkIssue = await client.issue(targetIssue.id);
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1408
|
+
const updatedSdkIssue = await client.issue(targetIssue.id);
|
|
1409
|
+
updatedIssue = await transformIssue(updatedSdkIssue);
|
|
1410
|
+
} catch (refreshError) {
|
|
1411
|
+
const refreshMessage = String(refreshError?.message || refreshError || 'unknown');
|
|
1412
|
+
const isRefreshRateLimited = refreshError?.type === 'Ratelimited' || refreshMessage.toLowerCase().includes('rate limit');
|
|
1413
|
+
|
|
1414
|
+
if (!isRefreshRateLimited) {
|
|
1415
|
+
throw refreshError;
|
|
1416
|
+
}
|
|
1284
1417
|
|
|
1285
|
-
|
|
1418
|
+
debug('updateIssue: post-update refresh was rate-limited; returning fallback issue payload', {
|
|
1419
|
+
issueRef,
|
|
1420
|
+
issueId: targetIssue?.id,
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
usedRateLimitFallback = true;
|
|
1424
|
+
updatedIssue = buildFallbackUpdatedIssue(targetIssue, updateInput);
|
|
1425
|
+
}
|
|
1286
1426
|
|
|
1287
1427
|
const changed = [...Object.keys(updateInput)];
|
|
1288
1428
|
if (parentOfRefs.length > 0) changed.push('parentOf');
|
|
@@ -1294,6 +1434,7 @@ export async function updateIssue(client, issueRef, patch = {}) {
|
|
|
1294
1434
|
return {
|
|
1295
1435
|
issue: updatedIssue || targetIssue,
|
|
1296
1436
|
changed,
|
|
1437
|
+
usedRateLimitFallback,
|
|
1297
1438
|
};
|
|
1298
1439
|
}, 'updateIssue');
|
|
1299
1440
|
}
|
package/FUNCTIONALITY.md
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# pi-linear-tools Functionality
|
|
2
|
-
|
|
3
|
-
## Scope
|
|
4
|
-
|
|
5
|
-
`pi-linear-tools` provides Linear SDK functionality as a pi extension package.
|
|
6
|
-
|
|
7
|
-
Included:
|
|
8
|
-
- extension configuration command (`/linear-tools-config`)
|
|
9
|
-
- issue/project/milestone tools powered by `@linear/sdk`
|
|
10
|
-
- issue start flow with optional git branch creation/switch
|
|
11
|
-
- settings persistence for API key/default team/project team mapping
|
|
12
|
-
|
|
13
|
-
Excluded:
|
|
14
|
-
- daemon runtime
|
|
15
|
-
- polling loop
|
|
16
|
-
- systemd service installation/control
|
|
17
|
-
- tmux/process session management
|
|
18
|
-
- RPC process lifecycle management
|
|
19
|
-
|
|
20
|
-
## Core modules
|
|
21
|
-
|
|
22
|
-
- `src/linear-client.js`: Linear SDK client factory
|
|
23
|
-
- `src/linear.js`: issue/project/milestone operations and formatting helpers
|
|
24
|
-
- `src/settings.js`: settings defaults/validation/load/save
|
|
25
|
-
- `src/logger.js`: structured logging
|
|
26
|
-
- `extensions/pi-linear-tools.js`: command and tool registration
|
|
27
|
-
- `src/cli.js`: optional local CLI for settings operations
|
|
28
|
-
|
|
29
|
-
## Tool behavior notes
|
|
30
|
-
|
|
31
|
-
- `LINEAR_API_KEY` is resolved from env first, then settings
|
|
32
|
-
- project references may be project name or project ID
|
|
33
|
-
- issue references may be identifier (`ABC-123`) or Linear issue ID
|
|
34
|
-
- default team resolution order for issue creation:
|
|
35
|
-
1. explicit `team` parameter
|
|
36
|
-
2. project-level configured team
|
|
37
|
-
3. global `defaultTeam`
|
|
38
|
-
- `linear_issue` `start` action:
|
|
39
|
-
- uses Linear branch name if available
|
|
40
|
-
- can create/switch git branch via `pi.exec("git", ...)`
|
|
41
|
-
|
|
42
|
-
## Settings schema
|
|
43
|
-
|
|
44
|
-
```json
|
|
45
|
-
{
|
|
46
|
-
"schemaVersion": 1,
|
|
47
|
-
"linearApiKey": null,
|
|
48
|
-
"defaultTeam": null,
|
|
49
|
-
"projects": {
|
|
50
|
-
"<linear-project-id>": {
|
|
51
|
-
"scope": {
|
|
52
|
-
"team": "ENG"
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
```
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# Post-release verification checklist
|
|
2
|
-
|
|
3
|
-
## Registry and install checks
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
npm view @fink-andreas/pi-linear-tools version
|
|
7
|
-
npm install -g @fink-andreas/pi-linear-tools
|
|
8
|
-
pi-linear-tools --help
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## pi package route smoke test
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
pi install git:github.com/fink-andreas/pi-linear-tools
|
|
15
|
-
# then enable extension resource and run
|
|
16
|
-
/linear-tools-help
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Basic command smoke test
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
pi-linear-tools project list
|
|
23
|
-
pi-linear-tools team list
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Closeout
|
|
27
|
-
|
|
28
|
-
- Capture any regressions as follow-up Linear issues
|
|
29
|
-
- Post release summary to INN-234
|
|
30
|
-
- Mark milestone complete
|
package/RELEASE.md
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# Release Runbook
|
|
2
|
-
|
|
3
|
-
## npm publish (`v0.1.0`)
|
|
4
|
-
|
|
5
|
-
1. Ensure clean working tree
|
|
6
|
-
```bash
|
|
7
|
-
git status
|
|
8
|
-
```
|
|
9
|
-
2. Run pre-publish checks
|
|
10
|
-
```bash
|
|
11
|
-
npm run release:check
|
|
12
|
-
```
|
|
13
|
-
3. Verify npm authentication
|
|
14
|
-
```bash
|
|
15
|
-
npm whoami
|
|
16
|
-
```
|
|
17
|
-
4. Publish package
|
|
18
|
-
```bash
|
|
19
|
-
npm publish --access public
|
|
20
|
-
```
|
|
21
|
-
5. Verify published version
|
|
22
|
-
```bash
|
|
23
|
-
npm view @fink-andreas/pi-linear-tools version
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Post-publish quick validation
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
npm install -g @fink-andreas/pi-linear-tools
|
|
30
|
-
pi-linear-tools --help
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## GitHub release
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
git tag v0.1.0
|
|
37
|
-
git push origin v0.1.0
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Create release notes from `RELEASE_NOTES_v0.1.0.md`:
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
gh release create v0.1.0 --title "v0.1.0" --notes-file RELEASE_NOTES_v0.1.0.md
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Verify release:
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
gh release view v0.1.0
|
|
50
|
-
```
|