@fink-andreas/pi-linear-tools 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,113 @@
1
1
  import { loadSettings, saveSettings } from '../src/settings.js';
2
2
  import { createLinearClient } from '../src/linear-client.js';
3
- import { debug } from '../src/logger.js';
3
+ import { setQuietMode } from '../src/logger.js';
4
4
  import {
5
5
  resolveProjectRef,
6
6
  fetchTeams,
7
7
  fetchWorkspaces,
8
8
  } from '../src/linear.js';
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { pathToFileURL } from 'node:url';
12
+
13
+ function isPiCodingAgentRoot(dir) {
14
+ const pkgPath = path.join(dir, 'package.json');
15
+ if (!fs.existsSync(pkgPath)) return false;
16
+ try {
17
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
18
+ return pkg?.name === '@mariozechner/pi-coding-agent';
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ function findPiCodingAgentRoot() {
25
+ const entry = process.argv?.[1];
26
+ if (!entry) return null;
27
+
28
+ // Method 1: walk up from argv1 (works when argv1 is .../pi-coding-agent/dist/cli.js)
29
+ {
30
+ let dir = path.dirname(entry);
31
+ for (let i = 0; i < 20; i += 1) {
32
+ if (isPiCodingAgentRoot(dir)) {
33
+ return dir;
34
+ }
35
+ const parent = path.dirname(dir);
36
+ if (parent === dir) break;
37
+ dir = parent;
38
+ }
39
+ }
40
+
41
+ // Method 2: npm global layout guess (works when argv1 is .../<prefix>/bin/pi)
42
+ // <prefix>/bin/pi -> <prefix>/lib/node_modules/@mariozechner/pi-coding-agent
43
+ {
44
+ const binDir = path.dirname(entry);
45
+ const prefix = path.resolve(binDir, '..');
46
+ const candidate = path.join(prefix, 'lib', 'node_modules', '@mariozechner', 'pi-coding-agent');
47
+ if (isPiCodingAgentRoot(candidate)) {
48
+ return candidate;
49
+ }
50
+ }
51
+
52
+ // Method 3: common global node_modules locations
53
+ for (const candidate of [
54
+ '/usr/local/lib/node_modules/@mariozechner/pi-coding-agent',
55
+ '/usr/lib/node_modules/@mariozechner/pi-coding-agent',
56
+ ]) {
57
+ if (isPiCodingAgentRoot(candidate)) {
58
+ return candidate;
59
+ }
60
+ }
61
+
62
+ return null;
63
+ }
64
+
65
+ async function importFromPiRoot(relativePathFromPiRoot) {
66
+ const piRoot = findPiCodingAgentRoot();
67
+
68
+ if (!piRoot) throw new Error('Unable to locate @mariozechner/pi-coding-agent installation');
69
+
70
+ const absPath = path.join(piRoot, relativePathFromPiRoot);
71
+ return import(pathToFileURL(absPath).href);
72
+ }
73
+
74
+ async function importPiCodingAgent() {
75
+ try {
76
+ return await import('@mariozechner/pi-coding-agent');
77
+ } catch {
78
+ return importFromPiRoot('dist/index.js');
79
+ }
80
+ }
81
+
82
+ async function importPiTui() {
83
+ try {
84
+ return await import('@mariozechner/pi-tui');
85
+ } catch {
86
+ // pi-tui is a dependency of pi-coding-agent and may be nested under it
87
+ return importFromPiRoot('node_modules/@mariozechner/pi-tui/dist/index.js');
88
+ }
89
+ }
90
+
91
+ // Optional imports for markdown rendering (provided by pi runtime)
92
+ let Markdown = null;
93
+ let Text = null;
94
+ let getMarkdownTheme = null;
95
+
96
+ try {
97
+ const piTui = await importPiTui();
98
+ Markdown = piTui?.Markdown || null;
99
+ Text = piTui?.Text || null;
100
+ } catch {
101
+ // ignore
102
+ }
103
+
104
+ try {
105
+ const piCodingAgent = await importPiCodingAgent();
106
+ getMarkdownTheme = piCodingAgent?.getMarkdownTheme || null;
107
+ } catch {
108
+ // ignore
109
+ }
110
+
9
111
  import {
10
112
  executeIssueList,
11
113
  executeIssueView,
@@ -22,6 +124,7 @@ import {
22
124
  executeMilestoneUpdate,
23
125
  executeMilestoneDelete,
24
126
  } from '../src/handlers.js';
127
+ import { authenticate, getAccessToken, logout } from '../src/auth/index.js';
25
128
 
26
129
  function parseArgs(argsString) {
27
130
  if (!argsString || !argsString.trim()) return [];
@@ -42,27 +145,57 @@ function readFlag(args, flag) {
42
145
 
43
146
  let cachedApiKey = null;
44
147
 
45
- async function getLinearApiKey() {
148
+ async function getLinearAuth() {
46
149
  const envKey = process.env.LINEAR_API_KEY;
47
150
  if (envKey && envKey.trim()) {
48
- return envKey.trim();
151
+ return { apiKey: envKey.trim() };
152
+ }
153
+
154
+ const settings = await loadSettings();
155
+ const authMethod = settings.authMethod || 'api-key';
156
+
157
+ if (authMethod === 'oauth') {
158
+ const accessToken = await getAccessToken();
159
+ if (accessToken) {
160
+ return { accessToken };
161
+ }
49
162
  }
50
163
 
51
164
  if (cachedApiKey) {
52
- return cachedApiKey;
165
+ return { apiKey: cachedApiKey };
53
166
  }
54
167
 
55
- try {
56
- const settings = await loadSettings();
57
- if (settings.linearApiKey && settings.linearApiKey.trim()) {
58
- cachedApiKey = settings.linearApiKey.trim();
59
- return cachedApiKey;
60
- }
61
- } catch {
62
- // ignore, error below
168
+ const apiKey = settings.apiKey || settings.linearApiKey;
169
+ if (apiKey && apiKey.trim()) {
170
+ cachedApiKey = apiKey.trim();
171
+ return { apiKey: cachedApiKey };
172
+ }
173
+
174
+ const fallbackAccessToken = await getAccessToken();
175
+ if (fallbackAccessToken) {
176
+ return { accessToken: fallbackAccessToken };
63
177
  }
64
178
 
65
- throw new Error('LINEAR_API_KEY not set. Use /linear-tools-config --api-key <key> or set environment variable.');
179
+ throw new Error(
180
+ 'No Linear authentication configured. Use /linear-tools-config --api-key <key> or run `pi-linear-tools auth login` in CLI.'
181
+ );
182
+ }
183
+
184
+ async function createAuthenticatedClient() {
185
+ return createLinearClient(await getLinearAuth());
186
+ }
187
+
188
+ function withMilestoneScopeHint(error) {
189
+ const message = String(error?.message || error || 'Unknown error');
190
+
191
+ if (/invalid scope/i.test(message) && /write/i.test(message)) {
192
+ return new Error(
193
+ `${message}\nHint: Milestone create/update/delete require Linear write scope. ` +
194
+ `Use API key auth for milestone management: /linear-tools-config --api-key <key>`
195
+ );
196
+ }
197
+
198
+ return error;
66
199
  }
67
200
 
68
201
  async function resolveDefaultTeam(projectId) {
@@ -120,38 +253,166 @@ async function startGitBranchForIssue(pi, branchName, fromRef = 'HEAD', onBranch
120
253
  return { action: 'switched', branchName };
121
254
  }
122
255
 
123
- async function runInteractiveConfigFlow(ctx) {
256
+ async function runInteractiveConfigFlow(ctx, pi) {
124
257
  const settings = await loadSettings();
258
+ const previousAuthMethod = settings.authMethod || 'api-key';
125
259
  const envKey = process.env.LINEAR_API_KEY?.trim();
126
260
 
127
- let apiKey = envKey || settings.linearApiKey?.trim() || null;
261
+ let client;
262
+ let apiKey = settings.apiKey?.trim() || settings.linearApiKey?.trim() || null;
263
+ let accessToken = null;
128
264
 
129
- if (!apiKey) {
130
- const authMethod = await ctx.ui.select('Select authentication method', ['OAuth', 'API Key']);
265
+ setQuietMode(true);
266
+ try {
267
+ accessToken = await getAccessToken();
268
+ } finally {
269
+ setQuietMode(false);
270
+ }
131
271
 
132
- if (!authMethod) {
272
+ const hasWorkingAuth = !!(envKey || apiKey || accessToken);
273
+
274
+ if (hasWorkingAuth) {
275
+ const source = envKey ? 'environment API key' : (accessToken ? 'OAuth token' : 'stored API key');
276
+ const logoutSelection = await ctx.ui.select(
277
+ `Existing authentication detected (${source}). Logout and re-authenticate?`,
278
+ ['No', 'Yes']
279
+ );
280
+
281
+ if (!logoutSelection) {
133
282
  ctx.ui.notify('Configuration cancelled', 'warning');
134
283
  return;
135
284
  }
136
285
 
137
- if (authMethod === 'OAuth') {
138
- ctx.ui.notify('OAuth setup is not implemented yet. Aborting.', 'warning');
139
- return;
286
+ if (logoutSelection === 'Yes') {
287
+ setQuietMode(true);
288
+ try {
289
+ await logout();
290
+ } finally {
291
+ setQuietMode(false);
292
+ }
293
+
294
+ settings.apiKey = null;
295
+ if (Object.prototype.hasOwnProperty.call(settings, 'linearApiKey')) {
296
+ delete settings.linearApiKey;
297
+ }
298
+ cachedApiKey = null;
299
+ accessToken = null;
300
+ apiKey = null;
301
+
302
+ await saveSettings(settings);
303
+ ctx.ui.notify('Stored authentication cleared.', 'info');
304
+
305
+ if (envKey) {
306
+ ctx.ui.notify('LINEAR_API_KEY is still set in environment and cannot be removed by this command.', 'warning');
307
+ }
308
+ } else {
309
+ if (envKey) {
310
+ client = createLinearClient(envKey);
311
+ } else if (accessToken) {
312
+ settings.authMethod = 'oauth';
313
+ client = createLinearClient({ accessToken });
314
+ } else if (apiKey) {
315
+ settings.authMethod = 'api-key';
316
+ cachedApiKey = apiKey;
317
+ client = createLinearClient(apiKey);
318
+ }
140
319
  }
320
+ }
321
+
322
+ if (!client) {
323
+ const selectedAuthMethod = await ctx.ui.select('Select authentication method', ['API Key (recommended for full functionlaity)', 'OAuth']);
141
324
 
142
- const promptedKey = await ctx.ui.input('Enter Linear API key', 'lin_xxx');
143
- const normalized = String(promptedKey || '').trim();
144
- if (!normalized) {
145
- ctx.ui.notify('No API key provided. Aborting.', 'warning');
325
+ if (!selectedAuthMethod) {
326
+ ctx.ui.notify('Configuration cancelled', 'warning');
146
327
  return;
147
328
  }
148
329
 
149
- apiKey = normalized;
150
- settings.linearApiKey = normalized;
151
- cachedApiKey = normalized;
330
+ if (selectedAuthMethod === 'OAuth') {
331
+ settings.authMethod = 'oauth';
332
+ cachedApiKey = null;
333
+
334
+ setQuietMode(true);
335
+ try {
336
+ accessToken = await getAccessToken();
337
+
338
+ if (!accessToken) {
339
+ ctx.ui.notify('Starting OAuth login...', 'info');
340
+ try {
341
+ await authenticate({
342
+ onAuthorizationUrl: async (authUrl) => {
343
+ pi.sendMessage({
344
+ customType: 'pi-linear-tools',
345
+ content: [
346
+ '### Linear OAuth login',
347
+ '',
348
+ `[Open authorization URL](${authUrl})`,
349
+ '',
350
+ 'If browser did not open automatically, copy and open this URL:',
351
+ `\`${authUrl}\``,
352
+ '',
353
+ 'After authorizing, paste the callback URL in the next prompt.',
354
+ ].join('\n'),
355
+ display: true,
356
+ });
357
+ ctx.ui.notify('Complete OAuth in browser, then paste callback URL in the prompt.', 'info');
358
+ },
359
+ manualCodeInput: async () => {
360
+ const entered = await ctx.ui.input(
361
+ 'Paste callback URL from browser (or type "cancel")',
362
+ 'http://localhost:34711/callback?code=...&state=...'
363
+ );
364
+ const normalized = String(entered || '').trim();
365
+ if (!normalized || normalized.toLowerCase() === 'cancel') {
366
+ return null;
367
+ }
368
+ return normalized;
369
+ },
370
+ });
371
+ } catch (error) {
372
+ if (String(error?.message || '').includes('cancelled by user')) {
373
+ ctx.ui.notify('OAuth authentication cancelled.', 'warning');
374
+ return;
375
+ }
376
+ throw error;
377
+ }
378
+ accessToken = await getAccessToken();
379
+ }
380
+
381
+ if (!accessToken) {
382
+ throw new Error('OAuth authentication failed: no access token available after login');
383
+ }
384
+
385
+ client = createLinearClient({ accessToken });
386
+ } finally {
387
+ setQuietMode(false);
388
+ }
389
+ } else {
390
+ setQuietMode(true);
391
+ try {
392
+ await logout();
393
+ } finally {
394
+ setQuietMode(false);
395
+ }
396
+
397
+ if (!envKey && !apiKey) {
398
+ const promptedKey = await ctx.ui.input('Enter Linear API key', 'lin_xxx');
399
+ const normalized = String(promptedKey || '').trim();
400
+ if (!normalized) {
401
+ ctx.ui.notify('No API key provided. Aborting.', 'warning');
402
+ return;
403
+ }
404
+
405
+ apiKey = normalized;
406
+ }
407
+
408
+ const selectedApiKey = envKey || apiKey;
409
+ settings.apiKey = selectedApiKey;
410
+ settings.authMethod = 'api-key';
411
+ cachedApiKey = selectedApiKey;
412
+ client = createLinearClient(selectedApiKey);
413
+ }
152
414
  }
153
415
 
154
- const client = createLinearClient(apiKey);
155
416
  const workspaces = await fetchWorkspaces(client);
156
417
 
157
418
  if (workspaces.length === 0) {
@@ -188,9 +449,59 @@ async function runInteractiveConfigFlow(ctx) {
188
449
 
189
450
  await saveSettings(settings);
190
451
  ctx.ui.notify(`Configuration saved: workspace ${selectedWorkspace.name}, team ${selectedTeam.key}`, 'info');
452
+
453
+ if (previousAuthMethod !== settings.authMethod) {
454
+ ctx.ui.notify(
455
+ 'Authentication method changed. Please restart pi to refresh and make the correct tools available.',
456
+ 'warning'
457
+ );
458
+ }
191
459
  }
192
460
 
193
- function registerLinearTools(pi) {
461
+ async function shouldExposeMilestoneTool() {
462
+ const settings = await loadSettings();
463
+ const authMethod = settings.authMethod || 'api-key';
464
+ const apiKeyFromSettings = (settings.apiKey || settings.linearApiKey || '').trim();
465
+ const apiKeyFromEnv = (process.env.LINEAR_API_KEY || '').trim();
466
+ const hasApiKey = !!(apiKeyFromEnv || apiKeyFromSettings);
467
+
468
+ return authMethod === 'api-key' || hasApiKey;
469
+ }
470
+
471
+ /**
472
+ * Render tool result as markdown
473
+ */
474
+ function renderMarkdownResult(result, _options, _theme) {
475
+ const text = result.content?.[0]?.text || '';
476
+
477
+ // Fall back to plain text if markdown packages not available
478
+ if (!Markdown || !getMarkdownTheme) {
479
+ const lines = text.split('\n');
480
+ return {
481
+ render: (width) => lines.map((line) => (width && line.length > width ? line.slice(0, width) : line)),
482
+ invalidate: () => {},
483
+ };
484
+ }
485
+
486
+ // Return Markdown component directly - the TUI will call its render() method
487
+ try {
488
+ const mdTheme = getMarkdownTheme();
489
+ return new Markdown(text, 0, 0, mdTheme, _theme ? { color: (t) => _theme.fg('toolOutput', t) } : undefined);
490
+ } catch (error) {
491
+ // If markdown rendering fails for any reason, show a visible error so we can diagnose.
492
+ const msg = `[pi-linear-tools] Markdown render failed: ${String(error?.message || error)}`;
493
+ if (Text) {
494
+ return new Text((_theme ? _theme.fg('error', msg) : msg) + `\n\n` + text, 0, 0);
495
+ }
496
+ const lines = (msg + '\n\n' + text).split('\n');
497
+ return {
498
+ render: (width) => lines.map((line) => (width && line.length > width ? line.slice(0, width) : line)),
499
+ invalidate: () => {},
500
+ };
501
+ }
502
+ }
503
+
504
+ async function registerLinearTools(pi) {
194
505
  if (typeof pi.registerTool !== 'function') return;
195
506
 
196
507
  pi.registerTool({
@@ -302,10 +613,6 @@ function registerLinearTools(pi) {
302
613
  type: 'string',
303
614
  description: 'Parent comment ID for reply (for comment)',
304
615
  },
305
- branch: {
306
- type: 'string',
307
- description: 'Custom branch name override (for start)',
308
- },
309
616
  fromRef: {
310
617
  type: 'string',
311
618
  description: 'Git ref to branch from (default: HEAD, for start)',
@@ -319,9 +626,9 @@ function registerLinearTools(pi) {
319
626
  required: ['action'],
320
627
  additionalProperties: false,
321
628
  },
629
+ renderResult: renderMarkdownResult,
322
630
  async execute(_toolCallId, params) {
323
- const apiKey = await getLinearApiKey();
324
- const client = createLinearClient(apiKey);
631
+ const client = await createAuthenticatedClient();
325
632
 
326
633
  switch (params.action) {
327
634
  case 'list':
@@ -364,9 +671,9 @@ function registerLinearTools(pi) {
364
671
  required: ['action'],
365
672
  additionalProperties: false,
366
673
  },
674
+ renderResult: renderMarkdownResult,
367
675
  async execute(_toolCallId, params) {
368
- const apiKey = await getLinearApiKey();
369
- const client = createLinearClient(apiKey);
676
+ const client = await createAuthenticatedClient();
370
677
 
371
678
  switch (params.action) {
372
679
  case 'list':
@@ -393,9 +700,9 @@ function registerLinearTools(pi) {
393
700
  required: ['action'],
394
701
  additionalProperties: false,
395
702
  },
703
+ renderResult: renderMarkdownResult,
396
704
  async execute(_toolCallId, params) {
397
- const apiKey = await getLinearApiKey();
398
- const client = createLinearClient(apiKey);
705
+ const client = await createAuthenticatedClient();
399
706
 
400
707
  switch (params.action) {
401
708
  case 'list':
@@ -406,72 +713,71 @@ function registerLinearTools(pi) {
406
713
  },
407
714
  });
408
715
 
409
- pi.registerTool({
410
- name: 'linear_milestone',
411
- label: 'Linear Milestone',
412
- description: 'Interact with Linear project milestones. Actions: list, view, create, update, delete',
413
- parameters: {
414
- type: 'object',
415
- properties: {
416
- action: {
417
- type: 'string',
418
- enum: ['list', 'view', 'create', 'update', 'delete'],
419
- description: 'Action to perform on milestone(s)',
420
- },
421
- milestone: {
422
- type: 'string',
423
- description: 'Milestone ID (for view, update, delete)',
424
- },
425
- project: {
426
- type: 'string',
427
- description: 'Project name or ID (for list, create)',
428
- },
429
- name: {
430
- type: 'string',
431
- description: 'Milestone name (required for create, optional for update)',
432
- },
433
- description: {
434
- type: 'string',
435
- description: 'Milestone description in markdown',
436
- },
437
- targetDate: {
438
- type: 'string',
439
- description: 'Target completion date (ISO 8601 date)',
440
- },
441
- status: {
442
- type: 'string',
443
- enum: ['backlogged', 'planned', 'inProgress', 'paused', 'completed', 'done', 'cancelled'],
444
- description: 'Milestone status',
445
- },
716
+ if (await shouldExposeMilestoneTool()) {
717
+ pi.registerTool({
718
+ name: 'linear_milestone',
719
+ label: 'Linear Milestone',
720
+ description: 'Interact with Linear project milestones. Actions: list, view, create, update, delete',
721
+ parameters: {
722
+ type: 'object',
723
+ properties: {
724
+ action: {
725
+ type: 'string',
726
+ enum: ['list', 'view', 'create', 'update', 'delete'],
727
+ description: 'Action to perform on milestone(s)',
728
+ },
729
+ milestone: {
730
+ type: 'string',
731
+ description: 'Milestone ID (for view, update, delete)',
732
+ },
733
+ project: {
734
+ type: 'string',
735
+ description: 'Project name or ID (for list, create)',
736
+ },
737
+ name: {
738
+ type: 'string',
739
+ description: 'Milestone name (required for create, optional for update)',
740
+ },
741
+ description: {
742
+ type: 'string',
743
+ description: 'Milestone description in markdown',
744
+ },
745
+ targetDate: {
746
+ type: 'string',
747
+ description: 'Target completion date (ISO 8601 date)',
748
+ },
749
+ },
750
+ required: ['action'],
751
+ additionalProperties: false,
446
752
  },
447
- required: ['action'],
448
- additionalProperties: false,
449
- },
450
- async execute(_toolCallId, params) {
451
- const apiKey = await getLinearApiKey();
452
- const client = createLinearClient(apiKey);
753
+ renderResult: renderMarkdownResult,
754
+ async execute(_toolCallId, params) {
755
+ const client = await createAuthenticatedClient();
453
756
 
454
- switch (params.action) {
455
- case 'list':
456
- return executeMilestoneList(client, params);
457
- case 'view':
458
- return executeMilestoneView(client, params);
459
- case 'create':
460
- return executeMilestoneCreate(client, params);
461
- case 'update':
462
- return executeMilestoneUpdate(client, params);
463
- case 'delete':
464
- return executeMilestoneDelete(client, params);
465
- default:
466
- throw new Error(`Unknown action: ${params.action}`);
467
- }
468
- },
469
- });
757
+ try {
758
+ switch (params.action) {
759
+ case 'list':
760
+ return await executeMilestoneList(client, params);
761
+ case 'view':
762
+ return await executeMilestoneView(client, params);
763
+ case 'create':
764
+ return await executeMilestoneCreate(client, params);
765
+ case 'update':
766
+ return await executeMilestoneUpdate(client, params);
767
+ case 'delete':
768
+ return await executeMilestoneDelete(client, params);
769
+ default:
770
+ throw new Error(`Unknown action: ${params.action}`);
771
+ }
772
+ } catch (error) {
773
+ throw withMilestoneScopeHint(error);
774
+ }
775
+ },
776
+ });
777
+ }
470
778
  }
471
779
 
472
- export default function piLinearToolsExtension(pi) {
473
- registerLinearTools(pi);
474
-
780
+ export default async function piLinearToolsExtension(pi) {
475
781
  pi.registerCommand('linear-tools-config', {
476
782
  description: 'Configure pi-linear-tools settings (API key and default team mappings)',
477
783
  handler: async (argsText, ctx) => {
@@ -483,11 +789,19 @@ export default function piLinearToolsExtension(pi) {
483
789
 
484
790
  if (apiKey) {
485
791
  const settings = await loadSettings();
486
- settings.linearApiKey = apiKey;
792
+ const previousAuthMethod = settings.authMethod || 'api-key';
793
+ settings.apiKey = apiKey;
794
+ settings.authMethod = 'api-key';
487
795
  await saveSettings(settings);
488
796
  cachedApiKey = null;
489
797
  if (ctx?.hasUI) {
490
798
  ctx.ui.notify('LINEAR_API_KEY saved to settings', 'info');
799
+ if (previousAuthMethod !== settings.authMethod) {
800
+ ctx.ui.notify(
801
+ 'Authentication method changed. Please restart pi to refresh and make the correct tools available.',
802
+ 'warning'
803
+ );
804
+ }
491
805
  }
492
806
  return;
493
807
  }
@@ -507,8 +821,7 @@ export default function piLinearToolsExtension(pi) {
507
821
 
508
822
  let projectId = projectName;
509
823
  try {
510
- const resolvedKey = await getLinearApiKey();
511
- const client = createLinearClient(resolvedKey);
824
+ const client = await createAuthenticatedClient();
512
825
  const resolved = await resolveProjectRef(client, projectName);
513
826
  projectId = resolved.id;
514
827
  } catch {
@@ -537,13 +850,13 @@ export default function piLinearToolsExtension(pi) {
537
850
  }
538
851
 
539
852
  if (!apiKey && !defaultTeam && !projectTeam && !projectName && ctx?.hasUI && ctx?.ui) {
540
- await runInteractiveConfigFlow(ctx);
853
+ await runInteractiveConfigFlow(ctx, pi);
541
854
  return;
542
855
  }
543
856
 
544
857
  const settings = await loadSettings();
545
- const hasKey = !!(settings.linearApiKey || process.env.LINEAR_API_KEY);
546
- const keySource = process.env.LINEAR_API_KEY ? 'environment' : (settings.linearApiKey ? 'settings' : 'not set');
858
+ const hasKey = !!(settings.apiKey || settings.linearApiKey || process.env.LINEAR_API_KEY);
859
+ const keySource = process.env.LINEAR_API_KEY ? 'environment' : (settings.apiKey || settings.linearApiKey ? 'settings' : 'not set');
547
860
 
548
861
  pi.sendMessage({
549
862
  customType: 'pi-linear-tools',
@@ -553,6 +866,16 @@ export default function piLinearToolsExtension(pi) {
553
866
  },
554
867
  });
555
868
 
869
+ pi.registerCommand('linear-tools-reload', {
870
+ description: 'Reload extension runtime (extensions, skills, prompts, themes)',
871
+ handler: async (_args, ctx) => {
872
+ if (ctx?.hasUI) {
873
+ ctx.ui.notify('Reloading runtime...', 'info');
874
+ }
875
+ await ctx.reload();
876
+ },
877
+ });
878
+
556
879
  pi.registerCommand('linear-tools-help', {
557
880
  description: 'Show pi-linear-tools commands and tools',
558
881
  handler: async (_args, ctx) => {
@@ -560,6 +883,20 @@ export default function piLinearToolsExtension(pi) {
560
883
  ctx.ui.notify('pi-linear-tools extension commands available', 'info');
561
884
  }
562
885
 
886
+ const showMilestoneTool = await shouldExposeMilestoneTool();
887
+ const toolLines = [
888
+ 'LLM-callable tools:',
889
+ ' linear_issue (list/view/create/update/comment/start/delete)',
890
+ ' linear_project (list)',
891
+ ' linear_team (list)',
892
+ ];
893
+
894
+ if (showMilestoneTool) {
895
+ toolLines.push(' linear_milestone (list/view/create/update/delete)');
896
+ } else {
897
+ toolLines.push(' linear_milestone hidden: requires API key auth');
898
+ }
899
+
563
900
  pi.sendMessage({
564
901
  customType: 'pi-linear-tools',
565
902
  content: [
@@ -568,15 +905,14 @@ export default function piLinearToolsExtension(pi) {
568
905
  ' /linear-tools-config --default-team <team-key>',
569
906
  ' /linear-tools-config --team <team-key> --project <project-name-or-id>',
570
907
  ' /linear-tools-help',
908
+ ' /linear-tools-reload',
571
909
  '',
572
- 'LLM-callable tools:',
573
- ' linear_issue (list/view/create/update/comment/start/delete)',
574
- ' linear_project (list)',
575
- ' linear_team (list)',
576
- ' linear_milestone (list/view/create/update/delete)',
910
+ ...toolLines,
577
911
  ].join('\n'),
578
912
  display: true,
579
913
  });
580
914
  },
581
915
  });
916
+
917
+ await registerLinearTools(pi);
582
918
  }