teleportation-cli 1.1.4 → 1.2.0

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.
Files changed (48) hide show
  1. package/.claude/hooks/config-loader.mjs +88 -34
  2. package/.claude/hooks/permission_request.mjs +392 -82
  3. package/.claude/hooks/post_tool_use.mjs +90 -0
  4. package/.claude/hooks/pre_tool_use.mjs +247 -305
  5. package/.claude/hooks/session-register.mjs +94 -105
  6. package/.claude/hooks/session_end.mjs +41 -42
  7. package/.claude/hooks/session_start.mjs +45 -60
  8. package/.claude/hooks/stop.mjs +752 -99
  9. package/.claude/hooks/user_prompt_submit.mjs +26 -3
  10. package/README.md +7 -0
  11. package/lib/auth/api-key.js +12 -0
  12. package/lib/auth/token-refresh.js +286 -0
  13. package/lib/cli/daemon-commands.js +1 -1
  14. package/lib/cli/teleport-commands.js +469 -0
  15. package/lib/daemon/daemon-v2.js +104 -0
  16. package/lib/daemon/lifecycle.js +56 -171
  17. package/lib/daemon/response-classifier.js +15 -1
  18. package/lib/daemon/services/index.js +3 -0
  19. package/lib/daemon/services/polling-service.js +173 -0
  20. package/lib/daemon/services/queue-service.js +318 -0
  21. package/lib/daemon/services/session-service.js +115 -0
  22. package/lib/daemon/state.js +35 -0
  23. package/lib/daemon/task-executor-v2.js +413 -0
  24. package/lib/daemon/task-executor.js +1235 -0
  25. package/lib/daemon/teleportation-daemon.js +770 -25
  26. package/lib/daemon/timeline-analyzer.js +215 -0
  27. package/lib/daemon/transcript-ingestion.js +696 -0
  28. package/lib/daemon/utils.js +91 -0
  29. package/lib/install/installer.js +184 -20
  30. package/lib/install/uhr-installer.js +136 -0
  31. package/lib/remote/providers/base-provider.js +46 -0
  32. package/lib/remote/providers/daytona-provider.js +58 -0
  33. package/lib/remote/providers/provider-factory.js +90 -19
  34. package/lib/remote/providers/sprites-provider.js +711 -0
  35. package/lib/teleport/exporters/claude-exporter.js +302 -0
  36. package/lib/teleport/exporters/gemini-exporter.js +307 -0
  37. package/lib/teleport/exporters/index.js +93 -0
  38. package/lib/teleport/exporters/interface.js +153 -0
  39. package/lib/teleport/fork-tracker.js +415 -0
  40. package/lib/teleport/git-committer.js +337 -0
  41. package/lib/teleport/index.js +48 -0
  42. package/lib/teleport/manager.js +620 -0
  43. package/lib/teleport/session-capture.js +282 -0
  44. package/package.json +11 -5
  45. package/teleportation-cli.cjs +632 -451
  46. package/.claude/hooks/heartbeat.mjs +0 -396
  47. package/lib/daemon/agentic-executor.js +0 -803
  48. package/lib/daemon/pid-manager.js +0 -160
@@ -7,7 +7,7 @@ const path = require('path');
7
7
  const { execSync } = require('child_process');
8
8
  const os = require('os');
9
9
 
10
- const CLI_VERSION = '1.1.3';
10
+ const CLI_VERSION = '1.1.4';
11
11
  const HOME_DIR = os.homedir();
12
12
  // Teleportation project directory (for development)
13
13
  // In production, hooks will be installed globally
@@ -20,7 +20,8 @@ const c = {
20
20
  yellow: (text) => '\x1b[1;33m' + text + '\x1b[0m',
21
21
  blue: (text) => '\x1b[0;34m' + text + '\x1b[0m',
22
22
  purple: (text) => '\x1b[0;35m' + text + '\x1b[0m',
23
- cyan: (text) => '\x1b[0;36m' + text + '\x1b[0m'
23
+ cyan: (text) => '\x1b[0;36m' + text + '\x1b[0m',
24
+ dim: (text) => '\x1b[2m' + text + '\x1b[0m'
24
25
  };
25
26
 
26
27
  // Configuration manager
