@eve-horizon/cli 0.2.0 → 0.2.5

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.
@@ -18,11 +18,26 @@ async function handleAdmin(subcommand, positionals, flags, context) {
18
18
  if (!['owner', 'admin', 'member'].includes(role)) {
19
19
  throw new Error(`Invalid role: ${role}. Must be one of: owner, admin, member`);
20
20
  }
21
+ if (!orgId) {
22
+ throw new Error('No org specified. Use --org <org_id> or set a default org in your profile.');
23
+ }
21
24
  const results = {
22
25
  keys_registered: 0,
23
26
  identities: [],
24
27
  };
28
+ // Add user to org first - this creates the user if they don't exist
29
+ if (orgId) {
30
+ const membership = await (0, client_1.requestJson)(context, `/orgs/${orgId}/members`, {
31
+ method: 'POST',
32
+ body: {
33
+ email,
34
+ role,
35
+ },
36
+ });
37
+ results.membership = membership;
38
+ }
25
39
  // Fetch and register GitHub SSH keys if username provided
40
+ // Now that user exists (created via org membership), identity registration will work
26
41
  if (githubUsername) {
27
42
  const keys = await fetchGitHubKeys(githubUsername);
28
43
  if (keys.length === 0) {
@@ -41,24 +56,6 @@ async function handleAdmin(subcommand, positionals, flags, context) {
41
56
  results.keys_registered += 1;
42
57
  }
43
58
  }
44
- // Add user to org if org_id provided
45
- if (orgId) {
46
- // Get user_id from the first registered identity, or look up by email
47
- let userId;
48
- if (results.identities.length > 0) {
49
- userId = results.identities[0].user_id;
50
- }
51
- if (userId) {
52
- const membership = await (0, client_1.requestJson)(context, `/orgs/${orgId}/members`, {
53
- method: 'POST',
54
- body: {
55
- user_id: userId,
56
- role,
57
- },
58
- });
59
- results.membership = membership;
60
- }
61
- }
62
59
  const summary = [
63
60
  `Invited ${email}`,
64
61
  results.keys_registered > 0 ? `${results.keys_registered} SSH key(s) registered` : null,
@@ -271,15 +271,70 @@ async function handleAuth(subcommand, flags, context, credentials) {
271
271
  console.log(tokenEntry.access_token);
272
272
  return;
273
273
  }
274
+ case 'mint': {
275
+ const email = (0, args_1.getStringFlag)(flags, ['email']);
276
+ const orgId = (0, args_1.getStringFlag)(flags, ['org']);
277
+ const projectId = (0, args_1.getStringFlag)(flags, ['project']);
278
+ const role = (0, args_1.getStringFlag)(flags, ['role']) ?? 'member';
279
+ const ttlStr = (0, args_1.getStringFlag)(flags, ['ttl']);
280
+ const ttlDays = ttlStr ? parseInt(ttlStr, 10) : undefined;
281
+ if (ttlDays !== undefined && (isNaN(ttlDays) || ttlDays < 1 || ttlDays > 90)) {
282
+ throw new Error('--ttl must be between 1 and 90 days');
283
+ }
284
+ if (!email) {
285
+ throw new Error('Usage: eve auth mint --email <email> [--org <org_id> | --project <project_id>] [--role <role>]');
286
+ }
287
+ if (!orgId && !projectId) {
288
+ throw new Error('Usage: eve auth mint --email <email> [--org <org_id> | --project <project_id>] [--role <role>]');
289
+ }
290
+ if (!['admin', 'member'].includes(role)) {
291
+ throw new Error(`Invalid role: ${role}. Must be one of: admin, member`);
292
+ }
293
+ const response = await (0, client_1.requestJson)(context, '/auth/mint', {
294
+ method: 'POST',
295
+ body: {
296
+ email,
297
+ org_id: orgId,
298
+ project_id: projectId,
299
+ role,
300
+ ttl_days: ttlDays,
301
+ },
302
+ });
303
+ if (json) {
304
+ (0, output_1.outputJson)(response, json);
305
+ return;
306
+ }
307
+ if (!response.access_token) {
308
+ throw new Error('Mint response missing access_token');
309
+ }
310
+ // Print only the token to stdout, nothing else
311
+ console.log(response.access_token);
312
+ return;
313
+ }
274
314
  case 'sync': {
275
315
  const claudeOnly = (0, args_1.getBooleanFlag)(flags, ['claude']) ?? false;
276
316
  const codexOnly = (0, args_1.getBooleanFlag)(flags, ['codex']) ?? false;
277
317
  const dryRun = (0, args_1.getBooleanFlag)(flags, ['dry-run']) ?? false;
278
- const system = (0, args_1.getBooleanFlag)(flags, ['system']) ?? false;
318
+ const orgIdFlag = (0, args_1.getStringFlag)(flags, ['org']);
279
319
  const projectIdFlag = (0, args_1.getStringFlag)(flags, ['project']);
280
- const projectId = projectIdFlag ?? context.projectId;
281
- if (!system && !projectId) {
282
- throw new Error('No project specified. Use --project <id> or set default in profile, or use --system for system secrets');
320
+ let scope;
321
+ if (projectIdFlag) {
322
+ scope = { type: 'project', projectId: projectIdFlag };
323
+ }
324
+ else if (orgIdFlag) {
325
+ scope = { type: 'org', orgId: orgIdFlag };
326
+ }
327
+ else {
328
+ // Default to user scope - need to fetch user ID
329
+ const meResponse = await (0, client_1.requestRaw)(context, '/auth/me', { allowError: true });
330
+ if (!meResponse.ok) {
331
+ throw new Error('Not authenticated. Run "eve auth login" first.');
332
+ }
333
+ const meData = meResponse.data;
334
+ if (!meData.user_id) {
335
+ throw new Error('Could not determine user ID. Try specifying --org or --project instead.');
336
+ }
337
+ scope = { type: 'user', userId: meData.user_id };
283
338
  }
284
339
  const extractClaude = !codexOnly; // Extract Claude unless --codex is specified
285
340
  const extractCodex = !claudeOnly; // Extract Codex unless --claude is specified
@@ -287,31 +342,42 @@ async function handleAuth(subcommand, flags, context, credentials) {
287
342
  const platform = process.platform;
288
343
  // Extract Claude OAuth tokens
289
344
  if (extractClaude) {
345
+ // Try macOS Keychain first
290
346
  if (platform === 'darwin') {
291
- try {
292
- const output = (0, node_child_process_1.execSync)('security find-generic-password -s "anthropic.claude" -w', { encoding: 'utf8' }).trim();
293
- if (output) {
294
- extractedTokens.CLAUDE_OAUTH_TOKEN = output;
347
+ for (const service of ['Claude Code-credentials', 'anthropic.claude']) {
348
+ try {
349
+ const output = (0, node_child_process_1.execSync)(`security find-generic-password -s "${service}" -w`, { encoding: 'utf8' }).trim();
350
+ if (output) {
351
+ extractedTokens.CLAUDE_CODE_OAUTH_TOKEN = output;
352
+ break;
353
+ }
354
+ }
355
+ catch {
356
+ // Token not found in keychain, continue
295
357
  }
296
- }
297
- catch (error) {
298
- // Token not found in keychain, continue
299
358
  }
300
359
  }
301
- else {
302
- // Linux: Check various credential file locations
360
+ // Check credential files (all platforms)
361
+ if (!extractedTokens.CLAUDE_CODE_OAUTH_TOKEN) {
303
362
  const credentialPaths = [
304
363
  `${(0, node_os_1.homedir)()}/.claude/.credentials.json`,
305
364
  `${(0, node_os_1.homedir)()}/.claude/credentials.json`,
306
365
  `${(0, node_os_1.homedir)()}/.config/claude/credentials.json`,
307
366
  ];
308
- for (const path of credentialPaths) {
309
- if ((0, node_fs_1.existsSync)(path)) {
367
+ for (const credPath of credentialPaths) {
368
+ if ((0, node_fs_1.existsSync)(credPath)) {
310
369
  try {
311
- const content = (0, node_fs_1.readFileSync)(path, 'utf8');
370
+ const content = (0, node_fs_1.readFileSync)(credPath, 'utf8');
312
371
  const creds = JSON.parse(content);
372
+ // Handle nested claudeAiOauth format (current Claude Code format)
373
+ const claudeOauth = creds.claudeAiOauth;
374
+ if (claudeOauth?.accessToken) {
375
+ extractedTokens.CLAUDE_CODE_OAUTH_TOKEN = claudeOauth.accessToken;
376
+ break;
377
+ }
378
+ // Fallback to legacy root-level tokens
313
379
  if (creds.oauth_token || creds.access_token) {
314
- extractedTokens.CLAUDE_OAUTH_TOKEN = creds.oauth_token || creds.access_token;
380
+ extractedTokens.CLAUDE_CODE_OAUTH_TOKEN = (creds.oauth_token || creds.access_token);
315
381
  break;
316
382
  }
317
383
  }
@@ -322,48 +388,78 @@ async function handleAuth(subcommand, flags, context, credentials) {
322
388
  }
323
389
  }
324
390
  }
325
- // Extract Codex OAuth tokens
391
+ // Extract Codex/Code OAuth tokens
326
392
  if (extractCodex) {
393
+ // Try macOS Keychain first
327
394
  if (platform === 'darwin') {
328
- try {
329
- const output = (0, node_child_process_1.execSync)('security find-generic-password -s "openai.codex" -w', { encoding: 'utf8' }).trim();
330
- if (output) {
331
- extractedTokens.CODEX_OAUTH_TOKEN = output;
395
+ for (const service of ['openai.codex', 'Code-credentials']) {
396
+ try {
397
+ const output = (0, node_child_process_1.execSync)(`security find-generic-password -s "${service}" -w`, { encoding: 'utf8' }).trim();
398
+ if (output) {
399
+ extractedTokens.CODEX_OAUTH_ACCESS_TOKEN = output;
400
+ break;
401
+ }
402
+ }
403
+ catch {
404
+ // Token not found in keychain, continue
332
405
  }
333
- }
334
- catch (error) {
335
- // Token not found in keychain, continue
336
406
  }
337
407
  }
338
- // Check ~/.codex/auth.json for Codex tokens (all platforms)
339
- const codexAuthPath = `${(0, node_os_1.homedir)()}/.codex/auth.json`;
340
- if ((0, node_fs_1.existsSync)(codexAuthPath)) {
341
- try {
342
- const content = (0, node_fs_1.readFileSync)(codexAuthPath, 'utf8');
343
- const auth = JSON.parse(content);
344
- if (auth.oauth_token || auth.access_token) {
345
- extractedTokens.CODEX_OAUTH_TOKEN = auth.oauth_token || auth.access_token;
408
+ // Check auth files (all platforms): ~/.codex/auth.json and ~/.code/auth.json
409
+ if (!extractedTokens.CODEX_OAUTH_ACCESS_TOKEN) {
410
+ const codexAuthPaths = [
411
+ `${(0, node_os_1.homedir)()}/.codex/auth.json`,
412
+ `${(0, node_os_1.homedir)()}/.code/auth.json`,
413
+ ];
414
+ for (const authPath of codexAuthPaths) {
415
+ if ((0, node_fs_1.existsSync)(authPath)) {
416
+ try {
417
+ const content = (0, node_fs_1.readFileSync)(authPath, 'utf8');
418
+ const auth = JSON.parse(content);
419
+ // Handle nested tokens format (current Codex CLI format)
420
+ const tokens = auth.tokens;
421
+ if (tokens?.access_token) {
422
+ extractedTokens.CODEX_OAUTH_ACCESS_TOKEN = tokens.access_token;
423
+ break;
424
+ }
425
+ // Fallback to root-level tokens
426
+ if (auth.oauth_token || auth.access_token) {
427
+ extractedTokens.CODEX_OAUTH_ACCESS_TOKEN = (auth.oauth_token || auth.access_token);
428
+ break;
429
+ }
430
+ // Also check for OPENAI_API_KEY in the auth file
431
+ if (auth.OPENAI_API_KEY && typeof auth.OPENAI_API_KEY === 'string') {
432
+ extractedTokens.OPENAI_API_KEY = auth.OPENAI_API_KEY;
433
+ break;
434
+ }
435
+ }
436
+ catch {
437
+ // Failed to parse, continue
438
+ }
346
439
  }
347
440
  }
348
- catch {
349
- // Failed to parse, continue
350
- }
351
441
  }
352
442
  }
353
443
  if (Object.keys(extractedTokens).length === 0) {
354
444
  (0, output_1.outputJson)({ extracted: 0, tokens: [] }, json, 'No tokens found on host machine');
355
445
  return;
356
446
  }
447
+ // Build target label for output
448
+ const targetLabel = scope.type === 'user' ? 'user' :
449
+ scope.type === 'org' ? `org ${scope.orgId}` :
450
+ `project ${scope.projectId}`;
357
451
  if (dryRun) {
358
452
  const tokenList = Object.keys(extractedTokens).map(key => ({
359
453
  name: key,
360
454
  value: `${extractedTokens[key].substring(0, 10)}...`,
361
455
  }));
362
- (0, output_1.outputJson)({ dry_run: true, would_set: tokenList, target: system ? 'system' : `project ${projectId}` }, json, `Would set ${tokenList.length} token(s) on ${system ? 'system' : `project ${projectId}`}:\n${tokenList.map(t => ` - ${t.name}`).join('\n')}`);
456
+ (0, output_1.outputJson)({ dry_run: true, would_set: tokenList, target: targetLabel, scope }, json, `Would set ${tokenList.length} token(s) on ${targetLabel}:\n${tokenList.map(t => ` - ${t.name}`).join('\n')}`);
363
457
  return;
364
458
  }
365
- // Set secrets via API
366
- const endpoint = system ? '/system/secrets' : `/projects/${projectId}/secrets`;
459
+ // Set secrets via API - endpoint depends on scope
460
+ const endpoint = scope.type === 'user' ? `/users/${scope.userId}/secrets` :
461
+ scope.type === 'org' ? `/orgs/${scope.orgId}/secrets` :
462
+ `/projects/${scope.projectId}/secrets`;
367
463
  const results = [];
368
464
  for (const [name, value] of Object.entries(extractedTokens)) {
369
465
  try {
@@ -387,15 +483,194 @@ async function handleAuth(subcommand, flags, context, credentials) {
387
483
  const successCount = results.filter(r => r.success).length;
388
484
  const failCount = results.filter(r => !r.success).length;
389
485
  (0, output_1.outputJson)({
390
- target: system ? 'system' : `project ${projectId}`,
486
+ target: targetLabel,
487
+ scope,
391
488
  results,
392
489
  success: successCount,
393
490
  failed: failCount,
394
- }, json, `✓ Set ${successCount} secret(s) on ${system ? 'system' : `project ${projectId}`}${failCount > 0 ? ` (${failCount} failed)` : ''}`);
491
+ }, json, `✓ Set ${successCount} secret(s) on ${targetLabel}${failCount > 0 ? ` (${failCount} failed)` : ''}`);
492
+ return;
493
+ }
494
+ case 'creds': {
495
+ // Show local credential status without syncing
496
+ const claudeOnly = (0, args_1.getBooleanFlag)(flags, ['claude']) ?? false;
497
+ const codexOnly = (0, args_1.getBooleanFlag)(flags, ['codex']) ?? false;
498
+ const checkClaude = !codexOnly;
499
+ const checkCodex = !claudeOnly;
500
+ const credentials = [];
501
+ const plat = process.platform;
502
+ // Check Claude credentials
503
+ if (checkClaude) {
504
+ let claudeFound = false;
505
+ let claudeSource = '';
506
+ let claudePreview = '';
507
+ let claudeExpires;
508
+ // Try macOS Keychain
509
+ if (plat === 'darwin' && !claudeFound) {
510
+ for (const service of ['Claude Code-credentials', 'anthropic.claude']) {
511
+ try {
512
+ const output = (0, node_child_process_1.execSync)(`security find-generic-password -s "${service}" -w`, { encoding: 'utf8' }).trim();
513
+ if (output) {
514
+ claudeFound = true;
515
+ claudeSource = `macOS Keychain (${service})`;
516
+ claudePreview = output.substring(0, 15) + '...';
517
+ break;
518
+ }
519
+ }
520
+ catch {
521
+ // Not found
522
+ }
523
+ }
524
+ }
525
+ // Check credential files
526
+ if (!claudeFound) {
527
+ const credentialPaths = [
528
+ `${(0, node_os_1.homedir)()}/.claude/.credentials.json`,
529
+ `${(0, node_os_1.homedir)()}/.claude/credentials.json`,
530
+ `${(0, node_os_1.homedir)()}/.config/claude/credentials.json`,
531
+ ];
532
+ for (const credPath of credentialPaths) {
533
+ if ((0, node_fs_1.existsSync)(credPath)) {
534
+ try {
535
+ const content = (0, node_fs_1.readFileSync)(credPath, 'utf8');
536
+ const creds = JSON.parse(content);
537
+ const claudeOauth = creds.claudeAiOauth;
538
+ if (claudeOauth?.accessToken) {
539
+ claudeFound = true;
540
+ claudeSource = credPath.replace((0, node_os_1.homedir)(), '~');
541
+ claudePreview = claudeOauth.accessToken.substring(0, 15) + '...';
542
+ if (claudeOauth.expiresAt) {
543
+ const expDate = new Date(claudeOauth.expiresAt);
544
+ claudeExpires = expDate.toISOString();
545
+ }
546
+ break;
547
+ }
548
+ if (creds.oauth_token || creds.access_token) {
549
+ claudeFound = true;
550
+ claudeSource = credPath.replace((0, node_os_1.homedir)(), '~');
551
+ const token = (creds.oauth_token || creds.access_token);
552
+ claudePreview = token.substring(0, 15) + '...';
553
+ break;
554
+ }
555
+ }
556
+ catch {
557
+ // Failed to parse
558
+ }
559
+ }
560
+ }
561
+ }
562
+ credentials.push({
563
+ name: 'Claude Code OAuth',
564
+ source: claudeFound ? claudeSource : 'not found',
565
+ found: claudeFound,
566
+ preview: claudePreview || undefined,
567
+ expiresAt: claudeExpires,
568
+ });
569
+ }
570
+ // Check Codex credentials
571
+ if (checkCodex) {
572
+ let codexFound = false;
573
+ let codexSource = '';
574
+ let codexPreview = '';
575
+ // Try macOS Keychain
576
+ if (plat === 'darwin' && !codexFound) {
577
+ for (const service of ['openai.codex', 'Code-credentials']) {
578
+ try {
579
+ const output = (0, node_child_process_1.execSync)(`security find-generic-password -s "${service}" -w`, { encoding: 'utf8' }).trim();
580
+ if (output) {
581
+ codexFound = true;
582
+ codexSource = `macOS Keychain (${service})`;
583
+ codexPreview = output.substring(0, 15) + '...';
584
+ break;
585
+ }
586
+ }
587
+ catch {
588
+ // Not found
589
+ }
590
+ }
591
+ }
592
+ // Check auth files
593
+ if (!codexFound) {
594
+ const codexAuthPaths = [
595
+ `${(0, node_os_1.homedir)()}/.codex/auth.json`,
596
+ `${(0, node_os_1.homedir)()}/.code/auth.json`,
597
+ ];
598
+ for (const authPath of codexAuthPaths) {
599
+ if ((0, node_fs_1.existsSync)(authPath)) {
600
+ try {
601
+ const content = (0, node_fs_1.readFileSync)(authPath, 'utf8');
602
+ const auth = JSON.parse(content);
603
+ const tokens = auth.tokens;
604
+ if (tokens?.access_token) {
605
+ codexFound = true;
606
+ codexSource = authPath.replace((0, node_os_1.homedir)(), '~');
607
+ codexPreview = tokens.access_token.substring(0, 15) + '...';
608
+ break;
609
+ }
610
+ if (auth.oauth_token || auth.access_token) {
611
+ codexFound = true;
612
+ codexSource = authPath.replace((0, node_os_1.homedir)(), '~');
613
+ const token = (auth.oauth_token || auth.access_token);
614
+ codexPreview = token.substring(0, 15) + '...';
615
+ break;
616
+ }
617
+ if (auth.OPENAI_API_KEY && typeof auth.OPENAI_API_KEY === 'string') {
618
+ codexFound = true;
619
+ codexSource = authPath.replace((0, node_os_1.homedir)(), '~') + ' (API key)';
620
+ codexPreview = auth.OPENAI_API_KEY.substring(0, 10) + '...';
621
+ break;
622
+ }
623
+ }
624
+ catch {
625
+ // Failed to parse
626
+ }
627
+ }
628
+ }
629
+ }
630
+ credentials.push({
631
+ name: 'Codex/Code OAuth',
632
+ source: codexFound ? codexSource : 'not found',
633
+ found: codexFound,
634
+ preview: codexPreview || undefined,
635
+ });
636
+ }
637
+ const foundCount = credentials.filter(c => c.found).length;
638
+ if (json) {
639
+ (0, output_1.outputJson)({ credentials, found: foundCount }, json);
640
+ return;
641
+ }
642
+ console.log('Local AI Tool Credentials:');
643
+ console.log('');
644
+ for (const cred of credentials) {
645
+ const status = cred.found ? '✓' : '✗';
646
+ console.log(` ${status} ${cred.name}`);
647
+ console.log(` Source: ${cred.source}`);
648
+ if (cred.preview) {
649
+ console.log(` Token: ${cred.preview}`);
650
+ }
651
+ if (cred.expiresAt) {
652
+ const expDate = new Date(cred.expiresAt);
653
+ const now = new Date();
654
+ const isExpired = expDate < now;
655
+ const expLabel = isExpired ? '(expired)' : '';
656
+ console.log(` Expires: ${cred.expiresAt} ${expLabel}`);
657
+ }
658
+ console.log('');
659
+ }
660
+ if (foundCount > 0) {
661
+ console.log(`Found ${foundCount} credential(s). Run 'eve auth sync' to sync to Eve.`);
662
+ }
663
+ else {
664
+ console.log('No local credentials found.');
665
+ console.log('');
666
+ console.log('To set up credentials:');
667
+ console.log(' Claude: Run "claude" CLI and log in');
668
+ console.log(' Codex: Run "codex" CLI and log in');
669
+ }
395
670
  return;
396
671
  }
397
672
  default:
398
- throw new Error('Usage: eve auth <login|logout|status|whoami|bootstrap|sync|token>');
673
+ throw new Error('Usage: eve auth <login|logout|status|whoami|bootstrap|sync|creds|token>');
399
674
  }
400
675
  }
401
676
  function signNonceWithSsh(keyPath, nonce) {