teleportation-cli 1.1.5 → 1.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.
Files changed (42) hide show
  1. package/.claude/hooks/permission_request.mjs +326 -59
  2. package/.claude/hooks/post_tool_use.mjs +90 -0
  3. package/.claude/hooks/pre_tool_use.mjs +212 -293
  4. package/.claude/hooks/session-register.mjs +89 -104
  5. package/.claude/hooks/session_end.mjs +41 -42
  6. package/.claude/hooks/session_start.mjs +45 -60
  7. package/.claude/hooks/stop.mjs +752 -99
  8. package/.claude/hooks/user_prompt_submit.mjs +26 -3
  9. package/lib/cli/daemon-commands.js +1 -1
  10. package/lib/cli/teleport-commands.js +469 -0
  11. package/lib/daemon/daemon-v2.js +104 -0
  12. package/lib/daemon/lifecycle.js +56 -171
  13. package/lib/daemon/services/index.js +3 -0
  14. package/lib/daemon/services/polling-service.js +173 -0
  15. package/lib/daemon/services/queue-service.js +318 -0
  16. package/lib/daemon/services/session-service.js +115 -0
  17. package/lib/daemon/state.js +35 -0
  18. package/lib/daemon/task-executor-v2.js +413 -0
  19. package/lib/daemon/task-executor.js +270 -96
  20. package/lib/daemon/teleportation-daemon.js +709 -126
  21. package/lib/daemon/timeline-analyzer.js +215 -0
  22. package/lib/daemon/transcript-ingestion.js +696 -0
  23. package/lib/daemon/utils.js +91 -0
  24. package/lib/install/installer.js +184 -20
  25. package/lib/install/uhr-installer.js +136 -0
  26. package/lib/remote/providers/base-provider.js +46 -0
  27. package/lib/remote/providers/daytona-provider.js +58 -0
  28. package/lib/remote/providers/provider-factory.js +90 -19
  29. package/lib/remote/providers/sprites-provider.js +711 -0
  30. package/lib/teleport/exporters/claude-exporter.js +302 -0
  31. package/lib/teleport/exporters/gemini-exporter.js +307 -0
  32. package/lib/teleport/exporters/index.js +93 -0
  33. package/lib/teleport/exporters/interface.js +153 -0
  34. package/lib/teleport/fork-tracker.js +415 -0
  35. package/lib/teleport/git-committer.js +337 -0
  36. package/lib/teleport/index.js +48 -0
  37. package/lib/teleport/manager.js +620 -0
  38. package/lib/teleport/session-capture.js +282 -0
  39. package/package.json +6 -2
  40. package/teleportation-cli.cjs +488 -453
  41. package/.claude/hooks/heartbeat.mjs +0 -396
  42. package/lib/daemon/pid-manager.js +0 -183