@@ -206,9 +207,19 @@ function commandHelp() {
206
207
  console.log(' ' + c.green('status') + ' Check system status and connectivity\n');
207
208
 
208
209
  console.log(c.yellow('Authentication:'));
209
- console.log(' ' + c.green('login') + ' Authenticate with API key or token');
210
+ console.log(' ' + c.green('login') + ' Authenticate with API key or JWT token');
211
+ console.log(' ' + c.dim('--api-key, -k <key> Legacy API key authentication'));
212
+ console.log(' ' + c.dim('--refresh-token, -rt JWT authentication (copy from web UI)'));
213
+ console.log(' ' + c.dim('--relay-url, -r <url> Override relay URL'));
214
+ console.log(' ' + c.green('claim') + ' Link an old API key to your account');
210
215
  console.log(' ' + c.green('github connect') + ' Save GitHub token for repo access (used by remote sessions)');
211
- console.log(' ' + c.green('logout') + ' Clear saved credentials\n');
216
+ console.log(' ' + c.green('logout') + ' Clear saved credentials');
217
+ console.log('');
218
+ console.log(' ' + c.dim('JWT Login (Recommended):'));
219
+ console.log(' ' + c.dim('1. Sign in at app.teleportation.dev'));
220
+ console.log(' ' + c.dim('2. Go to Settings > CLI Authentication'));
221
+ console.log(' ' + c.dim('3. Copy your refresh token'));
222
+ console.log(' ' + c.dim('4. Run: teleportation login --refresh-token <token>\n'));
212
223
 
213
224
  console.log(c.yellow('Setup Commands:'));
214
225
  console.log(' ' + c.green('on') + ' Enable remote control hooks');
@@ -257,6 +268,13 @@ function commandHelp() {
257
268
  console.log(' ' + c.green('remote pull') + ' Pull results from remote session');
258
269
  console.log(' ' + c.green('remote help') + ' Show remote commands help\n');
259
270
 
271
+ console.log(c.yellow('Session Teleportation:'));
272
+ console.log(' ' + c.green('teleport start') + ' Teleport current session to cloud');
273
+ console.log(' ' + c.green('teleport list') + ' List active teleports');
274
+ console.log(' ' + c.green('teleport status') + ' Show teleport status');
275
+ console.log(' ' + c.green('teleport stop') + ' Stop a teleport and create PR');
276
+ console.log(' ' + c.green('teleport help') + ' Show teleport commands help\n');
277
+
260
278
  console.log(c.yellow('Session Isolation:'));
261
279
  console.log(' ' + c.green('worktree create') + ' Create isolated worktree for a session');
262
280
  console.log(' ' + c.green('worktree list') + ' List all session worktrees');
@@ -285,55 +303,65 @@ function commandHelp() {
285
303
 
286
304
  async function commandOn() {
287
305
  console.log(c.yellow('šŸš€ Enabling Teleportation Remote Control...\n'));
288
-
306
+
289
307
  try {
290
- // Use installer module
291
- const installerPath = path.join(TELEPORTATION_DIR, 'lib', 'install', 'installer.js');
292
- const { install, checkNodeVersion, checkClaudeCode } = await import('file://' + installerPath);
293
-
294
- // Pre-flight checks
295
- const nodeCheck = checkNodeVersion();
296
- if (!nodeCheck.valid) {
297
- console.log(c.red(`āŒ ${nodeCheck.error}\n`));
298
- return;
299
- }
300
- console.log(c.green(`āœ… Node.js ${nodeCheck.version}\n`));
301
-
302
- const claudeCheck = checkClaudeCode();
303
- if (!claudeCheck.valid) {
304
- console.log(c.yellow(`āš ļø ${claudeCheck.error}\n`));
305
- console.log(c.cyan(' Continuing anyway...\n'));
308
+ // Try UHR first, fall back to legacy installer
309
+ const uhrInstallerPath = path.join(TELEPORTATION_DIR, 'lib', 'install', 'uhr-installer.js');
310
+ const { isUhrAvailable, installViaUhr } = await import('file://' + uhrInstallerPath);
311
+
312
+ if (await isUhrAvailable()) {
313
+ console.log(c.dim(' Using UHR (Universal Hook Registry)...'));
314
+ const manifestPath = path.join(TELEPORTATION_DIR, 'teleportation.uhr.json');
315
+ const hooksDir = path.join(TELEPORTATION_DIR, '.claude', 'hooks');
316
+ const uhrResult = await installViaUhr(manifestPath, hooksDir);
317
+
318
+ if (uhrResult.success) {
319
+ console.log(c.green('\nšŸŽ‰ Teleportation Remote Control ENABLED!'));
320
+ console.log(c.cyan('\nInstalled via UHR (Universal Hook Registry)'));
321
+ if (uhrResult.warnings.length > 0) {
322
+ uhrResult.warnings.forEach(w => console.log(c.yellow(` āš ļø ${w}`)));
323
+ }
324
+ } else {
325
+ console.log(c.yellow(` āš ļø UHR install failed: ${uhrResult.reason}`));
326
+ console.log(c.dim(' Falling back to legacy installer...'));
327
+ await _legacyInstall();
328
+ }
306
329
  } else {
307
- console.log(c.green(`āœ… Claude Code found: ${claudeCheck.path}\n`));
330
+ await _legacyInstall();
308
331
  }
309
-
310
- // Install hooks
311
- const sourceHooksDir = path.join(TELEPORTATION_DIR, '.claude', 'hooks');
312
- if (!fs.existsSync(sourceHooksDir)) {
313
- console.log(c.red(`āŒ Hooks not found at ${sourceHooksDir}\n`));
314
- return;
315
- }
316
-
317
- const result = await install(sourceHooksDir);
318
-
319
- console.log(c.green('\nšŸŽ‰ Teleportation Remote Control ENABLED!'));
320
- console.log(c.cyan('\nInstallation Summary:'));
321
- console.log(` Hooks verified: ${c.green(result.hooksVerified)}`);
322
- console.log(` Daemon installed: ${c.green(result.daemonInstalled + ' files')}`);
323
- console.log(` Settings file: ${c.green(result.settingsFile)}`);
324
- console.log(` Hooks directory: ${c.green(result.hooksDir)}`);
325
- console.log(` Daemon directory: ${c.green(result.daemonDir)}`);
332
+
326
333
  console.log(c.cyan('\nNext steps:'));
327
334
  console.log(' 1. Login: teleportation login');
328
335
  console.log(' 2. Check status: teleportation status');
329
336
  console.log(' 3. Run diagnostics: teleportation doctor\n');
330
-
337
+
331
338
  } catch (error) {
332
339
  console.log(c.red(`āŒ Installation failed: ${error.message}\n`));
333
340
  process.exit(1);
334
341
  }
335
342
  }
336
343
 
344
+ async function _legacyInstall(options = {}) {
345
+ const installerPath = path.join(TELEPORTATION_DIR, 'lib', 'install', 'installer.js');
346
+ const { install } = await import('file://' + installerPath);
347
+ const sourceHooksDir = path.join(TELEPORTATION_DIR, '.claude', 'hooks');
348
+
349
+ const result = await install(sourceHooksDir, options);
350
+
351
+ console.log(c.green('\nšŸŽ‰ Teleportation Remote Control ENABLED!'));
352
+ console.log(c.cyan('\nInstallation Summary:'));
353
+ if (result.hooksInstalled > 0) {
354
+ console.log(` Claude Code hooks: ${c.green(result.hooksInstalled)} installed`);
355
+ }
356
+ if (result.geminiHooksInstalled > 0) {
357
+ console.log(` Gemini CLI hooks: ${c.green(result.geminiHooksInstalled)} installed`);
358
+ }
359
+ console.log(` Daemon installed: ${c.green(result.daemonInstalled + ' files')}`);
360
+ console.log(` Settings file: ${c.green(result.settingsFile)}`);
361
+
362
+ return result;
363
+ }
364
+
337
365
  /**
338
366
  * Setup wizard - guided onboarding for new users
339
367
  * Creates backup before making changes, validates API key, installs hooks
@@ -447,7 +475,18 @@ async function commandSetup() {
447
475
 
448
476
  // Step 2: Configuration
449
477
  console.log('\n' + c.purple('Step 2 of 5: Configuration\n'));
450
- console.log(` Relay URL: ${c.cyan(relayUrl)}`);
478
+
479
+ // Choose assistants (PRD-0024 Parity)
480
+ console.log(' Which AI coding assistants do you use?');
481
+ console.log(' 1. Claude Code only');
482
+ console.log(' 2. Gemini CLI only');
483
+ console.log(' 3. Both');
484
+
485
+ const assistantChoice = await question('\n Select option (1-3, default: Both): ');
486
+ const includeClaude = assistantChoice === '1' || assistantChoice === '3' || !assistantChoice;
487
+ const includeGemini = assistantChoice === '2' || assistantChoice === '3' || !assistantChoice;
488
+
489
+ console.log(`\n Relay URL: ${c.cyan(relayUrl)}`);
451
490
 
452
491
  // Test relay connectivity
453
492
  console.log(c.cyan(' Testing connectivity...'));
@@ -494,14 +533,50 @@ async function commandSetup() {
494
533
 
495
534
  // Step 3: Install Hooks
496
535
  console.log('\n' + c.purple('Step 3 of 5: Installing Hooks\n'));
497
- console.log(' Installing Claude Code hooks to ~/.claude/hooks/');
536
+ console.log(' Installing Teleportation hooks...');
498
537
 
499
538
  try {
500
- const installerPath = path.join(TELEPORTATION_DIR, 'lib', 'install', 'installer.js');
501
- const { install } = await import('file://' + installerPath);
502
- const sourceHooksDir = path.join(TELEPORTATION_DIR, '.claude', 'hooks');
503
- const result = await install(sourceHooksDir);
504
- console.log(c.green(` āœ… ${result.hooksVerified} hooks installed successfully`));
539
+ // Try UHR first for Claude Code hooks
540
+ let uhrInstalled = false;
541
+ if (includeClaude) {
542
+ try {
543
+ const uhrInstallerPath = path.join(TELEPORTATION_DIR, 'lib', 'install', 'uhr-installer.js');
544
+ const { isUhrAvailable, installViaUhr } = await import('file://' + uhrInstallerPath);
545
+
546
+ if (await isUhrAvailable()) {
547
+ console.log(c.dim(' Using UHR (Universal Hook Registry)...'));
548
+ const manifestPath = path.join(TELEPORTATION_DIR, 'teleportation.uhr.json');
549
+ const hooksDir = path.join(TELEPORTATION_DIR, '.claude', 'hooks');
550
+ const uhrResult = await installViaUhr(manifestPath, hooksDir);
551
+
552
+ if (uhrResult.success) {
553
+ console.log(c.green(' āœ… Claude Code hooks installed via UHR'));
554
+ if (uhrResult.warnings.length > 0) {
555
+ uhrResult.warnings.forEach(w => console.log(c.yellow(` āš ļø ${w}`)));
556
+ }
557
+ uhrInstalled = true;
558
+ }
559
+ }
560
+ } catch (e) {
561
+ // UHR not available, fall through to legacy
562
+ }
563
+ }
564
+
565
+ if (!uhrInstalled) {
566
+ // Legacy installer fallback
567
+ const installerPath = path.join(TELEPORTATION_DIR, 'lib', 'install', 'installer.js');
568
+ const { install } = await import('file://' + installerPath);
569
+ const sourceHooksDir = path.join(TELEPORTATION_DIR, '.claude', 'hooks');
570
+
571
+ const result = await install(sourceHooksDir, { includeClaude, includeGemini });
572
+
573
+ if (result.hooksInstalled > 0) {
574
+ console.log(c.green(` āœ… ${result.hooksInstalled} Claude Code hooks installed`));
575
+ }
576
+ if (result.geminiHooksInstalled > 0) {
577
+ console.log(c.green(` āœ… ${result.geminiHooksInstalled} Gemini CLI hooks installed`));
578
+ }
579
+ }
505
580
  } catch (e) {
506
581
  console.log(c.red(` āŒ Failed to install hooks: ${e.message}`));
507
582
  console.log(c.yellow('\n Would you like to restore your previous configuration?'));
@@ -686,24 +761,42 @@ async function commandBackup(args) {
686
761
  }
687
762
  }
688
763
 
689
- function commandOff() {
764
+ async function commandOff() {
690
765
  console.log(c.yellow('šŸ›‘ Disabling Teleportation Remote Control...\n'));
691
-
692
- // Remove settings.json
693
- if (fs.existsSync(config.globalSettings)) {
694
- fs.unlinkSync(config.globalSettings);
695
- console.log(c.green('āœ… Removed ~/.claude/settings.json'));
766
+
767
+ // Try UHR uninstall first
768
+ let uhrUninstalled = false;
769
+ try {
770
+ const uhrInstallerPath = path.join(TELEPORTATION_DIR, 'lib', 'install', 'uhr-installer.js');
771
+ const { isUhrAvailable, uninstallViaUhr } = await import('file://' + uhrInstallerPath);
772
+
773
+ if (await isUhrAvailable()) {
774
+ const result = await uninstallViaUhr('teleportation');
775
+ if (result.success) {
776
+ console.log(c.green('āœ… Uninstalled via UHR'));
777
+ uhrUninstalled = true;
778
+ }
779
+ }
780
+ } catch (e) {
781
+ // UHR not available or failed, fall through to manual removal
696
782
  }
697
-
698
- // Remove hooks
699
- if (fs.existsSync(config.globalHooks)) {
700
- const hooks = fs.readdirSync(config.globalHooks).filter(f => f.endsWith('.mjs'));
701
- hooks.forEach(hook => {
702
- fs.unlinkSync(path.join(config.globalHooks, hook));
703
- });
704
- console.log(c.green(`āœ… Removed ${hooks.length} hooks`));
783
+
784
+ if (!uhrUninstalled) {
785
+ // Manual removal fallback
786
+ if (fs.existsSync(config.globalSettings)) {
787
+ fs.unlinkSync(config.globalSettings);
788
+ console.log(c.green('āœ… Removed ~/.claude/settings.json'));
789
+ }
790
+
791
+ if (fs.existsSync(config.globalHooks)) {
792
+ const hooks = fs.readdirSync(config.globalHooks).filter(f => f.endsWith('.mjs'));
793
+ hooks.forEach(hook => {
794
+ fs.unlinkSync(path.join(config.globalHooks, hook));
795
+ });
796
+ console.log(c.green(`āœ… Removed ${hooks.length} hooks`));
797
+ }
705
798
  }
706
-
799
+
707
800
  console.log(c.yellow('\nšŸ›‘ Teleportation Remote Control DISABLED'));
708
801
  console.log(c.cyan('Services are still running. Stop with: ./teleportation stop\n'));
709
802
  }
@@ -784,8 +877,13 @@ async function commandStatus() {
784
877
  if (result.valid) {
785
878
  console.log(' ' + c.green('āœ…') + ' API key validated');
786
879
  } else {
787
- console.log(' ' + c.red('āŒ') + ` API key invalid: ${result.error}`);
788
- issues.push('API key is invalid. Create a new one at app.teleportation.dev/api-keys');
880
+ if (result.isOrphan) {
881
+ console.log(' ' + c.yellow('āš ļø') + ` API key not linked: ${result.error}`);
882
+ warnings.push('Your API key is not linked to an account. Run `teleportation claim` to link it.');
883
+ } else {
884
+ console.log(' ' + c.red('āŒ') + ` API key invalid: ${result.error}`);
885
+ issues.push('API key is invalid. Create a new one at app.teleportation.dev/api-keys');
886
+ }
789
887
  }
790
888
  } catch (e) {
791
889
  console.log(' ' + c.yellow('āš ļø') + ' Could not validate API key');
@@ -798,13 +896,32 @@ async function commandStatus() {
798
896
  const hooksConfigured = config.isConfigured();
799
897
 
800
898
  if (hooksConfigured) {
801
- console.log(' ' + c.green('āœ…') + ' Enabled in Claude Code settings');
899
+ console.log(' ' + c.green('āœ…') + ' Claude Code hooks installed');
802
900
  const hookFiles = fs.readdirSync(config.globalHooks).filter(f => f.endsWith('.mjs'));
803
- console.log(' ' + c.green('āœ…') + ` ${hookFiles.length} hook files installed`);
804
- console.log(' Directory: ~/.claude/hooks/');
901
+ console.log(' ' + c.green('•') + ` ${hookFiles.length} files in ~/.claude/hooks/`);
902
+ } else {
903
+ console.log(' ' + c.red('āŒ') + ' Claude Code hooks not installed');
904
+ issues.push('Run `teleportation setup` to install Claude hooks');
905
+ }
906
+
907
+ // Gemini Hooks Status (Parity)
908
+ const globalGeminiHooks = path.join(HOME_DIR, '.gemini', 'hooks');
909
+ const globalGeminiSettings = path.join(HOME_DIR, '.gemini', 'settings.json');
910
+ const geminiHooksConfigured = fs.existsSync(globalGeminiSettings) && fs.existsSync(globalGeminiHooks);
911
+
912
+ if (geminiHooksConfigured) {
913
+ console.log(' ' + c.green('āœ…') + ' Gemini CLI hooks installed');
914
+ const geminiFiles = fs.readdirSync(globalGeminiHooks).filter(f => f.endsWith('.mjs'));
915
+ console.log(' ' + c.green('•') + ` ${geminiFiles.length} files in ~/.gemini/hooks/`);
805
916
  } else {
806
- console.log(' ' + c.red('āŒ') + ' Hooks not installed');
807
- issues.push('Run `teleportation setup` to install hooks');
917
+ try {
918
+ const { execSync } = require('child_process');
919
+ execSync('which gemini', { stdio: 'ignore' });
920
+ console.log(' ' + c.yellow('āš ļø') + ' Gemini CLI found but hooks not installed');
921
+ warnings.push('Run `teleportation setup` to install Gemini hooks');
922
+ } catch (e) {
923
+ // Gemini not installed, don't nag about hooks
924
+ }
808
925
  }
809
926
 
810
927
  // Config/credentials sync check
@@ -1101,22 +1218,43 @@ async function commandTest() {
1101
1218
  // Test 3: Relay service
1102
1219
  console.log(c.yellow('Test 3: Relay API Service'));
1103
1220
  const relayUrl = creds.RELAY_API_URL || 'https://api.teleportation.dev';
1104
- if (checkService('relay', 3030) && checkServiceHealth(relayUrl)) {
1221
+ const isLocalRelay = relayUrl.includes('localhost') || relayUrl.includes('127.0.0.1');
1222
+
1223
+ const relayHealthy = checkServiceHealth(relayUrl);
1224
+ // Only check local port if we expect a local relay
1225
+ const relayPortOpen = isLocalRelay ? checkService('relay', 3030) : true;
1226
+
1227
+ if (relayPortOpen && relayHealthy) {
1105
1228
  console.log(c.green(' āœ… PASS - Relay API running and healthy\n'));
1106
1229
  passed++;
1107
1230
  } else {
1108
- console.log(c.red(' āŒ FAIL - Relay API not running or unhealthy\n'));
1231
+ if (isLocalRelay && !relayPortOpen) {
1232
+ console.log(c.red(' āŒ FAIL - Local relay process not running (port 3030)\n'));
1233
+ } else if (!relayHealthy) {
1234
+ console.log(c.red(` āŒ FAIL - Relay API unreachable at ${relayUrl}\n`));
1235
+ } else {
1236
+ console.log(c.red(' āŒ FAIL - Relay API issue\n'));
1237
+ }
1109
1238
  failed++;
1110
1239
  }
1111
1240
 
1112
1241
  // Test 4: Storage service
1113
1242
  console.log(c.yellow('Test 4: Storage API Service'));
1114
- if (checkService('storage', 3040) && checkServiceHealth('http://localhost:3040')) {
1115
- console.log(c.green(' āœ… PASS - Storage API running and healthy\n'));
1116
- passed++;
1243
+ // Storage is usually local for dev, but might be remote in future.
1244
+ // For now, only check if we are in local dev mode (implied by local relay) or if explicitly checking local ports.
1245
+ // Actually, CLI users might not need Storage API directly running if using production relay (which handles storage).
1246
+
1247
+ if (isLocalRelay) {
1248
+ if (checkService('storage', 3040) && checkServiceHealth('http://localhost:3040')) {
1249
+ console.log(c.green(' āœ… PASS - Local Storage API running and healthy\n'));
1250
+ passed++;
1251
+ } else {
1252
+ console.log(c.red(' āŒ FAIL - Local Storage API not running or unhealthy\n'));
1253
+ failed++;
1254
+ }
1117
1255
  } else {
1118
- console.log(c.red(' āŒ FAIL - Storage API not running or unhealthy\n'));
1119
- failed++;
1256
+ console.log(c.green(' āœ… PASS - Using Remote Relay (Storage managed by Relay)\n'));
1257
+ passed++;
1120
1258
  }
1121
1259
 
1122
1260
  // Test 5: Hook execution
@@ -1124,8 +1262,28 @@ async function commandTest() {
1124
1262
  try {
1125
1263
  const testHook = path.join(config.globalHooks, 'pre_tool_use.mjs');
1126
1264
  if (fs.existsSync(testHook)) {
1127
- const testInput = '{"session_id":"test","tool_name":"Read","tool_input":{}}';
1128
- const envVars = `RELAY_API_URL="${creds.RELAY_API_URL || 'http://localhost:3030'}" RELAY_API_KEY="${creds.RELAY_API_KEY || 'dev-key-123'}"`;
1265
+ const testInput = '{"session_id":"test","tool_name":"Read","tool_input":{"file_path":"test.txt"}}';
1266
+ const relayUrl = creds.RELAY_API_URL || 'http://localhost:3030';
1267
+ const relayKey = creds.RELAY_API_KEY || 'dev-key-123';
1268
+ const envVars = `RELAY_API_URL="${relayUrl}" RELAY_API_KEY="${relayKey}"`;
1269
+
1270
+ // Register test session first so it appears correctly in UI
1271
+ try {
1272
+ await fetch(`${relayUrl}/api/sessions/register`, {
1273
+ method: 'POST',
1274
+ headers: {
1275
+ 'Content-Type': 'application/json',
1276
+ 'Authorization': `Bearer ${relayKey}`
1277
+ },
1278
+ body: JSON.stringify({
1279
+ session_id: 'test',
1280
+ meta: { project_name: 'teleportation-test', hostname: os.hostname() }
1281
+ })
1282
+ });
1283
+ } catch (e) {
1284
+ // Ignore registration errors for the hook test
1285
+ }
1286
+
1129
1287
  execSync(`echo '${testInput}' | ${envVars} node ${testHook}`, { stdio: 'ignore' });
1130
1288
  console.log(c.green(' āœ… PASS - Hook executes successfully\n'));
1131
1289
  passed++;
@@ -1158,50 +1316,74 @@ async function commandDoctor() {
1158
1316
  let checksPassed = 0;
1159
1317
  let checksFailed = 0;
1160
1318
 
1161
- // Check 1: Claude Code installation
1162
- console.log(c.yellow('1. Claude Code Installation'));
1319
+ // Check 1: Assistant installations
1320
+ console.log(c.yellow('1. Assistant Installations'));
1321
+ let anyAssistantFound = false;
1322
+
1323
+ // 1.1 Claude Code
1163
1324
  try {
1164
1325
  const claudeCodePath = execSync('which claude', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
1165
1326
  if (claudeCodePath) {
1166
- console.log(c.green(` āœ… Found: ${claudeCodePath}\n`));
1167
- checksPassed++;
1168
- } else {
1169
- console.log(c.yellow(' āš ļø Claude Code not found in PATH\n'));
1170
- issues.push('Claude Code not found');
1171
- recommendations.push('Install Claude Code or add it to your PATH');
1172
- checksFailed++;
1327
+ console.log(c.green(` āœ… Claude Code: ${claudeCodePath}`));
1328
+ anyAssistantFound = true;
1173
1329
  }
1174
- } catch (e) {
1175
- console.log(c.yellow(' āš ļø Could not detect Claude Code installation\n'));
1176
- issues.push('Claude Code detection failed');
1330
+ } catch (e) {}
1331
+
1332
+ // 1.2 Gemini CLI
1333
+ try {
1334
+ const geminiPath = execSync('which gemini', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
1335
+ if (geminiPath) {
1336
+ console.log(c.green(` āœ… Gemini CLI: ${geminiPath}`));
1337
+ anyAssistantFound = true;
1338
+ }
1339
+ } catch (e) {}
1340
+
1341
+ if (anyAssistantFound) {
1342
+ console.log('');
1343
+ checksPassed++;
1344
+ } else {
1345
+ console.log(c.red(' āŒ No supported AI coding assistants found (Claude/Gemini)\n'));
1346
+ issues.push('No AI assistants found');
1347
+ recommendations.push('Install Claude Code or Gemini CLI');
1177
1348
  checksFailed++;
1178
1349
  }
1179
1350
 
1180
1351
  // Check 2: Hooks installation
1181
1352
  console.log(c.yellow('2. Hooks Installation'));
1353
+
1354
+ // 2.1 Claude Hooks
1182
1355
  const hooksConfigured = config.isConfigured();
1183
1356
  if (hooksConfigured) {
1184
1357
  const hookFiles = fs.readdirSync(config.globalHooks).filter(f => f.endsWith('.mjs'));
1185
- console.log(c.green(` āœ… ${hookFiles.length} hooks installed\n`));
1186
- hookFiles.forEach(f => {
1187
- const hookPath = path.join(config.globalHooks, f);
1188
- const stats = fs.statSync(hookPath);
1189
- const isExecutable = (stats.mode & parseInt('111', 8)) !== 0;
1190
- if (isExecutable) {
1191
- console.log(c.green(` • ${f} (executable)\n`));
1192
- } else {
1193
- console.log(c.yellow(` • ${f} (not executable)\n`));
1194
- issues.push(`Hook ${f} is not executable`);
1195
- recommendations.push(`Run: chmod +x ${hookPath}`);
1196
- }
1197
- });
1198
- checksPassed++;
1358
+ console.log(c.green(` āœ… Claude: ${hookFiles.length} hooks installed`));
1199
1359
  } else {
1200
- console.log(c.red(' āŒ Hooks not configured\n'));
1201
- issues.push('Hooks not installed');
1202
- recommendations.push('Run: teleportation on');
1203
- checksFailed++;
1360
+ console.log(c.dim(' - Claude: hooks not installed'));
1361
+ }
1362
+
1363
+ // 2.2 Gemini Hooks
1364
+ const globalGeminiHooks = path.join(HOME_DIR, '.gemini', 'hooks');
1365
+ const globalGeminiSettings = path.join(HOME_DIR, '.gemini', 'settings.json');
1366
+ if (fs.existsSync(globalGeminiSettings) && fs.existsSync(globalGeminiHooks)) {
1367
+ const geminiFiles = fs.readdirSync(globalGeminiHooks).filter(f => f.endsWith('.mjs'));
1368
+ console.log(c.green(` āœ… Gemini: ${geminiFiles.length} hooks installed`));
1369
+ } else {
1370
+ console.log(c.dim(' - Gemini: hooks not installed'));
1371
+ }
1372
+
1373
+ // 2.3 UHR availability
1374
+ try {
1375
+ const uhrInstallerPath = path.join(TELEPORTATION_DIR, 'lib', 'install', 'uhr-installer.js');
1376
+ const { isUhrAvailable } = await import('file://' + uhrInstallerPath);
1377
+ if (await isUhrAvailable()) {
1378
+ console.log(c.green(' āœ… UHR (Universal Hook Registry) available'));
1379
+ } else {
1380
+ console.log(c.dim(' - UHR not installed (using legacy installer)'));
1381
+ }
1382
+ } catch (e) {
1383
+ console.log(c.dim(' - UHR check skipped'));
1204
1384
  }
1385
+ console.log('');
1386
+ checksPassed++;
1205
1387
 
1206
1388
  // Check 3: Credentials
1207
1389
  console.log(c.yellow('3. Credentials'));
@@ -1627,6 +1809,7 @@ async function commandLogin(args) {
1627
1809
  async function performLogin(manager, flags, positional) {
1628
1810
  let apiKey = flags['api-key'] || flags.k;
1629
1811
  let token = flags.token || flags.t;
1812
+ let refreshToken = flags['refresh-token'] || flags.rt;
1630
1813
  const relayApiUrl = flags['relay-url'] || flags.r || process.env.RELAY_API_URL || 'https://api.teleportation.dev';
1631
1814
 
1632
1815
  // Create backup before modifying credentials (only if credentials exist)
@@ -1685,8 +1868,23 @@ async function performLogin(manager, flags, positional) {
1685
1868
  console.log(c.green('āœ… Successfully authenticated with API key!\n'));
1686
1869
  console.log(c.cyan('Credentials saved to ~/.teleportation/credentials\n'));
1687
1870
 
1688
- // Auto-start daemon
1689
- console.log(c.cyan('Starting daemon for remote commands...'));
1871
+ // Kill any orphaned Claude processes spawned by the old daemon
1872
+ // These would be running tasks with old credentials
1873
+ try {
1874
+ const { execSync } = require('child_process');
1875
+ // Kill Claude processes spawned in task mode (have TELEPORTATION_TASK_MODE in their environment)
1876
+ // pkill returns exit code 1 if no processes found, so we ignore errors
1877
+ execSync('pkill -f "claude.*-p.*--output-format" 2>/dev/null || true', { stdio: 'ignore' });
1878
+ if (process.env.DEBUG) {
1879
+ console.log(c.dim('Cleaned up any orphaned Claude task processes.'));
1880
+ }
1881
+ } catch (e) {
1882
+ // Ignore errors - cleanup is best-effort
1883
+ }
1884
+
1885
+ // Restart daemon to pick up new credentials
1886
+ // (If daemon was running with old credentials, restart ensures it uses new ones)
1887
+ console.log(c.cyan('Restarting daemon with new credentials...'));
1690
1888
  try {
1691
1889
  const lifecyclePath = path.join(TELEPORTATION_DIR, 'lib', 'daemon', 'lifecycle.js');
1692
1890
  const { access } = await import('fs/promises');
@@ -1695,16 +1893,120 @@ async function performLogin(manager, flags, positional) {
1695
1893
  } catch {
1696
1894
  throw new Error('Daemon module not found. Please reinstall: npm install -g teleportation-cli');
1697
1895
  }
1698
- const { startDaemon } = await import('file://' + lifecyclePath);
1699
- const result = await startDaemon({ detached: true, silent: true });
1700
- console.log(c.green(`āœ… Daemon started (PID: ${result.pid})\n`));
1896
+ const { restartDaemon } = await import('file://' + lifecyclePath);
1897
+ const result = await restartDaemon({ stopTimeout: 5000, force: true });
1898
+ if (result.wasRunning) {
1899
+ console.log(c.green(`āœ… Daemon restarted with new credentials (PID: ${result.pid})\n`));
1900
+ } else {
1901
+ console.log(c.green(`āœ… Daemon started (PID: ${result.pid})\n`));
1902
+ }
1701
1903
  } catch (e) {
1702
- if (e.message && e.message.includes('already running')) {
1703
- console.log(c.green('āœ… Daemon already running\n'));
1904
+ console.log(c.yellow(`āš ļø Could not start daemon: ${e.message}`));
1905
+ console.log(c.cyan('You can start it manually with: teleportation daemon start\n'));
1906
+ }
1907
+
1908
+ console.log(c.yellow('āš ļø Restart Claude Code to apply changes to current session.\n'));
1909
+ return;
1910
+ } catch (error) {
1911
+ console.log(c.red(`āŒ Error: ${error.message}\n`));
1912
+ process.exit(1);
1913
+ }
1914
+ }
1915
+
1916
+ // If refresh token provided via flag (PRD-0019: JWT authentication)
1917
+ if (refreshToken) {
1918
+ console.log(c.yellow('Authenticating with JWT refresh token...\n'));
1919
+
1920
+ try {
1921
+ // Call POST /api/auth/refresh to validate and get access token
1922
+ console.log(c.cyan('Validating refresh token...'));
1923
+ const refreshResponse = await fetch(`${relayApiUrl}/api/auth/refresh`, {
1924
+ method: 'POST',
1925
+ headers: {
1926
+ 'Content-Type': 'application/json',
1927
+ },
1928
+ body: JSON.stringify({ refreshToken }),
1929
+ });
1930
+
1931
+ if (!refreshResponse.ok) {
1932
+ const errorData = await refreshResponse.json().catch(() => ({}));
1933
+ if (refreshResponse.status === 401) {
1934
+ console.log(c.red('āŒ Invalid or expired refresh token.'));
1935
+ console.log(c.cyan(' Get a new token from https://app.teleportation.dev\n'));
1936
+ } else if (refreshResponse.status === 503) {
1937
+ console.log(c.red('āŒ JWT authentication is not enabled on this relay.'));
1938
+ console.log(c.cyan(' Use --api-key for API key authentication instead.\n'));
1704
1939
  } else {
1705
- console.log(c.yellow(`āš ļø Could not start daemon: ${e.message}`));
1706
- console.log(c.cyan('You can start it manually with: teleportation daemon start\n'));
1940
+ console.log(c.red(`āŒ Failed to validate token: ${errorData.message || refreshResponse.statusText}\n`));
1941
+ }
1942
+ process.exit(1);
1943
+ }
1944
+
1945
+ const tokenData = await refreshResponse.json();
1946
+
1947
+ // Calculate when access token expires (server returns expiresIn in seconds)
1948
+ const accessTokenExpiresAt = Date.now() + (tokenData.expiresIn * 1000);
1949
+
1950
+ // Save credentials with JWT tokens
1951
+ const existingCredsForMerge = await manager.load().catch(() => null);
1952
+ const credentials = {
1953
+ ...(existingCredsForMerge && typeof existingCredsForMerge === 'object' ? existingCredsForMerge : {}),
1954
+ // JWT token fields
1955
+ refreshToken: tokenData.refreshToken, // Store the new rotated refresh token
1956
+ accessToken: tokenData.accessToken,
1957
+ accessTokenExpiresAt: accessTokenExpiresAt,
1958
+ refreshTokenId: tokenData.refreshTokenId,
1959
+ // Metadata
1960
+ relayApiUrl: relayApiUrl,
1961
+ authenticatedAt: Date.now(),
1962
+ method: 'jwt',
1963
+ // Clear legacy API key (optional - could keep for backward compat)
1964
+ // apiKey: undefined,
1965
+ };
1966
+
1967
+ await manager.save(credentials);
1968
+
1969
+ // Also sync config to match credentials
1970
+ try {
1971
+ const configManagerPath = path.join(TELEPORTATION_DIR, 'lib', 'config', 'manager.js');
1972
+ const { setConfigValue } = await import('file://' + configManagerPath);
1973
+ await setConfigValue('relay.url', relayApiUrl);
1974
+ } catch (configErr) {
1975
+ // Non-fatal - just warn
1976
+ console.log(c.yellow(`āš ļø Could not sync config: ${configErr.message}`));
1977
+ }
1978
+
1979
+ console.log(c.green('āœ… Successfully authenticated with JWT!\n'));
1980
+ console.log(c.cyan('Credentials saved to ~/.teleportation/credentials'));
1981
+ console.log(c.dim(`Access token expires in ${Math.floor(tokenData.expiresIn / 60)} minutes (auto-refreshes)\n`));
1982
+
1983
+ // Kill any orphaned Claude processes spawned by the old daemon
1984
+ try {
1985
+ const { execSync } = require('child_process');
1986
+ execSync('pkill -f "claude.*-p.*--output-format" 2>/dev/null || true', { stdio: 'ignore' });
1987
+ } catch (e) { /* Ignore */ }
1988
+
1989
+ // Restart daemon to pick up new credentials
1990
+ // (If daemon was running with old credentials, restart ensures it uses new ones)
1991
+ console.log(c.cyan('Restarting daemon with new credentials...'));
1992
+ try {
1993
+ const lifecyclePath = path.join(TELEPORTATION_DIR, 'lib', 'daemon', 'lifecycle.js');
1994
+ const { access } = await import('fs/promises');
1995
+ try {
1996
+ await access(lifecyclePath);
1997
+ } catch {
1998
+ throw new Error('Daemon module not found. Please reinstall: npm install -g teleportation-cli');
1999
+ }
2000
+ const { restartDaemon } = await import('file://' + lifecyclePath);
2001
+ const result = await restartDaemon({ stopTimeout: 5000, force: true });
2002
+ if (result.wasRunning) {
2003
+ console.log(c.green(`āœ… Daemon restarted with new credentials (PID: ${result.pid})\n`));
2004
+ } else {
2005
+ console.log(c.green(`āœ… Daemon started (PID: ${result.pid})\n`));
1707
2006
  }
2007
+ } catch (e) {
2008
+ console.log(c.yellow(`āš ļø Could not start daemon: ${e.message}`));
2009
+ console.log(c.cyan('You can start it manually with: teleportation daemon start\n'));
1708
2010
  }
1709
2011
 
1710
2012
  console.log(c.yellow('āš ļø Restart Claude Code to apply changes to current session.\n'));
@@ -1715,7 +2017,7 @@ async function performLogin(manager, flags, positional) {
1715
2017
  }
1716
2018
  }
1717
2019
 
1718
- // If token provided via flag
2020
+ // If token provided via flag (legacy - direct access token)
1719
2021
  if (token) {
1720
2022
  console.log(c.yellow('Authenticating with token...\n'));
1721
2023
 
@@ -1745,8 +2047,15 @@ async function performLogin(manager, flags, positional) {
1745
2047
  console.log(c.green('āœ… Successfully authenticated with token!\n'));
1746
2048
  console.log(c.cyan('Credentials saved to ~/.teleportation/credentials\n'));
1747
2049
 
1748
- // Auto-start daemon
1749
- console.log(c.cyan('Starting daemon for remote commands...'));
2050
+ // Kill any orphaned Claude processes spawned by the old daemon
2051
+ try {
2052
+ const { execSync } = require('child_process');
2053
+ execSync('pkill -f "claude.*-p.*--output-format" 2>/dev/null || true', { stdio: 'ignore' });
2054
+ } catch (e) { /* Ignore */ }
2055
+
2056
+ // Restart daemon to pick up new credentials
2057
+ // (If daemon was running with old credentials, restart ensures it uses new ones)
2058
+ console.log(c.cyan('Restarting daemon with new credentials...'));
1750
2059
  try {
1751
2060
  const lifecyclePath = path.join(TELEPORTATION_DIR, 'lib', 'daemon', 'lifecycle.js');
1752
2061
  const { access } = await import('fs/promises');
@@ -1755,16 +2064,16 @@ async function performLogin(manager, flags, positional) {
1755
2064
  } catch {
1756
2065
  throw new Error('Daemon module not found. Please reinstall: npm install -g teleportation-cli');
1757
2066
  }
1758
- const { startDaemon } = await import('file://' + lifecyclePath);
1759
- const result = await startDaemon({ detached: true, silent: true });
1760
- console.log(c.green(`āœ… Daemon started (PID: ${result.pid})\n`));
1761
- } catch (e) {
1762
- if (e.message && e.message.includes('already running')) {
1763
- console.log(c.green('āœ… Daemon already running\n'));
2067
+ const { restartDaemon } = await import('file://' + lifecyclePath);
2068
+ const result = await restartDaemon({ stopTimeout: 5000, force: true });
2069
+ if (result.wasRunning) {
2070
+ console.log(c.green(`āœ… Daemon restarted with new credentials (PID: ${result.pid})\n`));
1764
2071
  } else {
1765
- console.log(c.yellow(`āš ļø Could not start daemon: ${e.message}`));
1766
- console.log(c.cyan('You can start it manually with: teleportation daemon start\n'));
2072
+ console.log(c.green(`āœ… Daemon started (PID: ${result.pid})\n`));
1767
2073
  }
2074
+ } catch (e) {
2075
+ console.log(c.yellow(`āš ļø Could not start daemon: ${e.message}`));
2076
+ console.log(c.cyan('You can start it manually with: teleportation daemon start\n'));
1768
2077
  }
1769
2078
 
1770
2079
  console.log(c.yellow('āš ļø Restart Claude Code to apply changes to current session.\n'));
@@ -1792,8 +2101,10 @@ async function performLogin(manager, flags, positional) {
1792
2101
 
1793
2102
  if (!input || input.trim() === '') {
1794
2103
  console.log(c.yellow('\nāš ļø No API key provided.'));
1795
- console.log(c.cyan(' Use --api-key flag or --token flag for non-interactive login.\n'));
1796
- console.log(c.cyan(' Example: teleportation login --api-key YOUR_KEY\n'));
2104
+ console.log(c.cyan(' Use --api-key or --refresh-token flag for non-interactive login.\n'));
2105
+ console.log(c.cyan(' Examples:'));
2106
+ console.log(c.cyan(' teleportation login --api-key YOUR_KEY'));
2107
+ console.log(c.cyan(' teleportation login --refresh-token YOUR_JWT_TOKEN\n'));
1797
2108
  resolve();
1798
2109
  return;
1799
2110
  }
@@ -1825,8 +2136,15 @@ async function performLogin(manager, flags, positional) {
1825
2136
  console.log(c.green('āœ… Successfully authenticated!\n'));
1826
2137
  console.log(c.cyan('Credentials saved to ~/.teleportation/credentials\n'));
1827
2138
 
1828
- // Auto-start daemon
1829
- console.log(c.cyan('Starting daemon for remote commands...'));
2139
+ // Kill any orphaned Claude processes spawned by the old daemon
2140
+ try {
2141
+ const { execSync } = require('child_process');
2142
+ execSync('pkill -f "claude.*-p.*--output-format" 2>/dev/null || true', { stdio: 'ignore' });
2143
+ } catch (e) { /* Ignore */ }
2144
+
2145
+ // Restart daemon to pick up new credentials
2146
+ // (If daemon was running with old credentials, restart ensures it uses new ones)
2147
+ console.log(c.cyan('Restarting daemon with new credentials...'));
1830
2148
  try {
1831
2149
  const lifecyclePath = path.join(TELEPORTATION_DIR, 'lib', 'daemon', 'lifecycle.js');
1832
2150
  const { access } = await import('fs/promises');
@@ -1835,16 +2153,16 @@ async function performLogin(manager, flags, positional) {
1835
2153
  } catch {
1836
2154
  throw new Error('Daemon module not found. Please reinstall: npm install -g teleportation-cli');
1837
2155
  }
1838
- const { startDaemon } = await import('file://' + lifecyclePath);
1839
- const result = await startDaemon({ detached: true, silent: true });
1840
- console.log(c.green(`āœ… Daemon started (PID: ${result.pid})\n`));
1841
- } catch (e) {
1842
- if (e.message && e.message.includes('already running')) {
1843
- console.log(c.green('āœ… Daemon already running\n'));
2156
+ const { restartDaemon } = await import('file://' + lifecyclePath);
2157
+ const result = await restartDaemon({ stopTimeout: 5000, force: true });
2158
+ if (result.wasRunning) {
2159
+ console.log(c.green(`āœ… Daemon restarted with new credentials (PID: ${result.pid})\n`));
1844
2160
  } else {
1845
- console.log(c.yellow(`āš ļø Could not start daemon: ${e.message}`));
1846
- console.log(c.cyan('You can start it manually with: teleportation daemon start\n'));
2161
+ console.log(c.green(`āœ… Daemon started (PID: ${result.pid})\n`));
1847
2162
  }
2163
+ } catch (e) {
2164
+ console.log(c.yellow(`āš ļø Could not start daemon: ${e.message}`));
2165
+ console.log(c.cyan('You can start it manually with: teleportation daemon start\n'));
1848
2166
  }
1849
2167
 
1850
2168
  resolve();
@@ -2036,6 +2354,30 @@ async function validateGitHubToken(token) {
2036
2354
  return response.json();
2037
2355
  }
2038
2356
 
2357
+ /**
2358
+ * Claim an orphan API key by opening the dashboard
2359
+ */
2360
+ async function commandClaim() {
2361
+ console.log(c.purple('Teleportation: Claim Orphan API Key\n'));
2362
+
2363
+ const frontendUrl = process.env.FRONTEND_URL || 'https://app.teleportation.dev';
2364
+ const claimUrl = `${frontendUrl}/api-keys`;
2365
+
2366
+ console.log(c.cyan('If you have an old API key from before you logged in, you can link it'));
2367
+ console.log(c.cyan('to your account to migrate all your past sessions.\n'));
2368
+
2369
+ console.log(c.yellow(`Opening your browser to ${claimUrl}...\n`));
2370
+
2371
+ try {
2372
+ const { exec } = require('child_process');
2373
+ const startCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
2374
+ exec(`${startCmd} ${claimUrl}`);
2375
+ } catch (err) {
2376
+ console.log(c.red(`āŒ Failed to open browser: ${err.message}`));
2377
+ console.log(c.cyan(`Please visit ${claimUrl} manually to claim your key.`));
2378
+ }
2379
+ }
2380
+
2039
2381
  async function commandGithub(args) {
2040
2382
  const subCommand = args[0] || 'help';
2041
2383
  const { flags } = parseFlags(args.slice(1));
@@ -2340,6 +2682,89 @@ async function commandRemote(args) {
2340
2682
  }
2341
2683
  }
2342
2684
 
2685
+ async function commandTeleport(args) {
2686
+ const subCommand = args[0] || 'help';
2687
+
2688
+ // Parse flags
2689
+ const { flags, positional } = parseFlags(args.slice(1));
2690
+
2691
+ try {
2692
+ // Dynamically import teleport commands
2693
+ const teleportCommandsPath = path.join(TELEPORTATION_DIR, 'lib', 'cli', 'teleport-commands.js');
2694
+ const {
2695
+ commandTeleportStart,
2696
+ commandTeleportStatus,
2697
+ commandTeleportList,
2698
+ commandTeleportStop,
2699
+ commandTeleportHelp
2700
+ } = await import('file://' + teleportCommandsPath);
2701
+
2702
+ switch (subCommand) {
2703
+ case 'start':
2704
+ // Parse: teleportation teleport start --task "description" [--provider sprites|daytona] [--target-coder claude-code|gemini-cli] [--mode pause|fork]
2705
+ await commandTeleportStart({
2706
+ sessionId: flags['session-id'] || flags.id,
2707
+ task: flags.task,
2708
+ cwd: flags.cwd || process.cwd(),
2709
+ provider: flags.provider,
2710
+ targetCoder: flags['target-coder'] || flags.coder || 'claude-code',
2711
+ mode: flags.mode || 'pause',
2712
+ });
2713
+ break;
2714
+
2715
+ case 'status':
2716
+ // Parse: teleportation teleport status <teleport-id>
2717
+ const statusTeleportId = positional[0] || flags['teleport-id'] || flags.id;
2718
+ if (!statusTeleportId) {
2719
+ console.log(c.red('āŒ Error: Teleport ID is required\n'));
2720
+ console.log(c.cyan('Usage: teleportation teleport status <teleport-id>\n'));
2721
+ process.exit(1);
2722
+ }
2723
+ await commandTeleportStatus(statusTeleportId);
2724
+ break;
2725
+
2726
+ case 'list':
2727
+ case 'ls':
2728
+ // Parse: teleportation teleport list [--status running|completed|failed]
2729
+ await commandTeleportList({
2730
+ status: flags.status,
2731
+ });
2732
+ break;
2733
+
2734
+ case 'stop':
2735
+ // Parse: teleportation teleport stop <teleport-id> [--create-pr] [--base-branch main]
2736
+ const stopTeleportId = positional[0] || flags['teleport-id'] || flags.id;
2737
+ if (!stopTeleportId) {
2738
+ console.log(c.red('āŒ Error: Teleport ID is required\n'));
2739
+ console.log(c.cyan('Usage: teleportation teleport stop <teleport-id> [--create-pr]\n'));
2740
+ process.exit(1);
2741
+ }
2742
+ await commandTeleportStop(stopTeleportId, {
2743
+ createPR: flags['create-pr'] || false,
2744
+ baseBranch: flags['base-branch'],
2745
+ });
2746
+ break;
2747
+
2748
+ case 'help':
2749
+ case '--help':
2750
+ case '-h':
2751
+ commandTeleportHelp();
2752
+ break;
2753
+
2754
+ default:
2755
+ console.log(c.red(`Unknown teleport command: ${subCommand}\n`));
2756
+ console.log(c.cyan('Run "teleportation teleport help" for available commands\n'));
2757
+ process.exit(1);
2758
+ }
2759
+ } catch (error) {
2760
+ console.log(c.red(`āŒ Teleport command failed: ${error.message}\n`));
2761
+ if (process.env.DEBUG) {
2762
+ console.error(error.stack);
2763
+ }
2764
+ process.exit(1);
2765
+ }
2766
+ }
2767
+
2343
2768
  async function commandDaemon(args) {
2344
2769
  const subCommand = args[0] || 'status';
2345
2770
 
@@ -2675,334 +3100,75 @@ async function commandInboxAck(id) {
2675
3100
  }
2676
3101
 
2677
3102
  /**
2678
- * Install hooks globally to ~/.claude/hooks/
2679
- * This command copies hooks from the teleportation project to the global location
2680
- * and merges settings into ~/.claude/settings.json
3103
+ * Install hooks globally to ~/.claude/hooks/ and ~/.gemini/hooks/
3104
+ * This command uses the unified installer logic.
2681
3105
  */
2682
3106
  async function commandInstallHooks() {
2683
3107
  console.log(c.purple('šŸ”§ Installing Teleportation Hooks Globally\n'));
2684
3108
 
2685
- const globalHooksDir = path.join(HOME_DIR, '.claude', 'hooks');
2686
- const globalSettings = path.join(HOME_DIR, '.claude', 'settings.json');
2687
- const sourceHooksDir = path.join(TELEPORTATION_DIR, '.claude', 'hooks');
2688
-
2689
- // List of hooks to install
2690
- const hooks = [
2691
- 'pre_tool_use.mjs',
2692
- 'post_tool_use.mjs',
2693
- 'permission_request.mjs',
2694
- 'stop.mjs',
2695
- 'session_start.mjs',
2696
- 'session_end.mjs',
2697
- 'notification.mjs',
2698
- 'user_prompt_submit.mjs',
2699
- 'config-loader.mjs',
2700
- 'session-register.mjs',
2701
- 'heartbeat.mjs' // Spawned by session-register.mjs, needs to be in hooks directory
2702
- ];
2703
-
2704
- // Step 1: Ensure directories exist
2705
- console.log(c.yellow('Step 1: Creating directories...\n'));
2706
3109
  try {
2707
- if (!fs.existsSync(path.join(HOME_DIR, '.claude'))) {
2708
- fs.mkdirSync(path.join(HOME_DIR, '.claude'), { recursive: true });
2709
- }
2710
- if (!fs.existsSync(globalHooksDir)) {
2711
- fs.mkdirSync(globalHooksDir, { recursive: true });
2712
- }
2713
- console.log(c.green(' āœ… ~/.claude/hooks/ directory ready\n'));
2714
- } catch (e) {
2715
- console.log(c.red(` āŒ Failed to create directories: ${e.message}\n`));
2716
- process.exit(1);
2717
- }
2718
-
2719
- // Step 2: Copy hook files
2720
- console.log(c.yellow('Step 2: Copying hooks...\n'));
2721
- let installed = 0;
2722
- let failed = 0;
2723
-
2724
- for (const hook of hooks) {
2725
- const src = path.join(sourceHooksDir, hook);
2726
- const dest = path.join(globalHooksDir, hook);
2727
-
3110
+ // Try UHR first
3111
+ let installed = false;
2728
3112
  try {
2729
- if (!fs.existsSync(src)) {
2730
- console.log(c.yellow(` āš ļø ${hook} not found in source, skipping`));
2731
- continue;
3113
+ const uhrInstallerPath = path.join(TELEPORTATION_DIR, 'lib', 'install', 'uhr-installer.js');
3114
+ const { isUhrAvailable, installViaUhr } = await import('file://' + uhrInstallerPath);
3115
+
3116
+ if (await isUhrAvailable()) {
3117
+ console.log(c.dim(' Using UHR (Universal Hook Registry)...'));
3118
+ const manifestPath = path.join(TELEPORTATION_DIR, 'teleportation.uhr.json');
3119
+ const hooksDir = path.join(TELEPORTATION_DIR, '.claude', 'hooks');
3120
+ const uhrResult = await installViaUhr(manifestPath, hooksDir);
3121
+
3122
+ if (uhrResult.success) {
3123
+ console.log(c.green(' āœ… Hooks installed via UHR'));
3124
+ if (uhrResult.warnings.length > 0) {
3125
+ uhrResult.warnings.forEach(w => console.log(c.yellow(` āš ļø ${w}`)));
3126
+ }
3127
+ installed = true;
3128
+ } else {
3129
+ console.log(c.yellow(` āš ļø UHR install failed: ${uhrResult.reason}`));
3130
+ console.log(c.dim(' Falling back to legacy installer...'));
3131
+ }
2732
3132
  }
2733
-
2734
- fs.copyFileSync(src, dest);
2735
- fs.chmodSync(dest, 0o755); // Make executable
2736
- console.log(c.green(` āœ… ${hook}`));
2737
- installed++;
2738
3133
  } catch (e) {
2739
- console.log(c.red(` āŒ ${hook}: ${e.message}`));
2740
- failed++;
3134
+ // UHR not available
2741
3135
  }
2742
- }
2743
3136
 
2744
- console.log(`\n Installed: ${c.green(installed)}, Failed: ${failed > 0 ? c.red(failed) : '0'}\n`);
2745
-
2746
- // Step 2.5: Install lib files that hooks depend on (metadata.js, etc.)
2747
- console.log(c.yellow('Step 2.5: Installing lib files...\n'));
2748
- try {
2749
- const installerPath = path.join(TELEPORTATION_DIR, 'lib', 'install', 'installer.js');
2750
- if (fs.existsSync(installerPath)) {
2751
- const installer = await import('file://' + installerPath);
2752
- const libResult = await installer.installLibFiles();
2753
- if (libResult.installed.length > 0) {
2754
- libResult.installed.forEach(file => {
2755
- console.log(c.green(` āœ… ${file}`));
2756
- });
2757
- }
2758
- if (libResult.failed.length > 0) {
2759
- libResult.failed.forEach(({ file, error }) => {
2760
- console.log(c.yellow(` āš ļø ${file}: ${error}`));
2761
- });
2762
- }
2763
- console.log(`\n Lib files installed: ${c.green(libResult.installed.length)}, Failed: ${libResult.failed.length > 0 ? c.yellow(libResult.failed.length) : '0'}\n`);
2764
- } else {
2765
- console.log(c.yellow(' āš ļø Installer module not found, skipping lib files\n'));
2766
- }
2767
- } catch (e) {
2768
- console.log(c.yellow(` āš ļø Failed to install lib files: ${e.message}\n`));
2769
- }
2770
-
2771
- // Step 3: Update settings.json
2772
- console.log(c.yellow('Step 3: Updating Claude Code settings...\n'));
2773
-
2774
- try {
2775
- // Use SettingsManager for proper hook management
2776
- const settingsManagerPath = path.join(TELEPORTATION_DIR, 'lib', 'settings', 'manager.js');
2777
- const { SettingsManager } = await import('file://' + settingsManagerPath);
2778
- const settingsManager = new SettingsManager(globalSettings);
2779
-
2780
- // Remove ALL existing Teleportation hooks (regardless of path)
2781
- // This prevents accumulation of hooks from different installation locations
2782
- const removeResult = await settingsManager.removeTeleportationHooks();
2783
- if (removeResult.hooksRemoved > 0) {
2784
- console.log(c.cyan(` Removed ${removeResult.hooksRemoved} existing Teleportation hook(s)\n`));
2785
- }
2786
-
2787
- // Add new hooks pointing to global hooks directory
2788
- const addResult = await settingsManager.addHooks(globalHooksDir);
2789
- console.log(c.green(` āœ… Added ${addResult.hooksAdded} hook(s) to settings\n`));
2790
-
2791
- // Deduplicate in case there are any remaining duplicates
2792
- const dedupeResult = await settingsManager.deduplicate();
2793
- if (dedupeResult.duplicatesRemoved > 0) {
2794
- console.log(c.cyan(` Removed ${dedupeResult.duplicatesRemoved} duplicate hook(s)\n`));
2795
- }
2796
- } catch (e) {
2797
- console.log(c.red(` āŒ Failed to update settings: ${e.message}\n`));
2798
-
2799
- try {
2800
- // Fallback to manual merge if SettingsManager fails
2801
- console.log(c.yellow(' Attempting fallback method...\n'));
2802
-
2803
- const quotePath = (p) => JSON.stringify(p);
2804
-
2805
- const hooksConfig = {
2806
- PreToolUse: [{
2807
- matcher: ".*",
2808
- hooks: [{
2809
- type: "command",
2810
- command: `node ${quotePath(path.join(globalHooksDir, 'pre_tool_use.mjs'))}`
2811
- }]
2812
- }],
2813
- PostToolUse: [{
2814
- matcher: ".*",
2815
- hooks: [{
2816
- type: "command",
2817
- command: `node ${quotePath(path.join(globalHooksDir, 'post_tool_use.mjs'))}`
2818
- }]
2819
- }],
2820
- PermissionRequest: [{
2821
- matcher: ".*",
2822
- hooks: [{
2823
- type: "command",
2824
- command: `node ${quotePath(path.join(globalHooksDir, 'permission_request.mjs'))}`
2825
- }]
2826
- }],
2827
- Stop: [{
2828
- matcher: ".*",
2829
- hooks: [{
2830
- type: "command",
2831
- command: `node ${quotePath(path.join(globalHooksDir, 'stop.mjs'))}`
2832
- }]
2833
- }],
2834
- SessionStart: [{
2835
- matcher: ".*",
2836
- hooks: [{
2837
- type: "command",
2838
- command: `node ${quotePath(path.join(globalHooksDir, 'session_start.mjs'))}`
2839
- }]
2840
- }],
2841
- SessionEnd: [{
2842
- matcher: ".*",
2843
- hooks: [{
2844
- type: "command",
2845
- command: `node ${quotePath(path.join(globalHooksDir, 'session_end.mjs'))}`
2846
- }]
2847
- }],
2848
- Notification: [{
2849
- matcher: ".*",
2850
- hooks: [{
2851
- type: "command",
2852
- command: `node ${quotePath(path.join(globalHooksDir, 'notification.mjs'))}`
2853
- }]
2854
- }],
2855
- UserPromptSubmit: [{
2856
- matcher: ".*",
2857
- hooks: [{
2858
- type: "command",
2859
- command: `node ${quotePath(path.join(globalHooksDir, 'user_prompt_submit.mjs'))}`
2860
- }]
2861
- }]
2862
- };
3137
+ if (!installed) {
3138
+ // Legacy installer fallback
3139
+ const installerPath = path.join(TELEPORTATION_DIR, 'lib', 'install', 'installer.js');
3140
+ const { install } = await import('file://' + installerPath);
3141
+ const sourceHooksDir = path.join(TELEPORTATION_DIR, '.claude', 'hooks');
2863
3142
 
2864
- let existingSettings = {};
3143
+ const result = await install(sourceHooksDir);
2865
3144
 
2866
- // Load existing settings if present
2867
- if (fs.existsSync(globalSettings)) {
2868
- try {
2869
- const content = fs.readFileSync(globalSettings, 'utf8');
2870
- existingSettings = JSON.parse(content);
2871
- console.log(c.cyan(' Found existing settings, merging...\n'));
2872
- } catch (err) {
2873
- console.log(c.yellow(` āš ļø Could not parse existing settings, creating new...\n`));
3145
+ if (result.hooksInstalled > 0) {
3146
+ console.log(c.green(` āœ… ${result.hooksInstalled} Claude Code hooks installed`));
3147
+ console.log(c.dim(' Directory: ~/.claude/hooks/'));
2874
3148
  }
2875
- }
2876
-
2877
- // Load isTeleportationHook pattern for filtering
2878
- let isTeleportationHook;
2879
- try {
2880
- const settingsManagerPath = path.join(TELEPORTATION_DIR, 'lib', 'settings', 'manager.js');
2881
- const settingsModule = await import('file://' + settingsManagerPath);
2882
- isTeleportationHook = settingsModule.isTeleportationHook;
2883
- } catch (err) {
2884
- // Log the specific error for debugging
2885
- console.log(c.yellow(` āš ļø Could not load SettingsManager: ${err.message}`));
2886
- console.log(c.yellow(' Using simplified hook detection pattern\n'));
2887
-
2888
- // More robust fallback pattern matching the regex in SettingsManager
2889
- // Matches: .claude/hooks/(pre_tool_use|post_tool_use|permission_request|stop|session_start|session_end|notification|user_prompt_submit).mjs
2890
- isTeleportationHook = (cmd) => {
2891
- if (!cmd || typeof cmd !== 'string') return false;
2892
- return /\.claude\/hooks\/(pre_tool_use|post_tool_use|permission_request|stop|session_start|session_end|notification|user_prompt_submit)\.mjs/.test(cmd);
2893
- };
2894
- }
2895
-
2896
- // Merge hooks - preserve NON-teleportation user hooks, remove ALL teleportation hooks
2897
- const mergeHookArrays = (existing, incoming) => {
2898
- if (!existing || !Array.isArray(existing)) return incoming;
2899
- if (!incoming || !Array.isArray(incoming)) return existing;
2900
-
2901
- // Filter out ALL teleportation hooks (regardless of path)
2902
- const nonTeleportationHooks = existing.filter(matcher => {
2903
- if (!matcher.hooks || !Array.isArray(matcher.hooks)) return true;
2904
- // Keep matcher only if it has non-teleportation hooks
2905
- return !matcher.hooks.every(h => h.command && isTeleportationHook(h.command));
2906
- });
2907
-
2908
- // Combine: existing (non-teleportation) + incoming (new teleportation hooks)
2909
- return [...nonTeleportationHooks, ...incoming];
2910
- };
2911
-
2912
- // Merge all hook types with warnings about user hooks
2913
- const mergedHooks = { ...(existingSettings.hooks || {}) };
2914
- let hasUserHooks = false;
2915
-
2916
- for (const [hookType, hookConfig] of Object.entries(hooksConfig)) {
2917
- const existingHooksForType = existingSettings.hooks?.[hookType] || [];
2918
-
2919
- // Find user-defined hooks (not from teleportation)
2920
- const userHooks = existingHooksForType.filter(h => {
2921
- if (!h.hooks || !Array.isArray(h.hooks)) return true;
2922
- // Keep only hooks that are NOT teleportation hooks
2923
- return !h.hooks.every(hh => hh.command && isTeleportationHook(hh.command));
2924
- });
2925
-
2926
- if (userHooks.length > 0) {
2927
- hasUserHooks = true;
2928
- console.log(c.yellow(` āš ļø Preserving ${userHooks.length} custom ${hookType} hook(s):`));
2929
- userHooks.forEach(h => {
2930
- const cmds = (h.hooks || []).map(hh => hh.command || 'unknown');
2931
- cmds.forEach(cmd => console.log(c.dim(` • ${cmd}`)));
2932
- });
2933
- console.log('');
3149
+ if (result.geminiHooksInstalled > 0) {
3150
+ console.log(c.green(` āœ… ${result.geminiHooksInstalled} Gemini CLI hooks installed`));
3151
+ console.log(c.dim(' Directory: ~/.gemini/hooks/'));
2934
3152
  }
2935
-
2936
- mergedHooks[hookType] = mergeHookArrays(existingHooksForType, hookConfig);
2937
- }
2938
-
2939
- if (hasUserHooks) {
2940
- console.log(c.cyan(' Your custom hooks will continue to work alongside Teleportation hooks.\n'));
2941
- }
2942
-
2943
- const mergedSettings = {
2944
- ...existingSettings,
2945
- hooks: mergedHooks
2946
- };
2947
-
2948
- // Write settings
2949
- fs.writeFileSync(globalSettings, JSON.stringify(mergedSettings, null, 2));
2950
- console.log(c.green(' āœ… ~/.claude/settings.json updated\n'));
2951
-
2952
- // Deduplicate in fallback path (Gap #2)
2953
- console.log(c.yellow(' Running deduplication check...\n'));
2954
- let duplicatesRemoved = 0;
2955
-
2956
- for (const [hookType, matchers] of Object.entries(mergedSettings.hooks || {})) {
2957
- if (!Array.isArray(matchers)) continue;
2958
-
2959
- const seenCommands = new Set();
2960
- const uniqueMatchers = [];
2961
3153
 
2962
- for (const matcher of matchers) {
2963
- const commands = (matcher.hooks || []).map(h => h.command).filter(Boolean);
2964
- const isUnique = !commands.some(cmd => seenCommands.has(cmd));
2965
-
2966
- if (isUnique) {
2967
- uniqueMatchers.push(matcher);
2968
- commands.forEach(cmd => seenCommands.add(cmd));
2969
- } else {
2970
- duplicatesRemoved++;
2971
- }
3154
+ if (result.libFilesInstalled > 0) {
3155
+ console.log(c.green(` āœ… ${result.libFilesInstalled} shared library files installed`));
2972
3156
  }
2973
-
2974
- mergedSettings.hooks[hookType] = uniqueMatchers;
2975
3157
  }
2976
3158
 
2977
- if (duplicatesRemoved > 0) {
2978
- fs.writeFileSync(globalSettings, JSON.stringify(mergedSettings, null, 2));
2979
- console.log(c.cyan(` Removed ${duplicatesRemoved} duplicate(s) in fallback\n`));
2980
- } else {
2981
- console.log(c.green(' āœ… No duplicates found\n'));
2982
- }
2983
- } catch (fallbackErr) {
2984
- console.log(c.red(` āŒ Fallback also failed: ${fallbackErr.message}\n`));
2985
- console.log(c.red(' Please report this issue at: https://github.com/dundas/teleportation-private/issues\n'));
2986
- process.exit(1);
2987
- }
2988
- }
2989
-
2990
- // Summary
2991
- console.log(c.cyan('╭─────────────────────────────────────────────────────╮'));
2992
- console.log(c.cyan('│ │'));
2993
- console.log(c.cyan('│ šŸŽ‰ ') + c.green('Hooks Installed Successfully!') + c.cyan(' │'));
2994
- console.log(c.cyan('│ │'));
2995
- console.log(c.cyan('│ Hooks location: ~/.claude/hooks/ │'));
2996
- console.log(c.cyan('│ Settings: ~/.claude/settings.json │'));
2997
- console.log(c.cyan('│ │'));
2998
- console.log(c.cyan('│ ') + c.yellow('āš ļø Restart Claude Code') + c.cyan(' to activate hooks. │'));
2999
- console.log(c.cyan('│ │'));
3000
- console.log(c.cyan('╰─────────────────────────────────────────────────────╯\n'));
3159
+ console.log(c.cyan('\n╭─────────────────────────────────────────────────────╮'));
3160
+ console.log(c.cyan('│ │'));
3161
+ console.log(c.cyan('│ šŸŽ‰ ') + c.green('Hooks Installed Successfully!') + c.cyan(' │'));
3162
+ console.log(c.cyan('│ │'));
3163
+ console.log(c.cyan('│ ') + c.yellow('āš ļø Important:') + c.cyan(' Restart your AI assistants to │'));
3164
+ console.log(c.cyan('│ activate the new hooks. │'));
3165
+ console.log(c.cyan('│ │'));
3166
+ console.log(c.cyan('╰─────────────────────────────────────────────────────╯\n'));
3001
3167
 
3002
- console.log(c.cyan('Next steps:'));
3003
- console.log(' 1. If not logged in: teleportation login');
3004
- console.log(' 2. Check status: teleportation status');
3005
- console.log(' 3. Restart Claude Code to apply hooks\n');
3168
+ } catch (error) {
3169
+ console.log(c.red(`\nāŒ Installation failed: ${error.message}\n`));
3170
+ process.exit(1);
3171
+ }
3006
3172
  }
3007
3173
 
3008
3174
  async function commandCommand() {
@@ -3301,7 +3467,7 @@ const command = process.argv[2] || 'help';
3301
3467
  const args = process.argv.slice(3);
3302
3468
 
3303
3469
  // Handle async commands that need to complete before exit
3304
- const asyncCommands = ['login', 'logout', 'status', 'test', 'env', 'config', 'daemon', 'away', 'back', 'daemon-status', 'command', 'inbox', 'inbox-ack', 'install-hooks', 'update', 'remote'];
3470
+ const asyncCommands = ['login', 'logout', 'status', 'test', 'env', 'config', 'daemon', 'away', 'back', 'daemon-status', 'command', 'inbox', 'inbox-ack', 'install-hooks', 'update', 'remote', 'off'];
3305
3471
  // Keep this list in sync with switch cases below
3306
3472
  asyncCommands.push('github');
3307
3473
  if (asyncCommands.includes(command)) {
@@ -3335,7 +3501,10 @@ try {
3335
3501
  });
3336
3502
  break;
3337
3503
  case 'off':
3338
- commandOff();
3504
+ commandOff().catch(err => {
3505
+ console.log(c.red('Error: ' + err.message));
3506
+ process.exit(1);
3507
+ });
3339
3508
  break;
3340
3509
  case 'status':
3341
3510
  commandStatus().catch(err => {
@@ -3394,6 +3563,12 @@ try {
3394
3563
  process.exit(1);
3395
3564
  });
3396
3565
  break;
3566
+ case 'claim':
3567
+ commandClaim().catch(err => {
3568
+ console.error(c.red('āŒ Error:'), err.message);
3569
+ process.exit(1);
3570
+ });
3571
+ break;
3397
3572
  case 'github':
3398
3573
  commandGithub(args).catch(err => {
3399
3574
  console.error(c.red('āŒ Error:'), err.message);
@@ -3472,6 +3647,12 @@ try {
3472
3647
  process.exit(1);
3473
3648
  });
3474
3649
  break;
3650
+ case 'teleport':
3651
+ commandTeleport(args).catch(err => {
3652
+ console.error(c.red('āŒ Error:'), err.message);
3653
+ process.exit(1);
3654
+ });
3655
+ break;
3475
3656
  case 'version':
3476
3657
  case '--version':
3477
3658
  case '-v':