@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/index.js
CHANGED
|
@@ -1,951 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { createLinearClient, checkAndClearRateLimit, markRateLimited } from './src/linear-client.js';
|
|
3
|
-
import { setQuietMode } from './src/logger.js';
|
|
4
|
-
import {
|
|
5
|
-
resolveProjectRef,
|
|
6
|
-
fetchTeams,
|
|
7
|
-
fetchWorkspaces,
|
|
8
|
-
} from './src/linear.js';
|
|
9
|
-
import { isPiCodingAgentRoot, findPiCodingAgentRoot, importFromPiRoot, parseArgs, readFlag } from './src/shared.js';
|
|
10
|
-
|
|
11
|
-
async function importPiCodingAgent() {
|
|
12
|
-
try {
|
|
13
|
-
return await import('@mariozechner/pi-coding-agent');
|
|
14
|
-
} catch {
|
|
15
|
-
return importFromPiRoot('dist/index.js');
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async function importPiTui() {
|
|
20
|
-
try {
|
|
21
|
-
return await import('@mariozechner/pi-tui');
|
|
22
|
-
} catch {
|
|
23
|
-
// pi-tui is a dependency of pi-coding-agent and may be nested under it
|
|
24
|
-
return importFromPiRoot('node_modules/@mariozechner/pi-tui/dist/index.js');
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Optional imports for markdown rendering (provided by pi runtime)
|
|
29
|
-
let Markdown = null;
|
|
30
|
-
let Text = null;
|
|
31
|
-
let getMarkdownTheme = null;
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
const piTui = await importPiTui();
|
|
35
|
-
Markdown = piTui?.Markdown || null;
|
|
36
|
-
Text = piTui?.Text || null;
|
|
37
|
-
} catch {
|
|
38
|
-
// ignore
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
const piCodingAgent = await importPiCodingAgent();
|
|
43
|
-
getMarkdownTheme = piCodingAgent?.getMarkdownTheme || null;
|
|
44
|
-
} catch {
|
|
45
|
-
// ignore
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
import {
|
|
49
|
-
executeIssueList,
|
|
50
|
-
executeIssueView,
|
|
51
|
-
executeIssueCreate,
|
|
52
|
-
executeIssueUpdate,
|
|
53
|
-
executeIssueComment,
|
|
54
|
-
executeIssueStart,
|
|
55
|
-
executeIssueDelete,
|
|
56
|
-
executeProjectList,
|
|
57
|
-
executeTeamList,
|
|
58
|
-
executeMilestoneList,
|
|
59
|
-
executeMilestoneView,
|
|
60
|
-
executeMilestoneCreate,
|
|
61
|
-
executeMilestoneUpdate,
|
|
62
|
-
executeMilestoneDelete,
|
|
63
|
-
} from './src/handlers.js';
|
|
64
|
-
import { authenticate, getAccessToken, logout } from './src/auth/index.js';
|
|
65
|
-
import { withMilestoneScopeHint } from './src/error-hints.js';
|
|
66
|
-
|
|
67
|
-
let cachedApiKey = null;
|
|
68
|
-
|
|
69
|
-
async function getLinearAuth() {
|
|
70
|
-
const envKey = process.env.LINEAR_API_KEY;
|
|
71
|
-
if (envKey && envKey.trim()) {
|
|
72
|
-
return { apiKey: envKey.trim() };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const settings = await loadSettings();
|
|
76
|
-
const authMethod = settings.authMethod || 'api-key';
|
|
77
|
-
|
|
78
|
-
if (authMethod === 'oauth') {
|
|
79
|
-
const accessToken = await getAccessToken();
|
|
80
|
-
if (accessToken) {
|
|
81
|
-
return { accessToken };
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (cachedApiKey) {
|
|
86
|
-
return { apiKey: cachedApiKey };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const apiKey = settings.apiKey || settings.linearApiKey;
|
|
90
|
-
if (apiKey && apiKey.trim()) {
|
|
91
|
-
cachedApiKey = apiKey.trim();
|
|
92
|
-
return { apiKey: cachedApiKey };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const fallbackAccessToken = await getAccessToken();
|
|
96
|
-
if (fallbackAccessToken) {
|
|
97
|
-
return { accessToken: fallbackAccessToken };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
throw new Error(
|
|
101
|
-
'No Linear authentication configured. Use /linear-tools-config --api-key <key> or run `pi-linear-tools auth login` in CLI.'
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function createAuthenticatedClient() {
|
|
106
|
-
return createLinearClient(await getLinearAuth());
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async function resolveDefaultTeam(projectId) {
|
|
110
|
-
const settings = await loadSettings();
|
|
111
|
-
|
|
112
|
-
if (projectId && settings.projects?.[projectId]?.scope?.team) {
|
|
113
|
-
return settings.projects[projectId].scope.team;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return settings.defaultTeam || null;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async function runGit(pi, args) {
|
|
120
|
-
if (typeof pi.exec !== 'function') {
|
|
121
|
-
throw new Error('pi.exec is unavailable in this runtime; cannot run git operations');
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const result = await pi.exec('git', args);
|
|
125
|
-
if (result?.code !== 0) {
|
|
126
|
-
const stderr = String(result?.stderr || '').trim();
|
|
127
|
-
throw new Error(`git ${args.join(' ')} failed${stderr ? `: ${stderr}` : ''}`);
|
|
128
|
-
}
|
|
129
|
-
return result;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async function gitBranchExists(pi, branchName) {
|
|
133
|
-
if (typeof pi.exec !== 'function') return false;
|
|
134
|
-
const result = await pi.exec('git', ['rev-parse', '--verify', branchName]);
|
|
135
|
-
return result?.code === 0;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async function startGitBranchForIssue(pi, branchName, fromRef = 'HEAD', onBranchExists = 'switch') {
|
|
139
|
-
const exists = await gitBranchExists(pi, branchName);
|
|
140
|
-
|
|
141
|
-
if (!exists) {
|
|
142
|
-
await runGit(pi, ['checkout', '-b', branchName, fromRef || 'HEAD']);
|
|
143
|
-
return { action: 'created', branchName };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (onBranchExists === 'suffix') {
|
|
147
|
-
let suffix = 1;
|
|
148
|
-
let nextName = `${branchName}-${suffix}`;
|
|
149
|
-
|
|
150
|
-
// eslint-disable-next-line no-await-in-loop
|
|
151
|
-
while (await gitBranchExists(pi, nextName)) {
|
|
152
|
-
suffix += 1;
|
|
153
|
-
nextName = `${branchName}-${suffix}`;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
await runGit(pi, ['checkout', '-b', nextName, fromRef || 'HEAD']);
|
|
157
|
-
return { action: 'created-suffix', branchName: nextName };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
await runGit(pi, ['checkout', branchName]);
|
|
161
|
-
return { action: 'switched', branchName };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async function runInteractiveConfigFlow(ctx, pi) {
|
|
165
|
-
const settings = await loadSettings();
|
|
166
|
-
const previousAuthMethod = settings.authMethod || 'api-key';
|
|
167
|
-
const envKey = process.env.LINEAR_API_KEY?.trim();
|
|
168
|
-
|
|
169
|
-
let client;
|
|
170
|
-
let apiKey = settings.apiKey?.trim() || settings.linearApiKey?.trim() || null;
|
|
171
|
-
let accessToken = null;
|
|
172
|
-
|
|
173
|
-
setQuietMode(true);
|
|
174
|
-
try {
|
|
175
|
-
accessToken = await getAccessToken();
|
|
176
|
-
} finally {
|
|
177
|
-
setQuietMode(false);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const hasWorkingAuth = !!(envKey || apiKey || accessToken);
|
|
181
|
-
|
|
182
|
-
if (hasWorkingAuth) {
|
|
183
|
-
const source = envKey ? 'environment API key' : (accessToken ? 'OAuth token' : 'stored API key');
|
|
184
|
-
const logoutSelection = await ctx.ui.select(
|
|
185
|
-
`Existing authentication detected (${source}). Logout and re-authenticate?`,
|
|
186
|
-
['No', 'Yes']
|
|
187
|
-
);
|
|
188
|
-
|
|
189
|
-
if (!logoutSelection) {
|
|
190
|
-
ctx.ui.notify('Configuration cancelled', 'warning');
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (logoutSelection === 'Yes') {
|
|
195
|
-
setQuietMode(true);
|
|
196
|
-
try {
|
|
197
|
-
await logout();
|
|
198
|
-
} finally {
|
|
199
|
-
setQuietMode(false);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
settings.apiKey = null;
|
|
203
|
-
if (Object.prototype.hasOwnProperty.call(settings, 'linearApiKey')) {
|
|
204
|
-
delete settings.linearApiKey;
|
|
205
|
-
}
|
|
206
|
-
cachedApiKey = null;
|
|
207
|
-
accessToken = null;
|
|
208
|
-
apiKey = null;
|
|
209
|
-
|
|
210
|
-
await saveSettings(settings);
|
|
211
|
-
ctx.ui.notify('Stored authentication cleared.', 'info');
|
|
212
|
-
|
|
213
|
-
if (envKey) {
|
|
214
|
-
ctx.ui.notify('LINEAR_API_KEY is still set in environment and cannot be removed by this command.', 'warning');
|
|
215
|
-
}
|
|
216
|
-
} else {
|
|
217
|
-
if (envKey) {
|
|
218
|
-
client = createLinearClient(envKey);
|
|
219
|
-
} else if (accessToken) {
|
|
220
|
-
settings.authMethod = 'oauth';
|
|
221
|
-
client = createLinearClient({ accessToken });
|
|
222
|
-
} else if (apiKey) {
|
|
223
|
-
settings.authMethod = 'api-key';
|
|
224
|
-
cachedApiKey = apiKey;
|
|
225
|
-
client = createLinearClient(apiKey);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (!client) {
|
|
231
|
-
const selectedAuthMethod = await ctx.ui.select('Select authentication method', ['API Key (recommended for full functionlaity)', 'OAuth']);
|
|
232
|
-
|
|
233
|
-
if (!selectedAuthMethod) {
|
|
234
|
-
ctx.ui.notify('Configuration cancelled', 'warning');
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (selectedAuthMethod === 'OAuth') {
|
|
239
|
-
settings.authMethod = 'oauth';
|
|
240
|
-
cachedApiKey = null;
|
|
241
|
-
|
|
242
|
-
setQuietMode(true);
|
|
243
|
-
try {
|
|
244
|
-
accessToken = await getAccessToken();
|
|
245
|
-
|
|
246
|
-
if (!accessToken) {
|
|
247
|
-
ctx.ui.notify('Starting OAuth login...', 'info');
|
|
248
|
-
try {
|
|
249
|
-
await authenticate({
|
|
250
|
-
onAuthorizationUrl: async (authUrl) => {
|
|
251
|
-
pi.sendMessage({
|
|
252
|
-
customType: 'pi-linear-tools',
|
|
253
|
-
content: [
|
|
254
|
-
'### Linear OAuth login',
|
|
255
|
-
'',
|
|
256
|
-
`[Open authorization URL](${authUrl})`,
|
|
257
|
-
'',
|
|
258
|
-
'If browser did not open automatically, copy and open this URL:',
|
|
259
|
-
`\`${authUrl}\``,
|
|
260
|
-
'',
|
|
261
|
-
'After authorizing, paste the callback URL in the next prompt.',
|
|
262
|
-
].join('\n'),
|
|
263
|
-
display: true,
|
|
264
|
-
});
|
|
265
|
-
ctx.ui.notify('Complete OAuth in browser, then paste callback URL in the prompt.', 'info');
|
|
266
|
-
},
|
|
267
|
-
manualCodeInput: async () => {
|
|
268
|
-
const entered = await ctx.ui.input(
|
|
269
|
-
'Paste callback URL from browser (or type "cancel")',
|
|
270
|
-
'http://localhost:34711/callback?code=...&state=...'
|
|
271
|
-
);
|
|
272
|
-
const normalized = String(entered || '').trim();
|
|
273
|
-
if (!normalized || normalized.toLowerCase() === 'cancel') {
|
|
274
|
-
return null;
|
|
275
|
-
}
|
|
276
|
-
return normalized;
|
|
277
|
-
},
|
|
278
|
-
});
|
|
279
|
-
} catch (error) {
|
|
280
|
-
if (String(error?.message || '').includes('cancelled by user')) {
|
|
281
|
-
ctx.ui.notify('OAuth authentication cancelled.', 'warning');
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
throw error;
|
|
285
|
-
}
|
|
286
|
-
accessToken = await getAccessToken();
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (!accessToken) {
|
|
290
|
-
throw new Error('OAuth authentication failed: no access token available after login');
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
client = createLinearClient({ accessToken });
|
|
294
|
-
} finally {
|
|
295
|
-
setQuietMode(false);
|
|
296
|
-
}
|
|
297
|
-
} else {
|
|
298
|
-
setQuietMode(true);
|
|
299
|
-
try {
|
|
300
|
-
await logout();
|
|
301
|
-
} finally {
|
|
302
|
-
setQuietMode(false);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (!envKey && !apiKey) {
|
|
306
|
-
const promptedKey = await ctx.ui.input('Enter Linear API key', 'lin_xxx');
|
|
307
|
-
const normalized = String(promptedKey || '').trim();
|
|
308
|
-
if (!normalized) {
|
|
309
|
-
ctx.ui.notify('No API key provided. Aborting.', 'warning');
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
apiKey = normalized;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const selectedApiKey = envKey || apiKey;
|
|
317
|
-
settings.apiKey = selectedApiKey;
|
|
318
|
-
settings.authMethod = 'api-key';
|
|
319
|
-
cachedApiKey = selectedApiKey;
|
|
320
|
-
client = createLinearClient(selectedApiKey);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const workspaces = await fetchWorkspaces(client);
|
|
325
|
-
|
|
326
|
-
if (workspaces.length === 0) {
|
|
327
|
-
throw new Error('No workspaces available for this Linear account');
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const workspaceOptions = workspaces.map((w) => `${w.name} (${w.id})`);
|
|
331
|
-
const selectedWorkspaceLabel = await ctx.ui.select('Select workspace', workspaceOptions);
|
|
332
|
-
if (!selectedWorkspaceLabel) {
|
|
333
|
-
ctx.ui.notify('Configuration cancelled', 'warning');
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const selectedWorkspace = workspaces[workspaceOptions.indexOf(selectedWorkspaceLabel)];
|
|
338
|
-
settings.defaultWorkspace = {
|
|
339
|
-
id: selectedWorkspace.id,
|
|
340
|
-
name: selectedWorkspace.name,
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
const teams = await fetchTeams(client);
|
|
344
|
-
if (teams.length === 0) {
|
|
345
|
-
throw new Error('No teams found in selected workspace');
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const teamOptions = teams.map((t) => `${t.key} - ${t.name} (${t.id})`);
|
|
349
|
-
const selectedTeamLabel = await ctx.ui.select('Select default team', teamOptions);
|
|
350
|
-
if (!selectedTeamLabel) {
|
|
351
|
-
ctx.ui.notify('Configuration cancelled', 'warning');
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
const selectedTeam = teams[teamOptions.indexOf(selectedTeamLabel)];
|
|
356
|
-
settings.defaultTeam = selectedTeam.key;
|
|
357
|
-
|
|
358
|
-
await saveSettings(settings);
|
|
359
|
-
ctx.ui.notify(`Configuration saved: workspace ${selectedWorkspace.name}, team ${selectedTeam.key}`, 'info');
|
|
360
|
-
|
|
361
|
-
if (previousAuthMethod !== settings.authMethod) {
|
|
362
|
-
ctx.ui.notify(
|
|
363
|
-
'Authentication method changed. Please restart pi to refresh and make the correct tools available.',
|
|
364
|
-
'warning'
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
async function shouldExposeMilestoneTool() {
|
|
370
|
-
const settings = await loadSettings();
|
|
371
|
-
const authMethod = settings.authMethod || 'api-key';
|
|
372
|
-
const apiKeyFromSettings = (settings.apiKey || settings.linearApiKey || '').trim();
|
|
373
|
-
const apiKeyFromEnv = (process.env.LINEAR_API_KEY || '').trim();
|
|
374
|
-
const hasApiKey = !!(apiKeyFromEnv || apiKeyFromSettings);
|
|
375
|
-
|
|
376
|
-
return authMethod === 'api-key' || hasApiKey;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* Render tool result as markdown
|
|
381
|
-
*/
|
|
382
|
-
function renderMarkdownResult(result, _options, _theme) {
|
|
383
|
-
const text = result.content?.[0]?.text || '';
|
|
384
|
-
|
|
385
|
-
// Fall back to plain text if markdown packages not available
|
|
386
|
-
if (!Markdown || !getMarkdownTheme) {
|
|
387
|
-
const lines = text.split('\n');
|
|
388
|
-
return {
|
|
389
|
-
render: (width) => lines.map((line) => (width && line.length > width ? line.slice(0, width) : line)),
|
|
390
|
-
invalidate: () => {},
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// Return Markdown component directly - the TUI will call its render() method
|
|
395
|
-
try {
|
|
396
|
-
const mdTheme = getMarkdownTheme();
|
|
397
|
-
return new Markdown(text, 0, 0, mdTheme, _theme ? { color: (t) => _theme.fg('toolOutput', t) } : undefined);
|
|
398
|
-
} catch (error) {
|
|
399
|
-
// If markdown rendering fails for any reason, show a visible error so we can diagnose.
|
|
400
|
-
const msg = `[pi-linear-tools] Markdown render failed: ${String(error?.message || error)}`;
|
|
401
|
-
if (Text) {
|
|
402
|
-
return new Text((_theme ? _theme.fg('error', msg) : msg) + `\n\n` + text, 0, 0);
|
|
403
|
-
}
|
|
404
|
-
const lines = (msg + '\n\n' + text).split('\n');
|
|
405
|
-
return {
|
|
406
|
-
render: (width) => lines.map((line) => (width && line.length > width ? line.slice(0, width) : line)),
|
|
407
|
-
invalidate: () => {},
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
async function registerLinearTools(pi) {
|
|
413
|
-
if (typeof pi.registerTool !== 'function') {
|
|
414
|
-
console.warn('[pi-linear-tools] pi.registerTool not available');
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
pi.registerTool({
|
|
419
|
-
name: 'linear_issue',
|
|
420
|
-
label: 'Linear Issue',
|
|
421
|
-
description: 'Interact with Linear issues. Actions: list, view, create, update, comment, start, delete',
|
|
422
|
-
parameters: {
|
|
423
|
-
type: 'object',
|
|
424
|
-
properties: {
|
|
425
|
-
action: {
|
|
426
|
-
type: 'string',
|
|
427
|
-
enum: ['list', 'view', 'create', 'update', 'comment', 'start', 'delete'],
|
|
428
|
-
description: 'Action to perform on issue(s)',
|
|
429
|
-
},
|
|
430
|
-
issue: {
|
|
431
|
-
type: 'string',
|
|
432
|
-
description: 'Issue key (ABC-123) or Linear issue ID (for view, update, comment, start, delete)',
|
|
433
|
-
},
|
|
434
|
-
project: {
|
|
435
|
-
type: 'string',
|
|
436
|
-
description: 'Project name or ID for listing/creating issues (default: current repo directory name)',
|
|
437
|
-
},
|
|
438
|
-
states: {
|
|
439
|
-
type: 'array',
|
|
440
|
-
items: { type: 'string' },
|
|
441
|
-
description: 'Filter by state names for listing',
|
|
442
|
-
},
|
|
443
|
-
assignee: {
|
|
444
|
-
type: 'string',
|
|
445
|
-
description: 'For list: "me" or "all". For create/update: "me" or assignee ID.',
|
|
446
|
-
},
|
|
447
|
-
assigneeId: {
|
|
448
|
-
type: 'string',
|
|
449
|
-
description: 'Optional explicit assignee ID alias for update/create debugging/compatibility.',
|
|
450
|
-
},
|
|
451
|
-
limit: {
|
|
452
|
-
type: 'number',
|
|
453
|
-
description: 'Maximum number of issues to list (default: 50)',
|
|
454
|
-
},
|
|
455
|
-
includeComments: {
|
|
456
|
-
type: 'boolean',
|
|
457
|
-
description: 'Include comments when viewing issue (default: true)',
|
|
458
|
-
},
|
|
459
|
-
title: {
|
|
460
|
-
type: 'string',
|
|
461
|
-
description: 'Issue title (required for create, optional for update)',
|
|
462
|
-
},
|
|
463
|
-
description: {
|
|
464
|
-
type: 'string',
|
|
465
|
-
description: 'Issue description in markdown (for create, update)',
|
|
466
|
-
},
|
|
467
|
-
priority: {
|
|
468
|
-
type: 'number',
|
|
469
|
-
description: 'Priority 0..4 (for create, update)',
|
|
470
|
-
},
|
|
471
|
-
state: {
|
|
472
|
-
type: 'string',
|
|
473
|
-
description: 'Target state name or ID (for create, update)',
|
|
474
|
-
},
|
|
475
|
-
milestone: {
|
|
476
|
-
type: 'string',
|
|
477
|
-
description: 'For update: milestone name/ID, or "none" to clear milestone assignment.',
|
|
478
|
-
},
|
|
479
|
-
projectMilestoneId: {
|
|
480
|
-
type: 'string',
|
|
481
|
-
description: 'Optional explicit milestone ID alias for update.',
|
|
482
|
-
},
|
|
483
|
-
subIssueOf: {
|
|
484
|
-
type: 'string',
|
|
485
|
-
description: 'For update: set this issue as sub-issue of the given issue key/ID, or "none" to clear parent.',
|
|
486
|
-
},
|
|
487
|
-
parentOf: {
|
|
488
|
-
type: 'array',
|
|
489
|
-
items: { type: 'string' },
|
|
490
|
-
description: 'For update: set listed issues as children of this issue.',
|
|
491
|
-
},
|
|
492
|
-
blockedBy: {
|
|
493
|
-
type: 'array',
|
|
494
|
-
items: { type: 'string' },
|
|
495
|
-
description: 'For update: add "blocked by" dependencies (issues that block this issue).',
|
|
496
|
-
},
|
|
497
|
-
blocking: {
|
|
498
|
-
type: 'array',
|
|
499
|
-
items: { type: 'string' },
|
|
500
|
-
description: 'For update: add "blocking" dependencies (issues this issue blocks).',
|
|
501
|
-
},
|
|
502
|
-
relatedTo: {
|
|
503
|
-
type: 'array',
|
|
504
|
-
items: { type: 'string' },
|
|
505
|
-
description: 'For update: add related issue links.',
|
|
506
|
-
},
|
|
507
|
-
duplicateOf: {
|
|
508
|
-
type: 'string',
|
|
509
|
-
description: 'For update: mark this issue as duplicate of the given issue key/ID.',
|
|
510
|
-
},
|
|
511
|
-
team: {
|
|
512
|
-
type: 'string',
|
|
513
|
-
description: 'Team key (e.g. ENG) or name (optional if default team configured)',
|
|
514
|
-
},
|
|
515
|
-
parentId: {
|
|
516
|
-
type: 'string',
|
|
517
|
-
description: 'Parent issue ID for sub-issues (for create)',
|
|
518
|
-
},
|
|
519
|
-
body: {
|
|
520
|
-
type: 'string',
|
|
521
|
-
description: 'Comment body in markdown (for comment)',
|
|
522
|
-
},
|
|
523
|
-
parentCommentId: {
|
|
524
|
-
type: 'string',
|
|
525
|
-
description: 'Parent comment ID for reply (for comment)',
|
|
526
|
-
},
|
|
527
|
-
fromRef: {
|
|
528
|
-
type: 'string',
|
|
529
|
-
description: 'Git ref to branch from (default: HEAD, for start)',
|
|
530
|
-
},
|
|
531
|
-
onBranchExists: {
|
|
532
|
-
type: 'string',
|
|
533
|
-
enum: ['switch', 'suffix'],
|
|
534
|
-
description: 'When branch exists: switch to it or create suffixed branch (for start)',
|
|
535
|
-
},
|
|
536
|
-
},
|
|
537
|
-
required: ['action'],
|
|
538
|
-
additionalProperties: false,
|
|
539
|
-
},
|
|
540
|
-
renderResult: renderMarkdownResult,
|
|
541
|
-
async execute(_toolCallId, params) {
|
|
542
|
-
// Pre-check: skip API calls if we know we're rate limited
|
|
543
|
-
const { isRateLimited, resetAt } = checkAndClearRateLimit();
|
|
544
|
-
if (isRateLimited) {
|
|
545
|
-
throw new Error(
|
|
546
|
-
`Linear API rate limit exceeded (cached).\n\n` +
|
|
547
|
-
`The rate limit resets at: ${resetAt.toLocaleTimeString()}\n\n` +
|
|
548
|
-
`Please wait before making more requests.`
|
|
549
|
-
);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
try {
|
|
553
|
-
const client = await createAuthenticatedClient();
|
|
554
|
-
|
|
555
|
-
switch (params.action) {
|
|
556
|
-
case 'list':
|
|
557
|
-
return await executeIssueList(client, params);
|
|
558
|
-
case 'view':
|
|
559
|
-
return await executeIssueView(client, params);
|
|
560
|
-
case 'create':
|
|
561
|
-
return await executeIssueCreate(client, params, { resolveDefaultTeam });
|
|
562
|
-
case 'update':
|
|
563
|
-
return await executeIssueUpdate(client, params);
|
|
564
|
-
case 'comment':
|
|
565
|
-
return await executeIssueComment(client, params);
|
|
566
|
-
case 'start':
|
|
567
|
-
return await executeIssueStart(client, params, {
|
|
568
|
-
gitExecutor: async (branchName, fromRef, onBranchExists) => {
|
|
569
|
-
return startGitBranchForIssue(pi, branchName, fromRef, onBranchExists);
|
|
570
|
-
},
|
|
571
|
-
});
|
|
572
|
-
case 'delete':
|
|
573
|
-
return await executeIssueDelete(client, params);
|
|
574
|
-
default:
|
|
575
|
-
throw new Error(`Unknown action: ${params.action}`);
|
|
576
|
-
}
|
|
577
|
-
} catch (error) {
|
|
578
|
-
// Comprehensive error handling - catch ALL errors including SDK's RatelimitedLinearError
|
|
579
|
-
const errorType = error?.type || '';
|
|
580
|
-
const errorMessage = String(error?.message || error || 'Unknown error');
|
|
581
|
-
|
|
582
|
-
// Rate limit error - provide clear reset time and mark globally
|
|
583
|
-
if (errorType === 'Ratelimited' || errorMessage.toLowerCase().includes('rate limit')) {
|
|
584
|
-
const resetTimestamp = error?.requestsResetAt || (Date.now() + 3600000);
|
|
585
|
-
const resetTime = new Date(resetTimestamp).toLocaleTimeString();
|
|
586
|
-
markRateLimited(resetTimestamp);
|
|
587
|
-
throw new Error(
|
|
588
|
-
`Linear API rate limit exceeded.\n\n` +
|
|
589
|
-
`The rate limit resets at: ${resetTime}\n\n` +
|
|
590
|
-
`Please wait before making more requests, or reduce the frequency of API calls.`
|
|
591
|
-
);
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// Authentication/Forbidden errors (handles SDK's ForbiddenLinearError)
|
|
595
|
-
if (errorType === 'Forbidden' || errorType === 'AuthenticationError' ||
|
|
596
|
-
errorMessage.toLowerCase().includes('forbidden') || errorMessage.toLowerCase().includes('unauthorized')) {
|
|
597
|
-
throw new Error(
|
|
598
|
-
`Linear API authentication failed: ${errorMessage}\n\n` +
|
|
599
|
-
`Please check your API key or OAuth token permissions.`
|
|
600
|
-
);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// Network errors (handles SDK's NetworkError)
|
|
604
|
-
if (errorType === 'NetworkError' || errorMessage.toLowerCase().includes('network')) {
|
|
605
|
-
throw new Error(
|
|
606
|
-
`Network error communicating with Linear API.\n\n` +
|
|
607
|
-
`Please check your internet connection and try again.`
|
|
608
|
-
);
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
// Internal server errors (handles SDK's InternalError)
|
|
612
|
-
if (errorType === 'InternalError' || (error?.status >= 500 && error?.status < 600)) {
|
|
613
|
-
throw new Error(
|
|
614
|
-
`Linear API server error (${error?.status || 'unknown'}).\n\n` +
|
|
615
|
-
`Linear may be experiencing issues. Please try again later.`
|
|
616
|
-
);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
// Re-throw if already formatted with "Linear API error:"
|
|
620
|
-
if (errorMessage.includes('Linear API error:')) {
|
|
621
|
-
throw error;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// Wrap unexpected errors with context - NEVER let raw errors propagate
|
|
625
|
-
throw new Error(`Linear issue operation failed: ${errorMessage}`);
|
|
626
|
-
}
|
|
627
|
-
},
|
|
628
|
-
});
|
|
629
|
-
|
|
630
|
-
pi.registerTool({
|
|
631
|
-
name: 'linear_project',
|
|
632
|
-
label: 'Linear Project',
|
|
633
|
-
description: 'Interact with Linear projects. Actions: list',
|
|
634
|
-
parameters: {
|
|
635
|
-
type: 'object',
|
|
636
|
-
properties: {
|
|
637
|
-
action: {
|
|
638
|
-
type: 'string',
|
|
639
|
-
enum: ['list'],
|
|
640
|
-
description: 'Action to perform on project(s)',
|
|
641
|
-
},
|
|
642
|
-
},
|
|
643
|
-
required: ['action'],
|
|
644
|
-
additionalProperties: false,
|
|
645
|
-
},
|
|
646
|
-
renderResult: renderMarkdownResult,
|
|
647
|
-
async execute(_toolCallId, params) {
|
|
648
|
-
try {
|
|
649
|
-
const client = await createAuthenticatedClient();
|
|
650
|
-
|
|
651
|
-
switch (params.action) {
|
|
652
|
-
case 'list':
|
|
653
|
-
return await executeProjectList(client);
|
|
654
|
-
default:
|
|
655
|
-
throw new Error(`Unknown action: ${params.action}`);
|
|
656
|
-
}
|
|
657
|
-
} catch (error) {
|
|
658
|
-
// Comprehensive error handling - catch ALL errors
|
|
659
|
-
const errorType = error?.type || '';
|
|
660
|
-
const errorMessage = String(error?.message || error || 'Unknown error');
|
|
661
|
-
|
|
662
|
-
if (errorType === 'Ratelimited' || errorMessage.toLowerCase().includes('rate limit')) {
|
|
663
|
-
const resetAt = error?.requestsResetAt
|
|
664
|
-
? new Date(error.requestsResetAt).toLocaleTimeString()
|
|
665
|
-
: 'approximately 1 hour from now';
|
|
666
|
-
throw new Error(`Linear API rate limit exceeded. Resets at: ${resetAt}. Please wait before retrying.`);
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
if (errorMessage.includes('Linear API error:')) {
|
|
670
|
-
throw error;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
throw new Error(`Linear project operation failed: ${errorMessage}`);
|
|
674
|
-
}
|
|
675
|
-
},
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
pi.registerTool({
|
|
679
|
-
name: 'linear_team',
|
|
680
|
-
label: 'Linear Team',
|
|
681
|
-
description: 'Interact with Linear teams. Actions: list',
|
|
682
|
-
parameters: {
|
|
683
|
-
type: 'object',
|
|
684
|
-
properties: {
|
|
685
|
-
action: {
|
|
686
|
-
type: 'string',
|
|
687
|
-
enum: ['list'],
|
|
688
|
-
description: 'Action to perform on team(s)',
|
|
689
|
-
},
|
|
690
|
-
},
|
|
691
|
-
required: ['action'],
|
|
692
|
-
additionalProperties: false,
|
|
693
|
-
},
|
|
694
|
-
renderResult: renderMarkdownResult,
|
|
695
|
-
async execute(_toolCallId, params) {
|
|
696
|
-
try {
|
|
697
|
-
const client = await createAuthenticatedClient();
|
|
698
|
-
|
|
699
|
-
switch (params.action) {
|
|
700
|
-
case 'list':
|
|
701
|
-
return await executeTeamList(client);
|
|
702
|
-
default:
|
|
703
|
-
throw new Error(`Unknown action: ${params.action}`);
|
|
704
|
-
}
|
|
705
|
-
} catch (error) {
|
|
706
|
-
// Comprehensive error handling - catch ALL errors
|
|
707
|
-
const errorType = error?.type || '';
|
|
708
|
-
const errorMessage = String(error?.message || error || 'Unknown error');
|
|
709
|
-
|
|
710
|
-
if (errorType === 'Ratelimited' || errorMessage.toLowerCase().includes('rate limit')) {
|
|
711
|
-
const resetAt = error?.requestsResetAt
|
|
712
|
-
? new Date(error.requestsResetAt).toLocaleTimeString()
|
|
713
|
-
: 'approximately 1 hour from now';
|
|
714
|
-
throw new Error(`Linear API rate limit exceeded. Resets at: ${resetAt}. Please wait before retrying.`);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
if (errorMessage.includes('Linear API error:')) {
|
|
718
|
-
throw error;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
throw new Error(`Linear team operation failed: ${errorMessage}`);
|
|
722
|
-
}
|
|
723
|
-
},
|
|
724
|
-
});
|
|
725
|
-
|
|
726
|
-
if (await shouldExposeMilestoneTool()) {
|
|
727
|
-
pi.registerTool({
|
|
728
|
-
name: 'linear_milestone',
|
|
729
|
-
label: 'Linear Milestone',
|
|
730
|
-
description: 'Interact with Linear project milestones. Actions: list, view, create, update, delete',
|
|
731
|
-
parameters: {
|
|
732
|
-
type: 'object',
|
|
733
|
-
properties: {
|
|
734
|
-
action: {
|
|
735
|
-
type: 'string',
|
|
736
|
-
enum: ['list', 'view', 'create', 'update', 'delete'],
|
|
737
|
-
description: 'Action to perform on milestone(s)',
|
|
738
|
-
},
|
|
739
|
-
milestone: {
|
|
740
|
-
type: 'string',
|
|
741
|
-
description: 'Milestone ID (for view, update, delete)',
|
|
742
|
-
},
|
|
743
|
-
project: {
|
|
744
|
-
type: 'string',
|
|
745
|
-
description: 'Project name or ID (for list, create)',
|
|
746
|
-
},
|
|
747
|
-
name: {
|
|
748
|
-
type: 'string',
|
|
749
|
-
description: 'Milestone name (required for create, optional for update)',
|
|
750
|
-
},
|
|
751
|
-
description: {
|
|
752
|
-
type: 'string',
|
|
753
|
-
description: 'Milestone description in markdown',
|
|
754
|
-
},
|
|
755
|
-
targetDate: {
|
|
756
|
-
type: 'string',
|
|
757
|
-
description: 'Target completion date (ISO 8601 date)',
|
|
758
|
-
},
|
|
759
|
-
},
|
|
760
|
-
required: ['action'],
|
|
761
|
-
additionalProperties: false,
|
|
762
|
-
},
|
|
763
|
-
renderResult: renderMarkdownResult,
|
|
764
|
-
async execute(_toolCallId, params) {
|
|
765
|
-
try {
|
|
766
|
-
const client = await createAuthenticatedClient();
|
|
767
|
-
|
|
768
|
-
switch (params.action) {
|
|
769
|
-
case 'list':
|
|
770
|
-
return await executeMilestoneList(client, params);
|
|
771
|
-
case 'view':
|
|
772
|
-
return await executeMilestoneView(client, params);
|
|
773
|
-
case 'create':
|
|
774
|
-
return await executeMilestoneCreate(client, params);
|
|
775
|
-
case 'update':
|
|
776
|
-
return await executeMilestoneUpdate(client, params);
|
|
777
|
-
case 'delete':
|
|
778
|
-
return await executeMilestoneDelete(client, params);
|
|
779
|
-
default:
|
|
780
|
-
throw new Error(`Unknown action: ${params.action}`);
|
|
781
|
-
}
|
|
782
|
-
} catch (error) {
|
|
783
|
-
// Apply milestone-specific hint, then wrap with operation context
|
|
784
|
-
const hintError = withMilestoneScopeHint(error);
|
|
785
|
-
// Comprehensive error handling - catch ALL errors
|
|
786
|
-
const errorType = error?.type || '';
|
|
787
|
-
const errorMessage = String(hintError?.message || hintError || 'Unknown error');
|
|
788
|
-
|
|
789
|
-
if (errorType === 'Ratelimited' || errorMessage.toLowerCase().includes('rate limit')) {
|
|
790
|
-
const resetAt = error?.requestsResetAt
|
|
791
|
-
? new Date(error.requestsResetAt).toLocaleTimeString()
|
|
792
|
-
: 'approximately 1 hour from now';
|
|
793
|
-
throw new Error(`Linear API rate limit exceeded. Resets at: ${resetAt}. Please wait before retrying.`);
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
if (errorMessage.includes('Linear API error:')) {
|
|
797
|
-
throw hintError;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
throw new Error(`Linear milestone operation failed: ${errorMessage}`);
|
|
801
|
-
}
|
|
802
|
-
},
|
|
803
|
-
});
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
export default async function piLinearToolsExtension(pi) {
|
|
808
|
-
// Safety wrapper: never let extension errors crash pi
|
|
809
|
-
try {
|
|
810
|
-
pi.registerCommand('linear-tools-config', {
|
|
811
|
-
description: 'Configure pi-linear-tools settings (API key and default team mappings)',
|
|
812
|
-
handler: async (argsText, ctx) => {
|
|
813
|
-
const args = parseArgs(argsText);
|
|
814
|
-
const apiKey = readFlag(args, '--api-key');
|
|
815
|
-
const defaultTeam = readFlag(args, '--default-team');
|
|
816
|
-
const projectTeam = readFlag(args, '--team');
|
|
817
|
-
const projectName = readFlag(args, '--project');
|
|
818
|
-
|
|
819
|
-
if (apiKey) {
|
|
820
|
-
const settings = await loadSettings();
|
|
821
|
-
const previousAuthMethod = settings.authMethod || 'api-key';
|
|
822
|
-
settings.apiKey = apiKey;
|
|
823
|
-
settings.authMethod = 'api-key';
|
|
824
|
-
await saveSettings(settings);
|
|
825
|
-
cachedApiKey = null;
|
|
826
|
-
if (ctx?.hasUI) {
|
|
827
|
-
ctx.ui.notify('LINEAR_API_KEY saved to settings', 'info');
|
|
828
|
-
if (previousAuthMethod !== settings.authMethod) {
|
|
829
|
-
ctx.ui.notify(
|
|
830
|
-
'Authentication method changed. Please restart pi to refresh and make the correct tools available.',
|
|
831
|
-
'warning'
|
|
832
|
-
);
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
return;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
if (defaultTeam) {
|
|
839
|
-
const settings = await loadSettings();
|
|
840
|
-
settings.defaultTeam = defaultTeam;
|
|
841
|
-
await saveSettings(settings);
|
|
842
|
-
if (ctx?.hasUI) {
|
|
843
|
-
ctx.ui.notify(`Default team set to: ${defaultTeam}`, 'info');
|
|
844
|
-
}
|
|
845
|
-
return;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
if (projectTeam && projectName) {
|
|
849
|
-
const settings = await loadSettings();
|
|
850
|
-
|
|
851
|
-
let projectId = projectName;
|
|
852
|
-
try {
|
|
853
|
-
const client = await createAuthenticatedClient();
|
|
854
|
-
const resolved = await resolveProjectRef(client, projectName);
|
|
855
|
-
projectId = resolved.id;
|
|
856
|
-
} catch {
|
|
857
|
-
// keep provided value as project ID/name key
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
if (!settings.projects[projectId]) {
|
|
861
|
-
settings.projects[projectId] = {
|
|
862
|
-
scope: {
|
|
863
|
-
team: null,
|
|
864
|
-
},
|
|
865
|
-
};
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
if (!settings.projects[projectId].scope) {
|
|
869
|
-
settings.projects[projectId].scope = { team: null };
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
settings.projects[projectId].scope.team = projectTeam;
|
|
873
|
-
await saveSettings(settings);
|
|
874
|
-
|
|
875
|
-
if (ctx?.hasUI) {
|
|
876
|
-
ctx.ui.notify(`Team for project "${projectName}" set to: ${projectTeam}`, 'info');
|
|
877
|
-
}
|
|
878
|
-
return;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
if (!apiKey && !defaultTeam && !projectTeam && !projectName && ctx?.hasUI && ctx?.ui) {
|
|
882
|
-
await runInteractiveConfigFlow(ctx, pi);
|
|
883
|
-
return;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
const settings = await loadSettings();
|
|
887
|
-
const hasKey = !!(settings.apiKey || settings.linearApiKey || process.env.LINEAR_API_KEY);
|
|
888
|
-
const keySource = process.env.LINEAR_API_KEY ? 'environment' : (settings.apiKey || settings.linearApiKey ? 'settings' : 'not set');
|
|
889
|
-
|
|
890
|
-
pi.sendMessage({
|
|
891
|
-
customType: 'pi-linear-tools',
|
|
892
|
-
content: `Configuration:\n LINEAR_API_KEY: ${hasKey ? 'configured' : 'not set'} (source: ${keySource})\n Default workspace: ${settings.defaultWorkspace?.name || 'not set'}\n Default team: ${settings.defaultTeam || 'not set'}\n Project team mappings: ${Object.keys(settings.projects || {}).length}\n\nCommands:\n /linear-tools-config --api-key lin_xxx\n /linear-tools-config --default-team ENG\n /linear-tools-config --team ENG --project MyProject\n\nNote: environment LINEAR_API_KEY takes precedence over settings file.`,
|
|
893
|
-
display: true,
|
|
894
|
-
});
|
|
895
|
-
},
|
|
896
|
-
});
|
|
897
|
-
|
|
898
|
-
pi.registerCommand('linear-tools-reload', {
|
|
899
|
-
description: 'Reload extension runtime (extensions, skills, prompts, themes)',
|
|
900
|
-
handler: async (_args, ctx) => {
|
|
901
|
-
if (ctx?.hasUI) {
|
|
902
|
-
ctx.ui.notify('Reloading runtime...', 'info');
|
|
903
|
-
}
|
|
904
|
-
await ctx.reload();
|
|
905
|
-
},
|
|
906
|
-
});
|
|
907
|
-
|
|
908
|
-
pi.registerCommand('linear-tools-help', {
|
|
909
|
-
description: 'Show pi-linear-tools commands and tools',
|
|
910
|
-
handler: async (_args, ctx) => {
|
|
911
|
-
if (ctx?.hasUI) {
|
|
912
|
-
ctx.ui.notify('pi-linear-tools extension commands available', 'info');
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
const showMilestoneTool = await shouldExposeMilestoneTool();
|
|
916
|
-
const toolLines = [
|
|
917
|
-
'LLM-callable tools:',
|
|
918
|
-
' linear_issue (list/view/create/update/comment/start/delete)',
|
|
919
|
-
' linear_project (list)',
|
|
920
|
-
' linear_team (list)',
|
|
921
|
-
];
|
|
922
|
-
|
|
923
|
-
if (showMilestoneTool) {
|
|
924
|
-
toolLines.push(' linear_milestone (list/view/create/update/delete)');
|
|
925
|
-
} else {
|
|
926
|
-
toolLines.push(' linear_milestone hidden: requires API key auth');
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
pi.sendMessage({
|
|
930
|
-
customType: 'pi-linear-tools',
|
|
931
|
-
content: [
|
|
932
|
-
'Commands:',
|
|
933
|
-
' /linear-tools-config --api-key <key>',
|
|
934
|
-
' /linear-tools-config --default-team <team-key>',
|
|
935
|
-
' /linear-tools-config --team <team-key> --project <project-name-or-id>',
|
|
936
|
-
' /linear-tools-help',
|
|
937
|
-
' /linear-tools-reload',
|
|
938
|
-
'',
|
|
939
|
-
...toolLines,
|
|
940
|
-
].join('\n'),
|
|
941
|
-
display: true,
|
|
942
|
-
});
|
|
943
|
-
},
|
|
944
|
-
});
|
|
945
|
-
|
|
946
|
-
await registerLinearTools(pi);
|
|
947
|
-
} catch (error) {
|
|
948
|
-
// Safety: never let extension initialization crash pi
|
|
949
|
-
console.error('[pi-linear-tools] Extension initialization failed:', error?.message || error);
|
|
950
|
-
}
|
|
951
|
-
}
|
|
1
|
+
export { default } from './extensions/pi-linear-tools.js';
|