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.
- package/.claude/hooks/config-loader.mjs +88 -34
- package/.claude/hooks/permission_request.mjs +392 -82
- package/.claude/hooks/post_tool_use.mjs +90 -0
- package/.claude/hooks/pre_tool_use.mjs +247 -305
- package/.claude/hooks/session-register.mjs +94 -105
- package/.claude/hooks/session_end.mjs +41 -42
- package/.claude/hooks/session_start.mjs +45 -60
- package/.claude/hooks/stop.mjs +752 -99
- package/.claude/hooks/user_prompt_submit.mjs +26 -3
- package/README.md +7 -0
- package/lib/auth/api-key.js +12 -0
- package/lib/auth/token-refresh.js +286 -0
- package/lib/cli/daemon-commands.js +1 -1
- package/lib/cli/teleport-commands.js +469 -0
- package/lib/daemon/daemon-v2.js +104 -0
- package/lib/daemon/lifecycle.js +56 -171
- package/lib/daemon/response-classifier.js +15 -1
- package/lib/daemon/services/index.js +3 -0
- package/lib/daemon/services/polling-service.js +173 -0
- package/lib/daemon/services/queue-service.js +318 -0
- package/lib/daemon/services/session-service.js +115 -0
- package/lib/daemon/state.js +35 -0
- package/lib/daemon/task-executor-v2.js +413 -0
- package/lib/daemon/task-executor.js +1235 -0
- package/lib/daemon/teleportation-daemon.js +770 -25
- package/lib/daemon/timeline-analyzer.js +215 -0
- package/lib/daemon/transcript-ingestion.js +696 -0
- package/lib/daemon/utils.js +91 -0
- package/lib/install/installer.js +184 -20
- package/lib/install/uhr-installer.js +136 -0
- package/lib/remote/providers/base-provider.js +46 -0
- package/lib/remote/providers/daytona-provider.js +58 -0
- package/lib/remote/providers/provider-factory.js +90 -19
- package/lib/remote/providers/sprites-provider.js +711 -0
- package/lib/teleport/exporters/claude-exporter.js +302 -0
- package/lib/teleport/exporters/gemini-exporter.js +307 -0
- package/lib/teleport/exporters/index.js +93 -0
- package/lib/teleport/exporters/interface.js +153 -0
- package/lib/teleport/fork-tracker.js +415 -0
- package/lib/teleport/git-committer.js +337 -0
- package/lib/teleport/index.js +48 -0
- package/lib/teleport/manager.js +620 -0
- package/lib/teleport/session-capture.js +282 -0
- package/package.json +11 -5
- package/teleportation-cli.cjs +632 -451
- package/.claude/hooks/heartbeat.mjs +0 -396
- package/lib/daemon/agentic-executor.js +0 -803
- package/lib/daemon/pid-manager.js +0 -160
package/teleportation-cli.cjs
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
//
|
|
291
|
-
const
|
|
292
|
-
const {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
536
|
+
console.log(' Installing Teleportation hooks...');
|
|
498
537
|
|
|
499
538
|
try {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
//
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
}
|
|
704
|
-
|
|
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
|
-
|
|
788
|
-
|
|
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('ā
') + '
|
|
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('
|
|
804
|
-
|
|
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
|
-
|
|
807
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
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.
|
|
1119
|
-
|
|
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
|
|
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:
|
|
1162
|
-
console.log(c.yellow('1.
|
|
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(` ā
|
|
1167
|
-
|
|
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
|
-
|
|
1176
|
-
|
|
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
|
|
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.
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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
|
-
//
|
|
1689
|
-
|
|
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 {
|
|
1699
|
-
const result = await
|
|
1700
|
-
|
|
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
|
-
|
|
1703
|
-
|
|
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.
|
|
1706
|
-
|
|
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
|
-
//
|
|
1749
|
-
|
|
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 {
|
|
1759
|
-
const result = await
|
|
1760
|
-
|
|
1761
|
-
|
|
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.
|
|
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
|
|
1796
|
-
console.log(c.cyan('
|
|
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
|
-
//
|
|
1829
|
-
|
|
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 {
|
|
1839
|
-
const result = await
|
|
1840
|
-
|
|
1841
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
2708
|
-
|
|
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
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
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
|
-
|
|
2740
|
-
failed++;
|
|
3134
|
+
// UHR not available
|
|
2741
3135
|
}
|
|
2742
|
-
}
|
|
2743
3136
|
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
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
|
-
|
|
3143
|
+
const result = await install(sourceHooksDir);
|
|
2865
3144
|
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2963
|
-
|
|
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
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
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
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
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':
|