@@ -268,6 +268,13 @@ function commandHelp() {
268
268
  console.log(' ' + c.green('remote pull') + ' Pull results from remote session');
269
269
  console.log(' ' + c.green('remote help') + ' Show remote commands help\n');
270
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
+
271
278
  console.log(c.yellow('Session Isolation:'));
272
279
  console.log(' ' + c.green('worktree create') + ' Create isolated worktree for a session');
273
280
  console.log(' ' + c.green('worktree list') + ' List all session worktrees');
@@ -296,55 +303,65 @@ function commandHelp() {
296
303
 
297
304
  async function commandOn() {
298
305
  console.log(c.yellow('šŸš€ Enabling Teleportation Remote Control...\n'));
299
-
306
+
300
307
  try {
301
- // Use installer module
302
- const installerPath = path.join(TELEPORTATION_DIR, 'lib', 'install', 'installer.js');
303
- const { install, checkNodeVersion, checkClaudeCode } = await import('file://' + installerPath);
304
-
305
- // Pre-flight checks
306
- const nodeCheck = checkNodeVersion();
307
- if (!nodeCheck.valid) {
308
- console.log(c.red(`āŒ ${nodeCheck.error}\n`));
309
- return;
310
- }
311
- console.log(c.green(`āœ… Node.js ${nodeCheck.version}\n`));
312
-
313
- const claudeCheck = checkClaudeCode();
314
- if (!claudeCheck.valid) {
315
- console.log(c.yellow(`āš ļø ${claudeCheck.error}\n`));
316
- 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
+ }
317
329
  } else {
318
- console.log(c.green(`āœ… Claude Code found: ${claudeCheck.path}\n`));
330
+ await _legacyInstall();
319
331
  }
320
-
321
- // Install hooks
322
- const sourceHooksDir = path.join(TELEPORTATION_DIR, '.claude', 'hooks');
323
- if (!fs.existsSync(sourceHooksDir)) {
324
- console.log(c.red(`āŒ Hooks not found at ${sourceHooksDir}\n`));
325
- return;
326
- }
327
-
328
- const result = await install(sourceHooksDir);
329
-
330
- console.log(c.green('\nšŸŽ‰ Teleportation Remote Control ENABLED!'));
331
- console.log(c.cyan('\nInstallation Summary:'));
332
- console.log(` Hooks verified: ${c.green(result.hooksVerified)}`);
333
- console.log(` Daemon installed: ${c.green(result.daemonInstalled + ' files')}`);
334
- console.log(` Settings file: ${c.green(result.settingsFile)}`);
335
- console.log(` Hooks directory: ${c.green(result.hooksDir)}`);
336
- console.log(` Daemon directory: ${c.green(result.daemonDir)}`);
332
+
337
333
  console.log(c.cyan('\nNext steps:'));
338
334
  console.log(' 1. Login: teleportation login');
339
335
  console.log(' 2. Check status: teleportation status');
340
336
  console.log(' 3. Run diagnostics: teleportation doctor\n');
341
-
337
+
342
338
  } catch (error) {
343
339
  console.log(c.red(`āŒ Installation failed: ${error.message}\n`));
344
340
  process.exit(1);
345
341
  }
346
342
  }
347
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
+
348
365
  /**
349
366
  * Setup wizard - guided onboarding for new users
350
367
  * Creates backup before making changes, validates API key, installs hooks
@@ -458,7 +475,18 @@ async function commandSetup() {
458
475
 
459
476
  // Step 2: Configuration
460
477
  console.log('\n' + c.purple('Step 2 of 5: Configuration\n'));
461
- 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)}`);
462
490
 
463
491
  // Test relay connectivity
464
492
  console.log(c.cyan(' Testing connectivity...'));
@@ -505,14 +533,50 @@ async function commandSetup() {
505
533
 
506
534
  // Step 3: Install Hooks
507
535
  console.log('\n' + c.purple('Step 3 of 5: Installing Hooks\n'));
508
- console.log(' Installing Claude Code hooks to ~/.claude/hooks/');
536
+ console.log(' Installing Teleportation hooks...');
509
537
 
510
538
  try {
511
- const installerPath = path.join(TELEPORTATION_DIR, 'lib', 'install', 'installer.js');
512
- const { install } = await import('file://' + installerPath);
513
- const sourceHooksDir = path.join(TELEPORTATION_DIR, '.claude', 'hooks');
514
- const result = await install(sourceHooksDir);
515
- 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
+ }
516
580
  } catch (e) {
517
581
  console.log(c.red(` āŒ Failed to install hooks: ${e.message}`));
518
582
  console.log(c.yellow('\n Would you like to restore your previous configuration?'));
@@ -697,24 +761,42 @@ async function commandBackup(args) {
697
761
  }
698
762
  }
699
763
 
700
- function commandOff() {
764
+ async function commandOff() {
701
765
  console.log(c.yellow('šŸ›‘ Disabling Teleportation Remote Control...\n'));
702
-
703
- // Remove settings.json
704
- if (fs.existsSync(config.globalSettings)) {
705
- fs.unlinkSync(config.globalSettings);
706
- 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
707
782
  }
708
-
709
- // Remove hooks
710
- if (fs.existsSync(config.globalHooks)) {
711
- const hooks = fs.readdirSync(config.globalHooks).filter(f => f.endsWith('.mjs'));
712
- hooks.forEach(hook => {
713
- fs.unlinkSync(path.join(config.globalHooks, hook));
714
- });
715
- 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
+ }
716
798
  }
717
-
799
+
718
800
  console.log(c.yellow('\nšŸ›‘ Teleportation Remote Control DISABLED'));
719
801
  console.log(c.cyan('Services are still running. Stop with: ./teleportation stop\n'));
720
802
  }
@@ -814,13 +896,32 @@ async function commandStatus() {
814
896
  const hooksConfigured = config.isConfigured();
815
897
 
816
898
  if (hooksConfigured) {
817
- console.log(' ' + c.green('āœ…') + ' Enabled in Claude Code settings');
899
+ console.log(' ' + c.green('āœ…') + ' Claude Code hooks installed');
818
900
  const hookFiles = fs.readdirSync(config.globalHooks).filter(f => f.endsWith('.mjs'));
819
- console.log(' ' + c.green('āœ…') + ` ${hookFiles.length} hook files installed`);
820
- 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/`);
821
916
  } else {
822
- console.log(' ' + c.red('āŒ') + ' Hooks not installed');
823
- 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
+ }
824
925
  }
825
926
 
826
927
  // Config/credentials sync check
@@ -1117,22 +1218,43 @@ async function commandTest() {
1117
1218
  // Test 3: Relay service
1118
1219
  console.log(c.yellow('Test 3: Relay API Service'));
1119
1220
  const relayUrl = creds.RELAY_API_URL || 'https://api.teleportation.dev';
1120
- 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) {
1121
1228
  console.log(c.green(' āœ… PASS - Relay API running and healthy\n'));
1122
1229
  passed++;
1123
1230
  } else {
1124
- 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
+ }
1125
1238
  failed++;
1126
1239
  }
1127
1240
 
1128
1241
  // Test 4: Storage service
1129
1242
  console.log(c.yellow('Test 4: Storage API Service'));
1130
- if (checkService('storage', 3040) && checkServiceHealth('http://localhost:3040')) {
1131
- console.log(c.green(' āœ… PASS - Storage API running and healthy\n'));
1132
- 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
+ }
1133
1255
  } else {
1134
- console.log(c.red(' āŒ FAIL - Storage API not running or unhealthy\n'));
1135
- failed++;
1256
+ console.log(c.green(' āœ… PASS - Using Remote Relay (Storage managed by Relay)\n'));
1257
+ passed++;
1136
1258
  }
1137
1259
 
1138
1260
  // Test 5: Hook execution
@@ -1140,8 +1262,28 @@ async function commandTest() {
1140
1262
  try {
1141
1263
  const testHook = path.join(config.globalHooks, 'pre_tool_use.mjs');
1142
1264
  if (fs.existsSync(testHook)) {
1143
- const testInput = '{"session_id":"test","tool_name":"Read","tool_input":{}}';
1144
- 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
+
1145
1287
  execSync(`echo '${testInput}' | ${envVars} node ${testHook}`, { stdio: 'ignore' });
1146
1288
  console.log(c.green(' āœ… PASS - Hook executes successfully\n'));
1147
1289
  passed++;
@@ -1174,50 +1316,74 @@ async function commandDoctor() {
1174
1316
  let checksPassed = 0;
1175
1317
  let checksFailed = 0;
1176
1318
 
1177
- // Check 1: Claude Code installation
1178
- 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
1179
1324
  try {
1180
1325
  const claudeCodePath = execSync('which claude', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
1181
1326
  if (claudeCodePath) {
1182
- console.log(c.green(` āœ… Found: ${claudeCodePath}\n`));
1183
- checksPassed++;
1184
- } else {
1185
- console.log(c.yellow(' āš ļø Claude Code not found in PATH\n'));
1186
- issues.push('Claude Code not found');
1187
- recommendations.push('Install Claude Code or add it to your PATH');
1188
- checksFailed++;
1327
+ console.log(c.green(` āœ… Claude Code: ${claudeCodePath}`));
1328
+ anyAssistantFound = true;
1189
1329
  }
1190
- } catch (e) {
1191
- console.log(c.yellow(' āš ļø Could not detect Claude Code installation\n'));
1192
- 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');
1193
1348
  checksFailed++;
1194
1349
  }
1195
1350
 
1196
1351
  // Check 2: Hooks installation
1197
1352
  console.log(c.yellow('2. Hooks Installation'));
1353
+
1354
+ // 2.1 Claude Hooks
1198
1355
  const hooksConfigured = config.isConfigured();
1199
1356
  if (hooksConfigured) {
1200
1357
  const hookFiles = fs.readdirSync(config.globalHooks).filter(f => f.endsWith('.mjs'));
1201
- console.log(c.green(` āœ… ${hookFiles.length} hooks installed\n`));
1202
- hookFiles.forEach(f => {
1203
- const hookPath = path.join(config.globalHooks, f);
1204
- const stats = fs.statSync(hookPath);
1205
- const isExecutable = (stats.mode & parseInt('111', 8)) !== 0;
1206
- if (isExecutable) {
1207
- console.log(c.green(` • ${f} (executable)\n`));
1208
- } else {
1209
- console.log(c.yellow(` • ${f} (not executable)\n`));
1210
- issues.push(`Hook ${f} is not executable`);
1211
- recommendations.push(`Run: chmod +x ${hookPath}`);
1212
- }
1213
- });
1214
- checksPassed++;
1358
+ console.log(c.green(` āœ… Claude: ${hookFiles.length} hooks installed`));
1215
1359
  } else {
1216
- console.log(c.red(' āŒ Hooks not configured\n'));
1217
- issues.push('Hooks not installed');
1218
- recommendations.push('Run: teleportation on');
1219
- 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'));
1220
1384
  }
1385
+ console.log('');
1386
+ checksPassed++;
1221
1387
 
1222
1388
  // Check 3: Credentials
1223
1389
  console.log(c.yellow('3. Credentials'));
@@ -1702,8 +1868,23 @@ async function performLogin(manager, flags, positional) {
1702
1868
  console.log(c.green('āœ… Successfully authenticated with API key!\n'));
1703
1869
  console.log(c.cyan('Credentials saved to ~/.teleportation/credentials\n'));
1704
1870
 
1705
- // Auto-start daemon
1706
- 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...'));
1707
1888
  try {
1708
1889
  const lifecyclePath = path.join(TELEPORTATION_DIR, 'lib', 'daemon', 'lifecycle.js');
1709
1890
  const { access } = await import('fs/promises');
@@ -1712,16 +1893,16 @@ async function performLogin(manager, flags, positional) {
1712
1893
  } catch {
1713
1894
  throw new Error('Daemon module not found. Please reinstall: npm install -g teleportation-cli');
1714
1895
  }
1715
- const { startDaemon } = await import('file://' + lifecyclePath);
1716
- const result = await startDaemon({ detached: true, silent: true });
1717
- console.log(c.green(`āœ… Daemon started (PID: ${result.pid})\n`));
1718
- } catch (e) {
1719
- if (e.message && e.message.includes('already running')) {
1720
- console.log(c.green('āœ… Daemon already running\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`));
1721
1900
  } else {
1722
- console.log(c.yellow(`āš ļø Could not start daemon: ${e.message}`));
1723
- console.log(c.cyan('You can start it manually with: teleportation daemon start\n'));
1901
+ console.log(c.green(`āœ… Daemon started (PID: ${result.pid})\n`));
1724
1902
  }
1903
+ } catch (e) {
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'));
1725
1906
  }
1726
1907
 
1727
1908
  console.log(c.yellow('āš ļø Restart Claude Code to apply changes to current session.\n'));
@@ -1799,8 +1980,15 @@ async function performLogin(manager, flags, positional) {
1799
1980
  console.log(c.cyan('Credentials saved to ~/.teleportation/credentials'));
1800
1981
  console.log(c.dim(`Access token expires in ${Math.floor(tokenData.expiresIn / 60)} minutes (auto-refreshes)\n`));
1801
1982
 
1802
- // Auto-start daemon
1803
- console.log(c.cyan('Starting daemon for remote commands...'));
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...'));
1804
1992
  try {
1805
1993
  const lifecyclePath = path.join(TELEPORTATION_DIR, 'lib', 'daemon', 'lifecycle.js');
1806
1994
  const { access } = await import('fs/promises');
@@ -1809,16 +1997,16 @@ async function performLogin(manager, flags, positional) {
1809
1997
  } catch {
1810
1998
  throw new Error('Daemon module not found. Please reinstall: npm install -g teleportation-cli');
1811
1999
  }
1812
- const { startDaemon } = await import('file://' + lifecyclePath);
1813
- const result = await startDaemon({ detached: true, silent: true });
1814
- console.log(c.green(`āœ… Daemon started (PID: ${result.pid})\n`));
1815
- } catch (e) {
1816
- if (e.message && e.message.includes('already running')) {
1817
- console.log(c.green('āœ… Daemon already running\n'));
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`));
1818
2004
  } else {
1819
- console.log(c.yellow(`āš ļø Could not start daemon: ${e.message}`));
1820
- console.log(c.cyan('You can start it manually with: teleportation daemon start\n'));
2005
+ console.log(c.green(`āœ… Daemon started (PID: ${result.pid})\n`));
1821
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'));
1822
2010
  }
1823
2011
 
1824
2012
  console.log(c.yellow('āš ļø Restart Claude Code to apply changes to current session.\n'));
@@ -1859,8 +2047,15 @@ async function performLogin(manager, flags, positional) {
1859
2047
  console.log(c.green('āœ… Successfully authenticated with token!\n'));
1860
2048
  console.log(c.cyan('Credentials saved to ~/.teleportation/credentials\n'));
1861
2049
 
1862
- // Auto-start daemon
1863
- 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...'));
1864
2059
  try {
1865
2060
  const lifecyclePath = path.join(TELEPORTATION_DIR, 'lib', 'daemon', 'lifecycle.js');
1866
2061
  const { access } = await import('fs/promises');
@@ -1869,16 +2064,16 @@ async function performLogin(manager, flags, positional) {
1869
2064
  } catch {
1870
2065
  throw new Error('Daemon module not found. Please reinstall: npm install -g teleportation-cli');
1871
2066
  }
1872
- const { startDaemon } = await import('file://' + lifecyclePath);
1873
- const result = await startDaemon({ detached: true, silent: true });
1874
- console.log(c.green(`āœ… Daemon started (PID: ${result.pid})\n`));
1875
- } catch (e) {
1876
- if (e.message && e.message.includes('already running')) {
1877
- 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`));
1878
2071
  } else {
1879
- console.log(c.yellow(`āš ļø Could not start daemon: ${e.message}`));
1880
- 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`));
1881
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'));
1882
2077
  }
1883
2078
 
1884
2079
  console.log(c.yellow('āš ļø Restart Claude Code to apply changes to current session.\n'));
@@ -1941,8 +2136,15 @@ async function performLogin(manager, flags, positional) {
1941
2136
  console.log(c.green('āœ… Successfully authenticated!\n'));
1942
2137
  console.log(c.cyan('Credentials saved to ~/.teleportation/credentials\n'));
1943
2138
 
1944
- // Auto-start daemon
1945
- 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...'));
1946
2148
  try {
1947
2149
  const lifecyclePath = path.join(TELEPORTATION_DIR, 'lib', 'daemon', 'lifecycle.js');
1948
2150
  const { access } = await import('fs/promises');
@@ -1951,16 +2153,16 @@ async function performLogin(manager, flags, positional) {
1951
2153
  } catch {
1952
2154
  throw new Error('Daemon module not found. Please reinstall: npm install -g teleportation-cli');
1953
2155
  }
1954
- const { startDaemon } = await import('file://' + lifecyclePath);
1955
- const result = await startDaemon({ detached: true, silent: true });
1956
- console.log(c.green(`āœ… Daemon started (PID: ${result.pid})\n`));
1957
- } catch (e) {
1958
- if (e.message && e.message.includes('already running')) {
1959
- 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`));
1960
2160
  } else {
1961
- console.log(c.yellow(`āš ļø Could not start daemon: ${e.message}`));
1962
- 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`));
1963
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'));
1964
2166
  }
1965
2167
 
1966
2168
  resolve();
@@ -2480,6 +2682,89 @@ async function commandRemote(args) {
2480
2682
  }
2481
2683
  }
2482
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
+
2483
2768
  async function commandDaemon(args) {
2484
2769
  const subCommand = args[0] || 'status';
2485
2770
 
@@ -2815,334 +3100,75 @@ async function commandInboxAck(id) {
2815
3100
  }
2816
3101
 
2817
3102
  /**
2818
- * Install hooks globally to ~/.claude/hooks/
2819
- * This command copies hooks from the teleportation project to the global location
2820
- * 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.
2821
3105
  */
2822
3106
  async function commandInstallHooks() {
2823
3107
  console.log(c.purple('šŸ”§ Installing Teleportation Hooks Globally\n'));
2824
3108
 
2825
- const globalHooksDir = path.join(HOME_DIR, '.claude', 'hooks');
2826
- const globalSettings = path.join(HOME_DIR, '.claude', 'settings.json');
2827
- const sourceHooksDir = path.join(TELEPORTATION_DIR, '.claude', 'hooks');
2828
-
2829
- // List of hooks to install
2830
- const hooks = [
2831
- 'pre_tool_use.mjs',
2832
- 'post_tool_use.mjs',
2833
- 'permission_request.mjs',
2834
- 'stop.mjs',
2835
- 'session_start.mjs',
2836
- 'session_end.mjs',
2837
- 'notification.mjs',
2838
- 'user_prompt_submit.mjs',
2839
- 'config-loader.mjs',
2840
- 'session-register.mjs',
2841
- 'heartbeat.mjs' // Spawned by session-register.mjs, needs to be in hooks directory
2842
- ];
2843
-
2844
- // Step 1: Ensure directories exist
2845
- console.log(c.yellow('Step 1: Creating directories...\n'));
2846
3109
  try {
2847
- if (!fs.existsSync(path.join(HOME_DIR, '.claude'))) {
2848
- fs.mkdirSync(path.join(HOME_DIR, '.claude'), { recursive: true });
2849
- }
2850
- if (!fs.existsSync(globalHooksDir)) {
2851
- fs.mkdirSync(globalHooksDir, { recursive: true });
2852
- }
2853
- console.log(c.green(' āœ… ~/.claude/hooks/ directory ready\n'));
2854
- } catch (e) {
2855
- console.log(c.red(` āŒ Failed to create directories: ${e.message}\n`));
2856
- process.exit(1);
2857
- }
2858
-
2859
- // Step 2: Copy hook files
2860
- console.log(c.yellow('Step 2: Copying hooks...\n'));
2861
- let installed = 0;
2862
- let failed = 0;
2863
-
2864
- for (const hook of hooks) {
2865
- const src = path.join(sourceHooksDir, hook);
2866
- const dest = path.join(globalHooksDir, hook);
2867
-
3110
+ // Try UHR first
3111
+ let installed = false;
2868
3112
  try {
2869
- if (!fs.existsSync(src)) {
2870
- console.log(c.yellow(` āš ļø ${hook} not found in source, skipping`));
2871
- 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
+ }
2872
3132
  }
2873
-
2874
- fs.copyFileSync(src, dest);
2875
- fs.chmodSync(dest, 0o755); // Make executable
2876
- console.log(c.green(` āœ… ${hook}`));
2877
- installed++;
2878
3133
  } catch (e) {
2879
- console.log(c.red(` āŒ ${hook}: ${e.message}`));
2880
- failed++;
2881
- }
2882
- }
2883
-
2884
- console.log(`\n Installed: ${c.green(installed)}, Failed: ${failed > 0 ? c.red(failed) : '0'}\n`);
2885
-
2886
- // Step 2.5: Install lib files that hooks depend on (metadata.js, etc.)
2887
- console.log(c.yellow('Step 2.5: Installing lib files...\n'));
2888
- try {
2889
- const installerPath = path.join(TELEPORTATION_DIR, 'lib', 'install', 'installer.js');
2890
- if (fs.existsSync(installerPath)) {
2891
- const installer = await import('file://' + installerPath);
2892
- const libResult = await installer.installLibFiles();
2893
- if (libResult.installed.length > 0) {
2894
- libResult.installed.forEach(file => {
2895
- console.log(c.green(` āœ… ${file}`));
2896
- });
2897
- }
2898
- if (libResult.failed.length > 0) {
2899
- libResult.failed.forEach(({ file, error }) => {
2900
- console.log(c.yellow(` āš ļø ${file}: ${error}`));
2901
- });
2902
- }
2903
- console.log(`\n Lib files installed: ${c.green(libResult.installed.length)}, Failed: ${libResult.failed.length > 0 ? c.yellow(libResult.failed.length) : '0'}\n`);
2904
- } else {
2905
- console.log(c.yellow(' āš ļø Installer module not found, skipping lib files\n'));
3134
+ // UHR not available
2906
3135
  }
2907
- } catch (e) {
2908
- console.log(c.yellow(` āš ļø Failed to install lib files: ${e.message}\n`));
2909
- }
2910
-
2911
- // Step 3: Update settings.json
2912
- console.log(c.yellow('Step 3: Updating Claude Code settings...\n'));
2913
3136
 
2914
- try {
2915
- // Use SettingsManager for proper hook management
2916
- const settingsManagerPath = path.join(TELEPORTATION_DIR, 'lib', 'settings', 'manager.js');
2917
- const { SettingsManager } = await import('file://' + settingsManagerPath);
2918
- const settingsManager = new SettingsManager(globalSettings);
2919
-
2920
- // Remove ALL existing Teleportation hooks (regardless of path)
2921
- // This prevents accumulation of hooks from different installation locations
2922
- const removeResult = await settingsManager.removeTeleportationHooks();
2923
- if (removeResult.hooksRemoved > 0) {
2924
- console.log(c.cyan(` Removed ${removeResult.hooksRemoved} existing Teleportation hook(s)\n`));
2925
- }
2926
-
2927
- // Add new hooks pointing to global hooks directory
2928
- const addResult = await settingsManager.addHooks(globalHooksDir);
2929
- console.log(c.green(` āœ… Added ${addResult.hooksAdded} hook(s) to settings\n`));
2930
-
2931
- // Deduplicate in case there are any remaining duplicates
2932
- const dedupeResult = await settingsManager.deduplicate();
2933
- if (dedupeResult.duplicatesRemoved > 0) {
2934
- console.log(c.cyan(` Removed ${dedupeResult.duplicatesRemoved} duplicate hook(s)\n`));
2935
- }
2936
- } catch (e) {
2937
- console.log(c.red(` āŒ Failed to update settings: ${e.message}\n`));
2938
-
2939
- try {
2940
- // Fallback to manual merge if SettingsManager fails
2941
- console.log(c.yellow(' Attempting fallback method...\n'));
2942
-
2943
- const quotePath = (p) => JSON.stringify(p);
2944
-
2945
- const hooksConfig = {
2946
- PreToolUse: [{
2947
- matcher: ".*",
2948
- hooks: [{
2949
- type: "command",
2950
- command: `node ${quotePath(path.join(globalHooksDir, 'pre_tool_use.mjs'))}`
2951
- }]
2952
- }],
2953
- PostToolUse: [{
2954
- matcher: ".*",
2955
- hooks: [{
2956
- type: "command",
2957
- command: `node ${quotePath(path.join(globalHooksDir, 'post_tool_use.mjs'))}`
2958
- }]
2959
- }],
2960
- PermissionRequest: [{
2961
- matcher: ".*",
2962
- hooks: [{
2963
- type: "command",
2964
- command: `node ${quotePath(path.join(globalHooksDir, 'permission_request.mjs'))}`
2965
- }]
2966
- }],
2967
- Stop: [{
2968
- matcher: ".*",
2969
- hooks: [{
2970
- type: "command",
2971
- command: `node ${quotePath(path.join(globalHooksDir, 'stop.mjs'))}`
2972
- }]
2973
- }],
2974
- SessionStart: [{
2975
- matcher: ".*",
2976
- hooks: [{
2977
- type: "command",
2978
- command: `node ${quotePath(path.join(globalHooksDir, 'session_start.mjs'))}`
2979
- }]
2980
- }],
2981
- SessionEnd: [{
2982
- matcher: ".*",
2983
- hooks: [{
2984
- type: "command",
2985
- command: `node ${quotePath(path.join(globalHooksDir, 'session_end.mjs'))}`
2986
- }]
2987
- }],
2988
- Notification: [{
2989
- matcher: ".*",
2990
- hooks: [{
2991
- type: "command",
2992
- command: `node ${quotePath(path.join(globalHooksDir, 'notification.mjs'))}`
2993
- }]
2994
- }],
2995
- UserPromptSubmit: [{
2996
- matcher: ".*",
2997
- hooks: [{
2998
- type: "command",
2999
- command: `node ${quotePath(path.join(globalHooksDir, 'user_prompt_submit.mjs'))}`
3000
- }]
3001
- }]
3002
- };
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');
3003
3142
 
3004
- let existingSettings = {};
3143
+ const result = await install(sourceHooksDir);
3005
3144
 
3006
- // Load existing settings if present
3007
- if (fs.existsSync(globalSettings)) {
3008
- try {
3009
- const content = fs.readFileSync(globalSettings, 'utf8');
3010
- existingSettings = JSON.parse(content);
3011
- console.log(c.cyan(' Found existing settings, merging...\n'));
3012
- } catch (err) {
3013
- 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/'));
3014
3148
  }
3015
- }
3016
-
3017
- // Load isTeleportationHook pattern for filtering
3018
- let isTeleportationHook;
3019
- try {
3020
- const settingsManagerPath = path.join(TELEPORTATION_DIR, 'lib', 'settings', 'manager.js');
3021
- const settingsModule = await import('file://' + settingsManagerPath);
3022
- isTeleportationHook = settingsModule.isTeleportationHook;
3023
- } catch (err) {
3024
- // Log the specific error for debugging
3025
- console.log(c.yellow(` āš ļø Could not load SettingsManager: ${err.message}`));
3026
- console.log(c.yellow(' Using simplified hook detection pattern\n'));
3027
-
3028
- // More robust fallback pattern matching the regex in SettingsManager
3029
- // Matches: .claude/hooks/(pre_tool_use|post_tool_use|permission_request|stop|session_start|session_end|notification|user_prompt_submit).mjs
3030
- isTeleportationHook = (cmd) => {
3031
- if (!cmd || typeof cmd !== 'string') return false;
3032
- return /\.claude\/hooks\/(pre_tool_use|post_tool_use|permission_request|stop|session_start|session_end|notification|user_prompt_submit)\.mjs/.test(cmd);
3033
- };
3034
- }
3035
-
3036
- // Merge hooks - preserve NON-teleportation user hooks, remove ALL teleportation hooks
3037
- const mergeHookArrays = (existing, incoming) => {
3038
- if (!existing || !Array.isArray(existing)) return incoming;
3039
- if (!incoming || !Array.isArray(incoming)) return existing;
3040
-
3041
- // Filter out ALL teleportation hooks (regardless of path)
3042
- const nonTeleportationHooks = existing.filter(matcher => {
3043
- if (!matcher.hooks || !Array.isArray(matcher.hooks)) return true;
3044
- // Keep matcher only if it has non-teleportation hooks
3045
- return !matcher.hooks.every(h => h.command && isTeleportationHook(h.command));
3046
- });
3047
-
3048
- // Combine: existing (non-teleportation) + incoming (new teleportation hooks)
3049
- return [...nonTeleportationHooks, ...incoming];
3050
- };
3051
-
3052
- // Merge all hook types with warnings about user hooks
3053
- const mergedHooks = { ...(existingSettings.hooks || {}) };
3054
- let hasUserHooks = false;
3055
-
3056
- for (const [hookType, hookConfig] of Object.entries(hooksConfig)) {
3057
- const existingHooksForType = existingSettings.hooks?.[hookType] || [];
3058
-
3059
- // Find user-defined hooks (not from teleportation)
3060
- const userHooks = existingHooksForType.filter(h => {
3061
- if (!h.hooks || !Array.isArray(h.hooks)) return true;
3062
- // Keep only hooks that are NOT teleportation hooks
3063
- return !h.hooks.every(hh => hh.command && isTeleportationHook(hh.command));
3064
- });
3065
-
3066
- if (userHooks.length > 0) {
3067
- hasUserHooks = true;
3068
- console.log(c.yellow(` āš ļø Preserving ${userHooks.length} custom ${hookType} hook(s):`));
3069
- userHooks.forEach(h => {
3070
- const cmds = (h.hooks || []).map(hh => hh.command || 'unknown');
3071
- cmds.forEach(cmd => console.log(c.dim(` • ${cmd}`)));
3072
- });
3073
- 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/'));
3074
3152
  }
3075
-
3076
- mergedHooks[hookType] = mergeHookArrays(existingHooksForType, hookConfig);
3077
- }
3078
-
3079
- if (hasUserHooks) {
3080
- console.log(c.cyan(' Your custom hooks will continue to work alongside Teleportation hooks.\n'));
3081
- }
3082
-
3083
- const mergedSettings = {
3084
- ...existingSettings,
3085
- hooks: mergedHooks
3086
- };
3087
-
3088
- // Write settings
3089
- fs.writeFileSync(globalSettings, JSON.stringify(mergedSettings, null, 2));
3090
- console.log(c.green(' āœ… ~/.claude/settings.json updated\n'));
3091
-
3092
- // Deduplicate in fallback path (Gap #2)
3093
- console.log(c.yellow(' Running deduplication check...\n'));
3094
- let duplicatesRemoved = 0;
3095
3153
 
3096
- for (const [hookType, matchers] of Object.entries(mergedSettings.hooks || {})) {
3097
- if (!Array.isArray(matchers)) continue;
3098
-
3099
- const seenCommands = new Set();
3100
- const uniqueMatchers = [];
3101
-
3102
- for (const matcher of matchers) {
3103
- const commands = (matcher.hooks || []).map(h => h.command).filter(Boolean);
3104
- const isUnique = !commands.some(cmd => seenCommands.has(cmd));
3105
-
3106
- if (isUnique) {
3107
- uniqueMatchers.push(matcher);
3108
- commands.forEach(cmd => seenCommands.add(cmd));
3109
- } else {
3110
- duplicatesRemoved++;
3111
- }
3154
+ if (result.libFilesInstalled > 0) {
3155
+ console.log(c.green(` āœ… ${result.libFilesInstalled} shared library files installed`));
3112
3156
  }
3113
-
3114
- mergedSettings.hooks[hookType] = uniqueMatchers;
3115
3157
  }
3116
3158
 
3117
- if (duplicatesRemoved > 0) {
3118
- fs.writeFileSync(globalSettings, JSON.stringify(mergedSettings, null, 2));
3119
- console.log(c.cyan(` Removed ${duplicatesRemoved} duplicate(s) in fallback\n`));
3120
- } else {
3121
- console.log(c.green(' āœ… No duplicates found\n'));
3122
- }
3123
- } catch (fallbackErr) {
3124
- console.log(c.red(` āŒ Fallback also failed: ${fallbackErr.message}\n`));
3125
- console.log(c.red(' Please report this issue at: https://github.com/dundas/teleportation-private/issues\n'));
3126
- process.exit(1);
3127
- }
3128
- }
3129
-
3130
- // Summary
3131
- console.log(c.cyan('╭─────────────────────────────────────────────────────╮'));
3132
- console.log(c.cyan('│ │'));
3133
- console.log(c.cyan('│ šŸŽ‰ ') + c.green('Hooks Installed Successfully!') + c.cyan(' │'));
3134
- console.log(c.cyan('│ │'));
3135
- console.log(c.cyan('│ Hooks location: ~/.claude/hooks/ │'));
3136
- console.log(c.cyan('│ Settings: ~/.claude/settings.json │'));
3137
- console.log(c.cyan('│ │'));
3138
- console.log(c.cyan('│ ') + c.yellow('āš ļø Restart Claude Code') + c.cyan(' to activate hooks. │'));
3139
- console.log(c.cyan('│ │'));
3140
- 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'));
3141
3167
 
3142
- console.log(c.cyan('Next steps:'));
3143
- console.log(' 1. If not logged in: teleportation login');
3144
- console.log(' 2. Check status: teleportation status');
3145
- 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
+ }
3146
3172
  }
3147
3173
 
3148
3174
  async function commandCommand() {
@@ -3441,7 +3467,7 @@ const command = process.argv[2] || 'help';
3441
3467
  const args = process.argv.slice(3);
3442
3468
 
3443
3469
  // Handle async commands that need to complete before exit
3444
- 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'];
3445
3471
  // Keep this list in sync with switch cases below
3446
3472
  asyncCommands.push('github');
3447
3473
  if (asyncCommands.includes(command)) {
@@ -3475,7 +3501,10 @@ try {
3475
3501
  });
3476
3502
  break;
3477
3503
  case 'off':
3478
- commandOff();
3504
+ commandOff().catch(err => {
3505
+ console.log(c.red('Error: ' + err.message));
3506
+ process.exit(1);
3507
+ });
3479
3508
  break;
3480
3509
  case 'status':
3481
3510
  commandStatus().catch(err => {
@@ -3618,6 +3647,12 @@ try {
3618
3647
  process.exit(1);
3619
3648
  });
3620
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;
3621
3656
  case 'version':
3622
3657
  case '--version':
3623
3658
  case '-v':