@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.
package/src/cli.js CHANGED
@@ -1,6 +1,13 @@
1
1
  import { loadSettings, saveSettings } from './settings.js';
2
2
  import { createLinearClient } from './linear-client.js';
3
3
  import { resolveProjectRef } from './linear.js';
4
+ import {
5
+ authenticate,
6
+ logout,
7
+ getAuthStatus,
8
+ isAuthenticated,
9
+ getAccessToken,
10
+ } from './auth/index.js';
4
11
  import {
5
12
  executeIssueList,
6
13
  executeIssueView,
@@ -60,31 +67,61 @@ function parseBoolean(value) {
60
67
  return undefined;
61
68
  }
62
69
 
63
- // ===== API KEY RESOLUTION =====
70
+ function withMilestoneScopeHint(error) {
71
+ const message = String(error?.message || error || 'Unknown error');
72
+
73
+ if (/invalid scope/i.test(message) && /write/i.test(message)) {
74
+ return new Error(
75
+ `${message}\nHint: Milestone create/update/delete require Linear write scope. ` +
76
+ `Use API key auth: pi-linear-tools config --api-key <key>`
77
+ );
78
+ }
79
+
80
+ return error;
81
+ }
82
+
83
+ // ===== AUTH RESOLUTION =====
64
84
 
65
85
  let cachedApiKey = null;
66
86
 
67
- async function getLinearApiKey() {
87
+ async function getLinearAuth() {
68
88
  const envKey = process.env.LINEAR_API_KEY;
69
89
  if (envKey && envKey.trim()) {
70
- return envKey.trim();
90
+ return { apiKey: envKey.trim() };
91
+ }
92
+
93
+ const settings = await loadSettings();
94
+ const authMethod = settings.authMethod || 'api-key';
95
+
96
+ if (authMethod === 'oauth') {
97
+ const accessToken = await getAccessToken();
98
+ if (accessToken) {
99
+ return { accessToken };
100
+ }
71
101
  }
72
102
 
73
103
  if (cachedApiKey) {
74
- return cachedApiKey;
104
+ return { apiKey: cachedApiKey };
75
105
  }
76
106
 
77
- try {
78
- const settings = await loadSettings();
79
- if (settings.linearApiKey && settings.linearApiKey.trim()) {
80
- cachedApiKey = settings.linearApiKey.trim();
81
- return cachedApiKey;
82
- }
83
- } catch {
84
- // ignore, error below
107
+ const apiKey = settings.apiKey || settings.linearApiKey;
108
+ if (apiKey && apiKey.trim()) {
109
+ cachedApiKey = apiKey.trim();
110
+ return { apiKey: cachedApiKey };
111
+ }
112
+
113
+ const fallbackAccessToken = await getAccessToken();
114
+ if (fallbackAccessToken) {
115
+ return { accessToken: fallbackAccessToken };
85
116
  }
86
117
 
87
- throw new Error('LINEAR_API_KEY not set. Run: pi-linear-tools config --api-key <key>');
118
+ throw new Error(
119
+ 'No Linear authentication configured. Run: pi-linear-tools auth login or pi-linear-tools config --api-key <key>'
120
+ );
121
+ }
122
+
123
+ async function createAuthenticatedClient() {
124
+ return createLinearClient(await getLinearAuth());
88
125
  }
89
126
 
90
127
  async function resolveDefaultTeam(projectId) {
@@ -106,15 +143,23 @@ Usage:
106
143
  pi-linear-tools <command> [options]
107
144
 
108
145
  Commands:
109
- help Show this help message
110
- config Show current configuration
111
- config --api-key <key> Set Linear API key
112
- config --default-team <key> Set default team
113
146
  issue <action> [options] Manage issues
114
147
  project <action> [options] Manage projects
115
148
  team <action> [options] Manage teams
116
149
  milestone <action> [options] Manage milestones
117
150
 
151
+ Other commands:
152
+ help Show this help message
153
+ auth <action> Manage authentication (OAuth 2.0)
154
+ config Show current configuration
155
+ config --api-key <key> Set Linear API key (legacy)
156
+ config --default-team <key> Set default team
157
+
158
+ Auth Actions:
159
+ login Authenticate with Linear via OAuth 2.0
160
+ logout Clear stored authentication tokens
161
+ status Show current authentication status
162
+
118
163
  Issue Actions:
119
164
  list [--project X] [--states X,Y] [--assignee me|all] [--limit N]
120
165
  view <issue> [--no-comments]
@@ -122,7 +167,7 @@ Issue Actions:
122
167
  update <issue> [--title X] [--description X] [--state X] [--priority 0-4]
123
168
  [--assignee me|ID] [--milestone X] [--sub-issue-of X]
124
169
  comment <issue> --body X
125
- start <issue> [--branch X] [--from-ref X] [--on-branch-exists switch|suffix]
170
+ start <issue> [--from-ref X] [--on-branch-exists switch|suffix]
126
171
  delete <issue>
127
172
 
128
173
  Project Actions:
@@ -147,6 +192,8 @@ Common Flags:
147
192
  --limit Max results (default: 50)
148
193
 
149
194
  Examples:
195
+ pi-linear-tools auth login
196
+ pi-linear-tools auth status
150
197
  pi-linear-tools issue list --project MyProject --states "In Progress,Backlog"
151
198
  pi-linear-tools issue view ENG-123
152
199
  pi-linear-tools issue create --title "Fix bug" --team ENG --priority 2
@@ -154,6 +201,12 @@ Examples:
154
201
  pi-linear-tools issue start ENG-123
155
202
  pi-linear-tools milestone list --project MyProject
156
203
  pi-linear-tools config --api-key lin_xxx
204
+
205
+ Authentication:
206
+ API key is the recommended authentication method (supports milestones).
207
+ Run 'pi-linear-tools config --api-key <key>' to authenticate.
208
+ For CI/headless environments, set the LINEAR_API_KEY environment variable.
209
+ OAuth 2.0 is also available via 'pi-linear-tools auth login'.
157
210
  `);
158
211
  }
159
212
 
@@ -207,7 +260,6 @@ Comment Options:
207
260
 
208
261
  Start Options:
209
262
  <issue> Issue key or ID
210
- --branch X Custom branch name (default: issue's branch name)
211
263
  --from-ref X Git ref to branch from (default: HEAD)
212
264
  --on-branch-exists X "switch" or "suffix" (default: switch)
213
265
 
@@ -276,6 +328,132 @@ Delete Options:
276
328
  `);
277
329
  }
278
330
 
331
+ function printAuthHelp() {
332
+ console.log(`pi-linear-tools auth - Manage Linear authentication
333
+
334
+ Usage:
335
+ pi-linear-tools auth <action>
336
+
337
+ Actions:
338
+ login Authenticate with Linear via OAuth 2.0
339
+ logout Clear stored authentication tokens
340
+ status Show current authentication status
341
+
342
+ Login:
343
+ Starts the OAuth 2.0 authentication flow:
344
+ 1. Opens your browser to Linear's authorization page
345
+ 2. You authorize the application
346
+ 3. Tokens are stored securely in your OS keychain
347
+ 4. Automatic token refresh keeps you authenticated
348
+
349
+ Logout:
350
+ Clears stored OAuth tokens from your keychain.
351
+ You'll need to authenticate again to access Linear.
352
+
353
+ Status:
354
+ Shows your current authentication status:
355
+ - Whether you're authenticated
356
+ - Token expiry time
357
+ - Granted OAuth scopes
358
+
359
+ Examples:
360
+ pi-linear-tools auth login
361
+ pi-linear-tools auth status
362
+ pi-linear-tools auth logout
363
+
364
+ Environment Variables (for CI/headless environments):
365
+ LINEAR_ACCESS_TOKEN OAuth access token
366
+ LINEAR_REFRESH_TOKEN OAuth refresh token
367
+ LINEAR_EXPIRES_AT Token expiry timestamp (milliseconds)
368
+ `);
369
+ }
370
+
371
+ // ===== AUTH HANDLERS =====
372
+
373
+ async function handleAuthLogin(args) {
374
+ const port = readFlag(args, '--port');
375
+
376
+ try {
377
+ const tokens = await authenticate({
378
+ port: port ? parseInt(port, 10) : undefined,
379
+ });
380
+
381
+ const settings = await loadSettings();
382
+ settings.authMethod = 'oauth';
383
+ await saveSettings(settings);
384
+
385
+ console.log('\n✓ Authentication successful!');
386
+ console.log(`✓ Token expires at: ${new Date(tokens.expiresAt).toLocaleString()}`);
387
+ console.log(`✓ Granted scopes: ${tokens.scope.join(', ')}`);
388
+ console.log('\nYou can now use pi-linear-tools commands.');
389
+ } catch (error) {
390
+ console.error('\n✗ Authentication failed:', error.message);
391
+ process.exitCode = 1;
392
+ }
393
+ }
394
+
395
+ async function handleAuthLogout() {
396
+ try {
397
+ await logout();
398
+ console.log('\n✓ Logged out successfully');
399
+ console.log('\nYou will need to authenticate again to access Linear.');
400
+ } catch (error) {
401
+ console.error('\n✗ Logout failed:', error.message);
402
+ process.exitCode = 1;
403
+ }
404
+ }
405
+
406
+ async function handleAuthStatus() {
407
+ try {
408
+ const status = await getAuthStatus();
409
+
410
+ if (!status) {
411
+ console.log('\nAuthentication status: Not authenticated');
412
+ console.log('\nTo authenticate, run: pi-linear-tools auth login');
413
+ console.log('\nFor CI/headless environments, set these environment variables:');
414
+ console.log(' LINEAR_ACCESS_TOKEN');
415
+ console.log(' LINEAR_REFRESH_TOKEN');
416
+ console.log(' LINEAR_EXPIRES_AT');
417
+ return;
418
+ }
419
+
420
+ const isAuth = await isAuthenticated();
421
+
422
+ console.log('\nAuthentication status:', isAuth ? 'Authenticated' : 'Token expired');
423
+ console.log(`Token expires at: ${new Date(status.expiresAt).toLocaleString()}`);
424
+
425
+ if (status.expiresIn > 0) {
426
+ const minutes = Math.floor(status.expiresIn / 60000);
427
+ console.log(`Time until expiry: ${minutes} minute${minutes !== 1 ? 's' : ''}`);
428
+ }
429
+
430
+ console.log(`Granted scopes: ${status.scopes.join(', ')}`);
431
+ } catch (error) {
432
+ console.error('\n✗ Failed to get authentication status:', error.message);
433
+ process.exitCode = 1;
434
+ }
435
+ }
436
+
437
+ async function handleAuth(args) {
438
+ const [action] = args;
439
+
440
+ if (!action || action === '--help' || action === '-h') {
441
+ printAuthHelp();
442
+ return;
443
+ }
444
+
445
+ switch (action) {
446
+ case 'login':
447
+ return handleAuthLogin(args);
448
+ case 'logout':
449
+ return handleAuthLogout();
450
+ case 'status':
451
+ return handleAuthStatus();
452
+ default:
453
+ throw new Error(`Unknown auth action: ${action}`);
454
+ }
455
+ }
456
+
279
457
  // ===== CONFIG HANDLER =====
280
458
 
281
459
  async function tryResolveProjectId(projectRef, explicitApiKey = null) {
@@ -303,7 +481,8 @@ async function handleConfig(args) {
303
481
 
304
482
  if (apiKey) {
305
483
  const settings = await loadSettings();
306
- settings.linearApiKey = apiKey;
484
+ settings.apiKey = apiKey;
485
+ settings.authMethod = 'api-key';
307
486
  await saveSettings(settings);
308
487
  cachedApiKey = null;
309
488
  console.log('LINEAR_API_KEY saved to settings');
@@ -324,7 +503,7 @@ async function handleConfig(args) {
324
503
  }
325
504
 
326
505
  const settings = await loadSettings();
327
- const projectId = await tryResolveProjectId(projectName, settings.linearApiKey);
506
+ const projectId = await tryResolveProjectId(projectName, settings.apiKey || settings.linearApiKey);
328
507
 
329
508
  if (!settings.projects[projectId]) {
330
509
  settings.projects[projectId] = { scope: { team: null } };
@@ -341,8 +520,8 @@ async function handleConfig(args) {
341
520
  }
342
521
 
343
522
  const settings = await loadSettings();
344
- const hasKey = !!(settings.linearApiKey || process.env.LINEAR_API_KEY);
345
- const keySource = process.env.LINEAR_API_KEY ? 'environment' : (settings.linearApiKey ? 'settings' : 'not set');
523
+ const hasKey = !!(settings.apiKey || settings.linearApiKey || process.env.LINEAR_API_KEY);
524
+ const keySource = process.env.LINEAR_API_KEY ? 'environment' : (settings.apiKey || settings.linearApiKey ? 'settings' : 'not set');
346
525
 
347
526
  console.log(`Configuration:
348
527
  LINEAR_API_KEY: ${hasKey ? 'configured' : 'not set'} (source: ${keySource})
@@ -358,8 +537,7 @@ Commands:
358
537
  // ===== ISSUE HANDLERS =====
359
538
 
360
539
  async function handleIssueList(args) {
361
- const apiKey = await getLinearApiKey();
362
- const client = createLinearClient(apiKey);
540
+ const client = await createAuthenticatedClient();
363
541
 
364
542
  const params = {
365
543
  project: readFlag(args, '--project'),
@@ -373,8 +551,7 @@ async function handleIssueList(args) {
373
551
  }
374
552
 
375
553
  async function handleIssueView(args) {
376
- const apiKey = await getLinearApiKey();
377
- const client = createLinearClient(apiKey);
554
+ const client = await createAuthenticatedClient();
378
555
 
379
556
  const positional = args.filter((a) => !a.startsWith('-'));
380
557
  if (positional.length === 0) {
@@ -391,8 +568,7 @@ async function handleIssueView(args) {
391
568
  }
392
569
 
393
570
  async function handleIssueCreate(args) {
394
- const apiKey = await getLinearApiKey();
395
- const client = createLinearClient(apiKey);
571
+ const client = await createAuthenticatedClient();
396
572
 
397
573
  const params = {
398
574
  title: readFlag(args, '--title'),
@@ -414,8 +590,7 @@ async function handleIssueCreate(args) {
414
590
  }
415
591
 
416
592
  async function handleIssueUpdate(args) {
417
- const apiKey = await getLinearApiKey();
418
- const client = createLinearClient(apiKey);
593
+ const client = await createAuthenticatedClient();
419
594
 
420
595
  const positional = args.filter((a) => !a.startsWith('-'));
421
596
  if (positional.length === 0) {
@@ -438,8 +613,7 @@ async function handleIssueUpdate(args) {
438
613
  }
439
614
 
440
615
  async function handleIssueComment(args) {
441
- const apiKey = await getLinearApiKey();
442
- const client = createLinearClient(apiKey);
616
+ const client = await createAuthenticatedClient();
443
617
 
444
618
  const positional = args.filter((a) => !a.startsWith('-'));
445
619
  if (positional.length === 0) {
@@ -460,8 +634,7 @@ async function handleIssueComment(args) {
460
634
  }
461
635
 
462
636
  async function handleIssueStart(args) {
463
- const apiKey = await getLinearApiKey();
464
- const client = createLinearClient(apiKey);
637
+ const client = await createAuthenticatedClient();
465
638
 
466
639
  const positional = args.filter((a) => !a.startsWith('-'));
467
640
  if (positional.length === 0) {
@@ -470,7 +643,6 @@ async function handleIssueStart(args) {
470
643
 
471
644
  const params = {
472
645
  issue: positional[0],
473
- branch: readFlag(args, '--branch'),
474
646
  fromRef: readFlag(args, '--from-ref'),
475
647
  onBranchExists: readFlag(args, '--on-branch-exists'),
476
648
  };
@@ -480,8 +652,7 @@ async function handleIssueStart(args) {
480
652
  }
481
653
 
482
654
  async function handleIssueDelete(args) {
483
- const apiKey = await getLinearApiKey();
484
- const client = createLinearClient(apiKey);
655
+ const client = await createAuthenticatedClient();
485
656
 
486
657
  const positional = args.filter((a) => !a.startsWith('-'));
487
658
  if (positional.length === 0) {
@@ -527,8 +698,7 @@ async function handleIssue(args) {
527
698
  // ===== PROJECT HANDLERS =====
528
699
 
529
700
  async function handleProjectList() {
530
- const apiKey = await getLinearApiKey();
531
- const client = createLinearClient(apiKey);
701
+ const client = await createAuthenticatedClient();
532
702
 
533
703
  const result = await executeProjectList(client);
534
704
  console.log(result.content[0].text);
@@ -553,8 +723,7 @@ async function handleProject(args) {
553
723
  // ===== TEAM HANDLERS =====
554
724
 
555
725
  async function handleTeamList() {
556
- const apiKey = await getLinearApiKey();
557
- const client = createLinearClient(apiKey);
726
+ const client = await createAuthenticatedClient();
558
727
 
559
728
  const result = await executeTeamList(client);
560
729
  console.log(result.content[0].text);
@@ -579,8 +748,7 @@ async function handleTeam(args) {
579
748
  // ===== MILESTONE HANDLERS =====
580
749
 
581
750
  async function handleMilestoneList(args) {
582
- const apiKey = await getLinearApiKey();
583
- const client = createLinearClient(apiKey);
751
+ const client = await createAuthenticatedClient();
584
752
 
585
753
  const params = {
586
754
  project: readFlag(args, '--project'),
@@ -591,8 +759,7 @@ async function handleMilestoneList(args) {
591
759
  }
592
760
 
593
761
  async function handleMilestoneView(args) {
594
- const apiKey = await getLinearApiKey();
595
- const client = createLinearClient(apiKey);
762
+ const client = await createAuthenticatedClient();
596
763
 
597
764
  const positional = args.filter((a) => !a.startsWith('-'));
598
765
  if (positional.length === 0) {
@@ -608,8 +775,7 @@ async function handleMilestoneView(args) {
608
775
  }
609
776
 
610
777
  async function handleMilestoneCreate(args) {
611
- const apiKey = await getLinearApiKey();
612
- const client = createLinearClient(apiKey);
778
+ const client = await createAuthenticatedClient();
613
779
 
614
780
  const params = {
615
781
  project: readFlag(args, '--project'),
@@ -628,8 +794,7 @@ async function handleMilestoneCreate(args) {
628
794
  }
629
795
 
630
796
  async function handleMilestoneUpdate(args) {
631
- const apiKey = await getLinearApiKey();
632
- const client = createLinearClient(apiKey);
797
+ const client = await createAuthenticatedClient();
633
798
 
634
799
  const positional = args.filter((a) => !a.startsWith('-'));
635
800
  if (positional.length === 0) {
@@ -649,8 +814,7 @@ async function handleMilestoneUpdate(args) {
649
814
  }
650
815
 
651
816
  async function handleMilestoneDelete(args) {
652
- const apiKey = await getLinearApiKey();
653
- const client = createLinearClient(apiKey);
817
+ const client = await createAuthenticatedClient();
654
818
 
655
819
  const positional = args.filter((a) => !a.startsWith('-'));
656
820
  if (positional.length === 0) {
@@ -673,19 +837,23 @@ async function handleMilestone(args) {
673
837
  return;
674
838
  }
675
839
 
676
- switch (action) {
677
- case 'list':
678
- return handleMilestoneList(rest);
679
- case 'view':
680
- return handleMilestoneView(rest);
681
- case 'create':
682
- return handleMilestoneCreate(rest);
683
- case 'update':
684
- return handleMilestoneUpdate(rest);
685
- case 'delete':
686
- return handleMilestoneDelete(rest);
687
- default:
688
- throw new Error(`Unknown milestone action: ${action}`);
840
+ try {
841
+ switch (action) {
842
+ case 'list':
843
+ return await handleMilestoneList(rest);
844
+ case 'view':
845
+ return await handleMilestoneView(rest);
846
+ case 'create':
847
+ return await handleMilestoneCreate(rest);
848
+ case 'update':
849
+ return await handleMilestoneUpdate(rest);
850
+ case 'delete':
851
+ return await handleMilestoneDelete(rest);
852
+ default:
853
+ throw new Error(`Unknown milestone action: ${action}`);
854
+ }
855
+ } catch (error) {
856
+ throw withMilestoneScopeHint(error);
689
857
  }
690
858
  }
691
859
 
@@ -699,6 +867,11 @@ export async function runCli(argv = process.argv.slice(2)) {
699
867
  return;
700
868
  }
701
869
 
870
+ if (command === 'auth') {
871
+ await handleAuth(rest);
872
+ return;
873
+ }
874
+
702
875
  if (command === 'config') {
703
876
  await handleConfig(rest);
704
877
  return;
package/src/handlers.js CHANGED
@@ -447,28 +447,31 @@ export async function executeIssueStart(client, params, options = {}) {
447
447
  const issue = ensureNonEmpty(params.issue, 'issue');
448
448
  const prepared = await prepareIssueStart(client, issue);
449
449
 
450
- const desiredBranch = params.branch || prepared.branchName;
451
- if (!desiredBranch) {
450
+ // Always use Linear's suggested branchName - it cannot be changed via API
451
+ // and using a custom branch would break Linear's branch-to-issue linking
452
+ const branchName = prepared.branchName;
453
+ if (!branchName) {
452
454
  throw new Error(
453
- `No branch name resolved for issue ${prepared.issue.identifier}. Provide the 'branch' parameter explicitly.`
455
+ `No branch name available for issue ${prepared.issue.identifier}. The issue may not have a team assigned.`
454
456
  );
455
457
  }
456
458
 
457
459
  let gitResult;
458
460
  if (gitExecutor) {
459
461
  // Use provided git executor (e.g., pi.exec)
460
- gitResult = await gitExecutor(desiredBranch, params.fromRef || 'HEAD', params.onBranchExists || 'switch');
462
+ gitResult = await gitExecutor(branchName, params.fromRef || 'HEAD', params.onBranchExists || 'switch');
461
463
  } else {
462
464
  // Use built-in child_process git operations
463
- gitResult = await startGitBranch(desiredBranch, params.fromRef || 'HEAD', params.onBranchExists || 'switch');
465
+ gitResult = await startGitBranch(branchName, params.fromRef || 'HEAD', params.onBranchExists || 'switch');
464
466
  }
465
467
 
466
468
  const updatedIssue = await setIssueState(client, prepared.issue.id, prepared.startedState.id);
467
469
 
470
+ const identifier = updatedIssue.identifier || prepared.issue.identifier;
468
471
  const compactTitle = String(updatedIssue.title || prepared.issue?.title || '').trim().toLowerCase();
469
472
  const summary = compactTitle
470
- ? `Started issue ${updatedIssue.identifier} (${compactTitle})`
471
- : `Started issue ${updatedIssue.identifier}`;
473
+ ? `Started issue ${identifier} (${compactTitle})`
474
+ : `Started issue ${identifier}`;
472
475
 
473
476
  return toTextResult(summary, {
474
477
  issueId: updatedIssue.id,
@@ -589,7 +592,7 @@ export async function executeMilestoneList(client, params) {
589
592
  ? ` → ${milestone.targetDate.split('T')[0]}`
590
593
  : '';
591
594
 
592
- lines.push(`- ${statusEmoji} **${milestone.name}** _[${milestone.status}]_ (${progressLabel})${dateLabel}`);
595
+ lines.push(`- ${statusEmoji} **${milestone.name}** _[${milestone.status}]_ (${progressLabel})${dateLabel} \`${milestone.id}\``);
593
596
  if (milestone.description) {
594
597
  lines.push(` ${milestone.description.split('\n')[0].slice(0, 100)}${milestone.description.length > 100 ? '...' : ''}`);
595
598
  }
@@ -731,11 +734,11 @@ export async function executeMilestoneCreate(client, params) {
731
734
  export async function executeMilestoneUpdate(client, params) {
732
735
  const milestoneId = ensureNonEmpty(params.milestone, 'milestone');
733
736
 
737
+ // Note: status is not included as it's a computed/read-only field in Linear's API
734
738
  const result = await updateProjectMilestone(client, milestoneId, {
735
739
  name: params.name,
736
740
  description: params.description,
737
741
  targetDate: params.targetDate,
738
- status: params.status,
739
742
  });
740
743
 
741
744
  const friendlyChanges = result.changed;
@@ -771,10 +774,15 @@ export async function executeMilestoneDelete(client, params) {
771
774
  const milestoneId = ensureNonEmpty(params.milestone, 'milestone');
772
775
  const result = await deleteProjectMilestone(client, milestoneId);
773
776
 
777
+ const label = result.name
778
+ ? `**${result.name}** (\`${milestoneId}\`)`
779
+ : `\`${milestoneId}\``;
780
+
774
781
  return toTextResult(
775
- `Deleted milestone \`${milestoneId}\``,
782
+ `Deleted milestone ${label}`,
776
783
  {
777
784
  milestoneId: result.milestoneId,
785
+ name: result.name,
778
786
  success: result.success,
779
787
  }
780
788
  );
@@ -2,29 +2,59 @@
2
2
  * Linear SDK client factory
3
3
  *
4
4
  * Creates a configured LinearClient instance for interacting with Linear API.
5
+ * Supports both API key and OAuth token authentication.
5
6
  */
6
7
 
7
8
  import { LinearClient } from '@linear/sdk';
9
+ import { debug, warn, error as logError } from './logger.js';
8
10
 
9
11
  /** @type {Function|null} Test-only client factory override */
10
12
  let _testClientFactory = null;
11
13
 
12
14
  /**
13
15
  * Create a Linear SDK client
14
- * @param {string} apiKey - Linear API key
16
+ *
17
+ * Supports two authentication methods:
18
+ * 1. API Key: Pass as string or { apiKey: '...' }
19
+ * 2. OAuth Token: Pass as { accessToken: '...' }
20
+ *
21
+ * @param {string|object} auth - Authentication credential
22
+ * @param {string} [auth.apiKey] - Linear API key (for API key auth)
23
+ * @param {string} [auth.accessToken] - OAuth access token (for OAuth auth)
15
24
  * @returns {LinearClient} Configured Linear client
16
25
  */
17
- export function createLinearClient(apiKey) {
26
+ export function createLinearClient(auth) {
18
27
  // Allow test override
19
28
  if (_testClientFactory) {
20
- return _testClientFactory(apiKey);
29
+ return _testClientFactory(auth);
21
30
  }
22
31
 
23
- if (!apiKey || typeof apiKey !== 'string') {
24
- throw new Error('Linear API key is required');
32
+ let clientConfig;
33
+
34
+ // Handle different input formats
35
+ if (typeof auth === 'string') {
36
+ // Legacy: API key passed as string
37
+ clientConfig = { apiKey: auth };
38
+ } else if (typeof auth === 'object' && auth !== null) {
39
+ // Object format: { apiKey: '...' } or { accessToken: '...' }
40
+ if (auth.accessToken) {
41
+ clientConfig = { apiKey: auth.accessToken };
42
+ debug('Creating Linear client with OAuth access token');
43
+ } else if (auth.apiKey) {
44
+ clientConfig = { apiKey: auth.apiKey };
45
+ debug('Creating Linear client with API key');
46
+ } else {
47
+ throw new Error(
48
+ 'Auth object must contain either apiKey or accessToken'
49
+ );
50
+ }
51
+ } else {
52
+ throw new Error(
53
+ 'Invalid auth parameter: must be a string (API key) or an object with apiKey or accessToken'
54
+ );
25
55
  }
26
56
 
27
- return new LinearClient({ apiKey });
57
+ return new LinearClient(clientConfig);
28
58
  }
29
59
 
30
60
  /**