teleportation-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.claude/hooks/config-loader.mjs +93 -0
  2. package/.claude/hooks/heartbeat.mjs +331 -0
  3. package/.claude/hooks/notification.mjs +35 -0
  4. package/.claude/hooks/permission_request.mjs +307 -0
  5. package/.claude/hooks/post_tool_use.mjs +137 -0
  6. package/.claude/hooks/pre_tool_use.mjs +451 -0
  7. package/.claude/hooks/session-register.mjs +274 -0
  8. package/.claude/hooks/session_end.mjs +256 -0
  9. package/.claude/hooks/session_start.mjs +308 -0
  10. package/.claude/hooks/stop.mjs +277 -0
  11. package/.claude/hooks/user_prompt_submit.mjs +91 -0
  12. package/LICENSE +21 -0
  13. package/README.md +243 -0
  14. package/lib/auth/api-key.js +110 -0
  15. package/lib/auth/credentials.js +341 -0
  16. package/lib/backup/manager.js +461 -0
  17. package/lib/cli/daemon-commands.js +299 -0
  18. package/lib/cli/index.js +303 -0
  19. package/lib/cli/session-commands.js +294 -0
  20. package/lib/cli/snapshot-commands.js +223 -0
  21. package/lib/cli/worktree-commands.js +291 -0
  22. package/lib/config/manager.js +306 -0
  23. package/lib/daemon/lifecycle.js +336 -0
  24. package/lib/daemon/pid-manager.js +160 -0
  25. package/lib/daemon/teleportation-daemon.js +2009 -0
  26. package/lib/handoff/config.js +102 -0
  27. package/lib/handoff/example.js +152 -0
  28. package/lib/handoff/git-handoff.js +351 -0
  29. package/lib/handoff/handoff.js +277 -0
  30. package/lib/handoff/index.js +25 -0
  31. package/lib/handoff/session-state.js +238 -0
  32. package/lib/install/installer.js +555 -0
  33. package/lib/machine-coders/claude-code-adapter.js +329 -0
  34. package/lib/machine-coders/example.js +239 -0
  35. package/lib/machine-coders/gemini-cli-adapter.js +406 -0
  36. package/lib/machine-coders/index.js +103 -0
  37. package/lib/machine-coders/interface.js +168 -0
  38. package/lib/router/classifier.js +251 -0
  39. package/lib/router/example.js +92 -0
  40. package/lib/router/index.js +69 -0
  41. package/lib/router/mech-llms-client.js +277 -0
  42. package/lib/router/models.js +188 -0
  43. package/lib/router/router.js +382 -0
  44. package/lib/session/cleanup.js +100 -0
  45. package/lib/session/metadata.js +258 -0
  46. package/lib/session/mute-checker.js +114 -0
  47. package/lib/session-registry/manager.js +302 -0
  48. package/lib/snapshot/manager.js +390 -0
  49. package/lib/utils/errors.js +166 -0
  50. package/lib/utils/logger.js +148 -0
  51. package/lib/utils/retry.js +155 -0
  52. package/lib/worktree/manager.js +301 -0
  53. package/package.json +66 -0
  54. package/teleportation-cli.cjs +2987 -0
@@ -0,0 +1,2987 @@
1
+ #!/usr/bin/env bun
2
+ // Teleportation CLI - Remote Claude Code Control Setup
3
+ // Version 1.0.0
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { execSync } = require('child_process');
8
+ const os = require('os');
9
+
10
+ const CLI_VERSION = '1.0.0';
11
+ const HOME_DIR = os.homedir();
12
+ // Teleportation project directory (for development)
13
+ // In production, hooks will be installed globally
14
+ const TELEPORTATION_DIR = process.env.TELEPORTATION_DIR || path.join(__dirname);
15
+
16
+ // Color helpers
17
+ const c = {
18
+ red: (text) => '\x1b[0;31m' + text + '\x1b[0m',
19
+ green: (text) => '\x1b[0;32m' + text + '\x1b[0m',
20
+ yellow: (text) => '\x1b[1;33m' + text + '\x1b[0m',
21
+ blue: (text) => '\x1b[0;34m' + text + '\x1b[0m',
22
+ purple: (text) => '\x1b[0;35m' + text + '\x1b[0m',
23
+ cyan: (text) => '\x1b[0;36m' + text + '\x1b[0m'
24
+ };
25
+
26
+ // Configuration manager
27
+ class ConfigManager {
28
+ constructor() {
29
+ this.globalHooksDir = path.join(HOME_DIR, '.claude');
30
+ this.globalSettings = path.join(this.globalHooksDir, 'settings.json');
31
+ this.globalHooks = path.join(this.globalHooksDir, 'hooks');
32
+ this.projectHooksDir = path.join(TELEPORTATION_DIR, '.claude', 'hooks');
33
+ this.envFile = path.join(HOME_DIR, '.teleportation-env');
34
+ this.zshrc = path.join(HOME_DIR, '.zshrc');
35
+ }
36
+
37
+ ensureDirectories() {
38
+ [this.globalHooksDir, this.globalHooks].forEach(dir => {
39
+ if (!fs.existsSync(dir)) {
40
+ fs.mkdirSync(dir, { recursive: true });
41
+ }
42
+ });
43
+ }
44
+
45
+ isConfigured() {
46
+ return fs.existsSync(this.globalSettings) &&
47
+ fs.existsSync(this.globalHooks) &&
48
+ fs.readdirSync(this.globalHooks).length > 0;
49
+ }
50
+
51
+ getEnvVars() {
52
+ // Synchronous version for backward compatibility
53
+ // For async credential loading, use getCredentials() instead
54
+ const vars = {
55
+ RELAY_API_URL: process.env.RELAY_API_URL || '',
56
+ RELAY_API_KEY: process.env.RELAY_API_KEY || '',
57
+ SLACK_WEBHOOK_URL: process.env.SLACK_WEBHOOK_URL || ''
58
+ };
59
+ return vars;
60
+ }
61
+
62
+ async getCredentials() {
63
+ return await getCredentials();
64
+ }
65
+
66
+ areEnvVarsSet() {
67
+ const vars = this.getEnvVars();
68
+ return vars.RELAY_API_URL && vars.RELAY_API_KEY;
69
+ }
70
+ }
71
+
72
+ const config = new ConfigManager();
73
+
74
+ // Credential loader (async, uses ES module)
75
+ let credentialManager = null;
76
+ async function loadCredentialManager() {
77
+ if (!credentialManager) {
78
+ try {
79
+ const { CredentialManager } = await import('./lib/auth/credentials.js');
80
+ credentialManager = new CredentialManager();
81
+ } catch (e) {
82
+ // Credential manager not available, will fall back to env vars
83
+ credentialManager = null;
84
+ }
85
+ }
86
+ return credentialManager;
87
+ }
88
+
89
+ // Load credentials on startup
90
+ let loadedCredentials = null;
91
+ async function loadCredentials() {
92
+ if (loadedCredentials !== null) return loadedCredentials;
93
+
94
+ try {
95
+ const manager = await loadCredentialManager();
96
+ if (manager) {
97
+ loadedCredentials = await manager.load();
98
+ }
99
+ } catch (e) {
100
+ // Distinguish between different error types
101
+ if (e.code === 'ENOENT') {
102
+ // File doesn't exist - OK, fall back to env vars
103
+ loadedCredentials = null;
104
+ } else if (e.message && e.message.includes('decrypt')) {
105
+ // Decryption failed - warn user but fall back
106
+ console.warn('⚠️ Credential file exists but could not be decrypted. Using environment variables.');
107
+ loadedCredentials = null;
108
+ } else {
109
+ // Other error - log for debugging but don't fail
110
+ console.error('Failed to load credentials:', e.message);
111
+ loadedCredentials = null;
112
+ }
113
+ }
114
+
115
+ return loadedCredentials;
116
+ }
117
+
118
+ // Get credentials with fallback to environment variables
119
+ async function getCredentials() {
120
+ const creds = await loadCredentials();
121
+ if (creds) {
122
+ return {
123
+ RELAY_API_URL: creds.relayApiUrl || process.env.RELAY_API_URL || '',
124
+ RELAY_API_KEY: creds.relayApiKey || creds.apiKey || process.env.RELAY_API_KEY || '',
125
+ SLACK_WEBHOOK_URL: creds.slackWebhookUrl || process.env.SLACK_WEBHOOK_URL || ''
126
+ };
127
+ }
128
+
129
+ // Fall back to environment variables
130
+ return {
131
+ RELAY_API_URL: process.env.RELAY_API_URL || '',
132
+ RELAY_API_KEY: process.env.RELAY_API_KEY || '',
133
+ SLACK_WEBHOOK_URL: process.env.SLACK_WEBHOOK_URL || ''
134
+ };
135
+ }
136
+
137
+ // Parse command line flags
138
+ function parseFlags(args) {
139
+ const flags = {};
140
+ const positional = [];
141
+
142
+ for (let i = 0; i < args.length; i++) {
143
+ const arg = args[i];
144
+ if (arg.startsWith('--')) {
145
+ const key = arg.slice(2);
146
+ if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
147
+ flags[key] = args[i + 1];
148
+ i++; // Skip next arg as it's the value
149
+ } else {
150
+ flags[key] = true; // Boolean flag
151
+ }
152
+ } else if (arg.startsWith('-')) {
153
+ // Short flags like -k
154
+ const key = arg.slice(1);
155
+ if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
156
+ flags[key] = args[i + 1];
157
+ i++;
158
+ } else {
159
+ flags[key] = true;
160
+ }
161
+ } else {
162
+ positional.push(arg);
163
+ }
164
+ }
165
+
166
+ return { flags, positional };
167
+ }
168
+
169
+ // Service checker
170
+ function checkService(name, port) {
171
+ try {
172
+ const result = execSync(`lsof -i :${port} 2>/dev/null | grep LISTEN`, { encoding: 'utf8' });
173
+ return result.includes('LISTEN');
174
+ } catch (e) {
175
+ return false;
176
+ }
177
+ }
178
+
179
+ function checkServiceHealth(url) {
180
+ try {
181
+ const result = execSync(`curl -s ${url}/health`, { encoding: 'utf8' });
182
+ return result.includes('healthy');
183
+ } catch (e) {
184
+ return false;
185
+ }
186
+ }
187
+
188
+ // Command handlers
189
+ function commandVersion() {
190
+ console.log(c.purple('Teleportation CLI'));
191
+ console.log(c.cyan(`Version: ${CLI_VERSION}`));
192
+ console.log(c.blue(`Node.js: ${process.version}`));
193
+ console.log(c.yellow(`Platform: ${process.platform} ${process.arch}`));
194
+ console.log(c.green(`Home: ${HOME_DIR}`));
195
+ }
196
+
197
+ function commandHelp() {
198
+ console.log(c.purple('Teleportation CLI v' + CLI_VERSION));
199
+ console.log(c.cyan('Remote Claude Code Control System\n'));
200
+
201
+ console.log(c.yellow('Usage:'));
202
+ console.log(' ./teleportation <command> [options]\n');
203
+
204
+ console.log(c.yellow('Getting Started:'));
205
+ console.log(' ' + c.green('setup') + ' ⭐ Guided setup wizard (recommended for new users)');
206
+ console.log(' ' + c.green('status') + ' Check system status and connectivity\n');
207
+
208
+ console.log(c.yellow('Authentication:'));
209
+ console.log(' ' + c.green('login') + ' Authenticate with API key or token');
210
+ console.log(' ' + c.green('logout') + ' Clear saved credentials\n');
211
+
212
+ console.log(c.yellow('Setup Commands:'));
213
+ console.log(' ' + c.green('on') + ' Enable remote control hooks');
214
+ console.log(' ' + c.green('off') + ' Disable remote control hooks');
215
+ console.log(' ' + c.green('install-hooks') + ' Install hooks globally to ~/.claude/hooks/');
216
+ console.log(' ' + c.green('update') + ' Update CLI and hooks to latest version');
217
+ console.log(' ' + c.green('test') + ' Run diagnostic tests');
218
+ console.log(' ' + c.green('doctor') + ' Run comprehensive diagnostics\n');
219
+
220
+ console.log(c.yellow('Backup & Restore:'));
221
+ console.log(' ' + c.green('backup list') + ' List all backups');
222
+ console.log(' ' + c.green('backup restore') + ' Restore from backup');
223
+ console.log(' ' + c.green('backup create') + ' Create a manual backup\n');
224
+
225
+ console.log(c.yellow('Service Management:'));
226
+ console.log(' ' + c.green('start') + ' Start relay and storage services');
227
+ console.log(' ' + c.green('stop') + ' Stop all services');
228
+ console.log(' ' + c.green('restart') + ' Restart all services');
229
+ console.log(' ' + c.green('logs') + ' View service logs\n');
230
+
231
+ console.log(c.yellow('Daemon Management:'));
232
+ console.log(' ' + c.green('daemon start') + ' Start the teleportation daemon');
233
+ console.log(' ' + c.green('daemon stop') + ' Stop the daemon');
234
+ console.log(' ' + c.green('daemon restart') + ' Restart the daemon');
235
+ console.log(' ' + c.green('daemon status') + ' Show daemon status');
236
+ console.log(' ' + c.green('daemon health') + ' Check daemon health\n');
237
+
238
+ console.log(c.yellow('Inbox & Messaging:'));
239
+ console.log(' ' + c.green('command "<text>"') + ' Enqueue a command message for this session');
240
+ console.log(' ' + c.green('inbox') + ' View next inbox message for this session');
241
+ console.log(' ' + c.green('inbox-ack <id>') + ' Acknowledge inbox message by id\n');
242
+
243
+ console.log(c.yellow('Configuration:'));
244
+ console.log(' ' + c.green('config') + ' Manage configuration');
245
+ console.log(' ' + c.green('config list') + ' Show all settings');
246
+ console.log(' ' + c.green('config get <key>') + ' Get specific setting');
247
+ console.log(' ' + c.green('config set <key> <value>') + ' Update setting');
248
+ console.log(' ' + c.green('config edit') + ' Open config in editor');
249
+ console.log(' ' + c.green('env') + ' Show environment variables\n');
250
+
251
+ console.log(c.yellow('Session Isolation:'));
252
+ console.log(' ' + c.green('worktree create') + ' Create isolated worktree for a session');
253
+ console.log(' ' + c.green('worktree list') + ' List all session worktrees');
254
+ console.log(' ' + c.green('worktree remove') + ' Remove a worktree');
255
+ console.log(' ' + c.green('worktree info') + ' Show worktree information');
256
+ console.log(' ' + c.green('snapshot create') + ' Create a code snapshot');
257
+ console.log(' ' + c.green('snapshot list') + ' List snapshots for a session');
258
+ console.log(' ' + c.green('snapshot restore') + ' Restore a previous snapshot');
259
+ console.log(' ' + c.green('session list') + ' List registered sessions');
260
+ console.log(' ' + c.green('session check-conflicts') + ' Check for file conflicts\n');
261
+
262
+ console.log(c.yellow('Information:'));
263
+ console.log(' ' + c.green('info') + ' Show detailed system info');
264
+ console.log(' ' + c.green('version') + ' Show version information');
265
+ console.log(' ' + c.green('help') + ' Show this help message\n');
266
+
267
+ console.log(c.purple('Examples:'));
268
+ console.log(' ./teleportation setup # ⭐ Recommended: guided setup');
269
+ console.log(' ./teleportation status # Check status');
270
+ console.log(' ./teleportation backup restore # Restore previous config');
271
+ console.log('');
272
+ console.log(c.purple('Quick Start (new users):'));
273
+ console.log(' Just run: ' + c.green('teleportation setup'));
274
+ console.log(' The wizard will guide you through everything!\n');
275
+ }
276
+
277
+ async function commandOn() {
278
+ console.log(c.yellow('🚀 Enabling Teleportation Remote Control...\n'));
279
+
280
+ try {
281
+ // Use installer module
282
+ const installerPath = path.join(TELEPORTATION_DIR, 'lib', 'install', 'installer.js');
283
+ const { install, checkNodeVersion, checkClaudeCode } = await import('file://' + installerPath);
284
+
285
+ // Pre-flight checks
286
+ const nodeCheck = checkNodeVersion();
287
+ if (!nodeCheck.valid) {
288
+ console.log(c.red(`❌ ${nodeCheck.error}\n`));
289
+ return;
290
+ }
291
+ console.log(c.green(`✅ Node.js ${nodeCheck.version}\n`));
292
+
293
+ const claudeCheck = checkClaudeCode();
294
+ if (!claudeCheck.valid) {
295
+ console.log(c.yellow(`⚠️ ${claudeCheck.error}\n`));
296
+ console.log(c.cyan(' Continuing anyway...\n'));
297
+ } else {
298
+ console.log(c.green(`✅ Claude Code found: ${claudeCheck.path}\n`));
299
+ }
300
+
301
+ // Install hooks
302
+ const sourceHooksDir = path.join(TELEPORTATION_DIR, '.claude', 'hooks');
303
+ if (!fs.existsSync(sourceHooksDir)) {
304
+ console.log(c.red(`❌ Hooks not found at ${sourceHooksDir}\n`));
305
+ return;
306
+ }
307
+
308
+ const result = await install(sourceHooksDir);
309
+
310
+ console.log(c.green('\n🎉 Teleportation Remote Control ENABLED!'));
311
+ console.log(c.cyan('\nInstallation Summary:'));
312
+ console.log(` Hooks verified: ${c.green(result.hooksVerified)}`);
313
+ console.log(` Daemon installed: ${c.green(result.daemonInstalled + ' files')}`);
314
+ console.log(` Settings file: ${c.green(result.settingsFile)}`);
315
+ console.log(` Hooks directory: ${c.green(result.hooksDir)}`);
316
+ console.log(` Daemon directory: ${c.green(result.daemonDir)}`);
317
+ console.log(c.cyan('\nNext steps:'));
318
+ console.log(' 1. Login: teleportation login');
319
+ console.log(' 2. Check status: teleportation status');
320
+ console.log(' 3. Run diagnostics: teleportation doctor\n');
321
+
322
+ } catch (error) {
323
+ console.log(c.red(`❌ Installation failed: ${error.message}\n`));
324
+ process.exit(1);
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Setup wizard - guided onboarding for new users
330
+ * Creates backup before making changes, validates API key, installs hooks
331
+ */
332
+ async function commandSetup() {
333
+ const readline = require('readline');
334
+
335
+ const rl = readline.createInterface({
336
+ input: process.stdin,
337
+ output: process.stdout
338
+ });
339
+
340
+ const question = (prompt) => new Promise(resolve => rl.question(prompt, resolve));
341
+
342
+ console.log('\n' + c.cyan('╭─────────────────────────────────────────────────────╮'));
343
+ console.log(c.cyan('│ │'));
344
+ console.log(c.cyan('│ 🚀 ') + c.purple('Teleportation Setup') + c.cyan(' │'));
345
+ console.log(c.cyan('│ │'));
346
+ console.log(c.cyan('│ Let\'s get you set up for remote Claude Code │'));
347
+ console.log(c.cyan('│ approvals in just a few steps. │'));
348
+ console.log(c.cyan('│ │'));
349
+ console.log(c.cyan('╰─────────────────────────────────────────────────────╯\n'));
350
+
351
+ try {
352
+ // Step 0: Check for existing files and create backup
353
+ console.log(c.yellow('Checking existing configuration...\n'));
354
+
355
+ const backupManagerPath = path.join(TELEPORTATION_DIR, 'lib', 'backup', 'manager.js');
356
+ const { BackupManager } = await import('file://' + backupManagerPath);
357
+ const backupManager = new BackupManager();
358
+
359
+ const existingFiles = backupManager.checkExistingFiles();
360
+
361
+ if (existingFiles.length > 0) {
362
+ console.log(c.yellow(' ⚠️ Found existing files that will be modified:'));
363
+ for (const file of existingFiles) {
364
+ const shortPath = file.path.replace(HOME_DIR, '~');
365
+ if (file.fileCount !== undefined) {
366
+ console.log(` • ${shortPath} (contains ${file.fileCount} files)`);
367
+ } else {
368
+ console.log(` • ${shortPath}`);
369
+ }
370
+ }
371
+ console.log('');
372
+
373
+ console.log(c.cyan(' Creating backup before proceeding...'));
374
+ const { backupPath, manifest } = await backupManager.createBackup('teleportation setup');
375
+ const shortBackupPath = backupPath.replace(HOME_DIR, '~');
376
+ console.log(c.green(` ✅ Backup saved to ${shortBackupPath}/`));
377
+ console.log(c.cyan(`\n To restore if needed: ${c.green('teleportation backup restore')}\n`));
378
+
379
+ const proceed = await question('Continue with setup? (Y/n): ');
380
+ if (proceed.toLowerCase() === 'n' || proceed.toLowerCase() === 'no') {
381
+ console.log(c.yellow('\nSetup cancelled.\n'));
382
+ rl.close();
383
+ return;
384
+ }
385
+ }
386
+
387
+ // Step 1: Authentication
388
+ console.log('\n' + c.purple('Step 1 of 4: Authentication\n'));
389
+ console.log(' You\'ll need to sign in to get an API key.\n');
390
+
391
+ // Try to open browser
392
+ const appUrl = 'https://app.teleportation.dev/login';
393
+ console.log(c.cyan(` → Opening ${appUrl} in your browser...`));
394
+
395
+ try {
396
+ const openCommand = process.platform === 'darwin' ? 'open' :
397
+ process.platform === 'win32' ? 'start' : 'xdg-open';
398
+ execSync(`${openCommand} ${appUrl}`, { stdio: 'ignore' });
399
+ } catch (e) {
400
+ console.log(c.yellow(` ⚠️ Could not open browser. Please visit: ${appUrl}`));
401
+ }
402
+
403
+ console.log('\n After signing in:');
404
+ console.log(' 1. Click "Keys" in the bottom navigation');
405
+ console.log(' 2. Click "Create" to generate a new API key');
406
+ console.log(' 3. Copy the key and paste it below\n');
407
+
408
+ const apiKey = await question(' Paste your API key: ');
409
+
410
+ if (!apiKey || !apiKey.trim()) {
411
+ console.log(c.red('\n ❌ No API key provided. Setup cancelled.\n'));
412
+ rl.close();
413
+ return;
414
+ }
415
+
416
+ const trimmedKey = apiKey.trim();
417
+ const relayUrl = 'https://api.teleportation.dev';
418
+
419
+ // Validate API key
420
+ console.log(c.cyan('\n Validating API key...'));
421
+ try {
422
+ const apiKeyPath = path.join(TELEPORTATION_DIR, 'lib', 'auth', 'api-key.js');
423
+ const { validateApiKey } = await import('file://' + apiKeyPath);
424
+ const result = await validateApiKey(trimmedKey, relayUrl);
425
+
426
+ if (!result.valid) {
427
+ console.log(c.red(`\n ❌ ${result.error}`));
428
+ console.log(c.yellow(' Please check your API key and try again.\n'));
429
+ rl.close();
430
+ return;
431
+ }
432
+ } catch (e) {
433
+ console.log(c.yellow(`\n ⚠️ Could not validate API key: ${e.message}`));
434
+ console.log(c.cyan(' Continuing anyway (will validate on first use)...'));
435
+ }
436
+
437
+ console.log(c.green(' ✅ API key validated successfully!'));
438
+
439
+ // Step 2: Configuration
440
+ console.log('\n' + c.purple('Step 2 of 4: Configuration\n'));
441
+ console.log(` Relay URL: ${c.cyan(relayUrl)}`);
442
+
443
+ // Test relay connectivity
444
+ console.log(c.cyan(' Testing connectivity...'));
445
+ try {
446
+ const start = Date.now();
447
+ const response = await fetch(`${relayUrl}/health`, {
448
+ signal: AbortSignal.timeout(5000)
449
+ });
450
+ const elapsed = Date.now() - start;
451
+
452
+ if (response.ok) {
453
+ console.log(c.green(` ✅ Relay is reachable (responded in ${elapsed}ms)`));
454
+ } else {
455
+ console.log(c.yellow(` ⚠️ Relay returned status ${response.status}`));
456
+ }
457
+ } catch (e) {
458
+ console.log(c.yellow(` ⚠️ Could not reach relay: ${e.message}`));
459
+ console.log(c.cyan(' Continuing anyway (will retry on use)...'));
460
+ }
461
+
462
+ // Save credentials
463
+ console.log(c.cyan('\n Saving credentials...'));
464
+ const manager = await loadCredentialManager();
465
+ if (manager) {
466
+ const credentials = {
467
+ apiKey: trimmedKey,
468
+ relayApiUrl: relayUrl,
469
+ authenticatedAt: Date.now(),
470
+ method: 'setup-wizard'
471
+ };
472
+ await manager.save(credentials);
473
+ console.log(c.green(' ✅ Credentials saved'));
474
+ }
475
+
476
+ // Update config to match
477
+ const configManagerPath = path.join(TELEPORTATION_DIR, 'lib', 'config', 'manager.js');
478
+ try {
479
+ const { setConfigValue } = await import('file://' + configManagerPath);
480
+ await setConfigValue('relay.url', relayUrl);
481
+ console.log(c.green(' ✅ Configuration updated'));
482
+ } catch (e) {
483
+ console.log(c.yellow(` ⚠️ Could not update config: ${e.message}`));
484
+ }
485
+
486
+ // Step 3: Install Hooks
487
+ console.log('\n' + c.purple('Step 3 of 4: Installing Hooks\n'));
488
+ console.log(' Installing Claude Code hooks to ~/.claude/hooks/');
489
+
490
+ try {
491
+ const installerPath = path.join(TELEPORTATION_DIR, 'lib', 'install', 'installer.js');
492
+ const { install } = await import('file://' + installerPath);
493
+ const sourceHooksDir = path.join(TELEPORTATION_DIR, '.claude', 'hooks');
494
+ const result = await install(sourceHooksDir);
495
+ console.log(c.green(` ✅ ${result.hooksVerified} hooks installed successfully`));
496
+ } catch (e) {
497
+ console.log(c.red(` ❌ Failed to install hooks: ${e.message}`));
498
+ console.log(c.yellow('\n Would you like to restore your previous configuration?'));
499
+ const restore = await question(' Restore backup? (y/N): ');
500
+ if (restore.toLowerCase() === 'y' || restore.toLowerCase() === 'yes') {
501
+ console.log(c.cyan('\n Restoring from backup...'));
502
+ try {
503
+ await backupManager.restore();
504
+ console.log(c.green(' ✅ Previous configuration restored'));
505
+ } catch (restoreErr) {
506
+ console.log(c.red(` ❌ Failed to restore: ${restoreErr.message}`));
507
+ }
508
+ }
509
+ rl.close();
510
+ return;
511
+ }
512
+
513
+ // Step 4: Verification
514
+ console.log('\n' + c.purple('Step 4 of 4: Verification\n'));
515
+ console.log(c.green(' ✅ Credentials saved'));
516
+ console.log(c.green(' ✅ Configuration saved'));
517
+ console.log(c.green(' ✅ Hooks installed'));
518
+ console.log(c.green(' ✅ Relay connectivity confirmed'));
519
+
520
+ // Success!
521
+ console.log('\n' + c.cyan('╭─────────────────────────────────────────────────────╮'));
522
+ console.log(c.cyan('│ │'));
523
+ console.log(c.cyan('│ 🎉 ') + c.green('Setup Complete!') + c.cyan(' │'));
524
+ console.log(c.cyan('│ │'));
525
+ console.log(c.cyan('│ ') + c.yellow('⚠️ Important:') + c.cyan(' Restart Claude Code to activate │'));
526
+ console.log(c.cyan('│ teleportation for your current session. │'));
527
+ console.log(c.cyan('│ │'));
528
+ console.log(c.cyan('│ Then open ') + c.green('https://app.teleportation.dev') + c.cyan(' on your │'));
529
+ console.log(c.cyan('│ phone to approve actions remotely. │'));
530
+ console.log(c.cyan('│ │'));
531
+ console.log(c.cyan('╰─────────────────────────────────────────────────────╯\n'));
532
+
533
+ rl.close();
534
+
535
+ } catch (error) {
536
+ console.log(c.red(`\n❌ Setup failed: ${error.message}\n`));
537
+ rl.close();
538
+ process.exit(1);
539
+ }
540
+ }
541
+
542
+ /**
543
+ * Backup command - list, create, restore backups
544
+ */
545
+ async function commandBackup(args) {
546
+ const subcommand = args[0] || 'list';
547
+
548
+ const backupManagerPath = path.join(TELEPORTATION_DIR, 'lib', 'backup', 'manager.js');
549
+ const { BackupManager } = await import('file://' + backupManagerPath);
550
+ const backupManager = new BackupManager();
551
+
552
+ switch (subcommand) {
553
+ case 'list': {
554
+ console.log(c.purple('Teleportation Backups\n'));
555
+
556
+ const backups = backupManager.listBackups();
557
+
558
+ if (backups.length === 0) {
559
+ console.log(c.yellow(' No backups found.\n'));
560
+ console.log(c.cyan(' Backups are created automatically during setup.'));
561
+ console.log(c.cyan(' Run: teleportation backup create\n'));
562
+ return;
563
+ }
564
+
565
+ backups.forEach((backup, index) => {
566
+ const isLatest = index === 0;
567
+ const date = new Date(backup.timestamp).toLocaleString();
568
+ console.log(` ${index + 1}. ${c.cyan(backup.id)}${isLatest ? c.green(' (latest)') : ''}`);
569
+ console.log(` Reason: ${backup.reason}`);
570
+ console.log(` Date: ${date}`);
571
+ console.log(` Files: ${backup.fileCount} backed up\n`);
572
+ });
573
+
574
+ console.log(c.cyan(' To restore: teleportation backup restore [backup-id]\n'));
575
+ break;
576
+ }
577
+
578
+ case 'create': {
579
+ console.log(c.purple('Creating Backup\n'));
580
+
581
+ const reason = args[1] || 'manual backup';
582
+ console.log(c.cyan(' Creating backup...'));
583
+
584
+ try {
585
+ const { backupPath, manifest } = await backupManager.createBackup(reason);
586
+ const shortPath = backupPath.replace(HOME_DIR, '~');
587
+ console.log(c.green(`\n ✅ Backup created: ${shortPath}/`));
588
+ console.log(` Files backed up: ${manifest.files.length}\n`);
589
+ } catch (e) {
590
+ console.log(c.red(`\n ❌ Failed to create backup: ${e.message}\n`));
591
+ process.exit(1);
592
+ }
593
+ break;
594
+ }
595
+
596
+ case 'restore': {
597
+ const backupId = args[1] || null;
598
+
599
+ console.log(c.purple('Restoring from Backup\n'));
600
+
601
+ if (!backupId) {
602
+ console.log(c.cyan(' Restoring from latest backup...'));
603
+ } else {
604
+ console.log(c.cyan(` Restoring from backup: ${backupId}...`));
605
+ }
606
+
607
+ try {
608
+ const { manifest, restoredFiles } = await backupManager.restore(backupId);
609
+ console.log(c.green(`\n ✅ Restored ${restoredFiles.length} files from backup\n`));
610
+
611
+ for (const file of restoredFiles) {
612
+ const shortPath = file.replace(HOME_DIR, '~');
613
+ console.log(c.green(` ✅ ${shortPath}`));
614
+ }
615
+
616
+ console.log(c.yellow('\n ⚠️ Restart Claude Code to apply restored settings.\n'));
617
+ } catch (e) {
618
+ console.log(c.red(`\n ❌ Failed to restore: ${e.message}\n`));
619
+ process.exit(1);
620
+ }
621
+ break;
622
+ }
623
+
624
+ case 'delete': {
625
+ const backupId = args[1];
626
+
627
+ if (!backupId) {
628
+ console.log(c.red(' ❌ Please specify a backup ID to delete.\n'));
629
+ console.log(c.cyan(' Usage: teleportation backup delete <backup-id>\n'));
630
+ return;
631
+ }
632
+
633
+ console.log(c.purple('Deleting Backup\n'));
634
+ console.log(c.cyan(` Deleting backup: ${backupId}...`));
635
+
636
+ try {
637
+ backupManager.deleteBackup(backupId);
638
+ console.log(c.green(`\n ✅ Backup deleted: ${backupId}\n`));
639
+ } catch (e) {
640
+ console.log(c.red(`\n ❌ Failed to delete: ${e.message}\n`));
641
+ }
642
+ break;
643
+ }
644
+
645
+ default:
646
+ console.log(c.red(` Unknown backup subcommand: ${subcommand}\n`));
647
+ console.log(c.cyan(' Available commands:'));
648
+ console.log(' backup list - List all backups');
649
+ console.log(' backup create - Create a new backup');
650
+ console.log(' backup restore - Restore from backup');
651
+ console.log(' backup delete - Delete a backup\n');
652
+ }
653
+ }
654
+
655
+ function commandOff() {
656
+ console.log(c.yellow('🛑 Disabling Teleportation Remote Control...\n'));
657
+
658
+ // Remove settings.json
659
+ if (fs.existsSync(config.globalSettings)) {
660
+ fs.unlinkSync(config.globalSettings);
661
+ console.log(c.green('✅ Removed ~/.claude/settings.json'));
662
+ }
663
+
664
+ // Remove hooks
665
+ if (fs.existsSync(config.globalHooks)) {
666
+ const hooks = fs.readdirSync(config.globalHooks).filter(f => f.endsWith('.mjs'));
667
+ hooks.forEach(hook => {
668
+ fs.unlinkSync(path.join(config.globalHooks, hook));
669
+ });
670
+ console.log(c.green(`✅ Removed ${hooks.length} hooks`));
671
+ }
672
+
673
+ console.log(c.yellow('\n🛑 Teleportation Remote Control DISABLED'));
674
+ console.log(c.cyan('Services are still running. Stop with: ./teleportation stop\n'));
675
+ }
676
+
677
+ async function commandStatus() {
678
+ console.log(c.purple('Teleportation System Status\n'));
679
+
680
+ let issues = [];
681
+ let warnings = [];
682
+
683
+ // Authentication status
684
+ console.log(c.yellow('Authentication:'));
685
+ const creds = await getCredentials();
686
+ const hasCredentials = await loadCredentials();
687
+
688
+ if (hasCredentials) {
689
+ console.log(' ' + c.green('✅') + ' Logged in');
690
+ console.log(' Source: Encrypted credentials file');
691
+ console.log(' API key: ' + c.cyan('***' + (creds.RELAY_API_KEY?.slice(-4) || '????')));
692
+ } else if (creds.RELAY_API_KEY) {
693
+ console.log(' ' + c.yellow('⚠️') + ' Using environment variables');
694
+ console.log(' API key: ' + c.cyan('***' + creds.RELAY_API_KEY.slice(-4)));
695
+ warnings.push('Consider running `teleportation login` for encrypted credential storage');
696
+ } else {
697
+ console.log(' ' + c.red('❌') + ' Not logged in');
698
+ issues.push('Run `teleportation setup` to configure authentication');
699
+ }
700
+
701
+ // Relay connection status
702
+ console.log('\n' + c.yellow('Relay Connection:'));
703
+ const relayUrl = creds.RELAY_API_URL;
704
+
705
+ if (!relayUrl) {
706
+ console.log(' ' + c.red('❌') + ' Relay URL not configured');
707
+ issues.push('Run `teleportation setup` to configure relay URL');
708
+ } else {
709
+ console.log(' URL: ' + c.cyan(relayUrl));
710
+
711
+ // Test connectivity
712
+ try {
713
+ const start = Date.now();
714
+ const response = await fetch(`${relayUrl}/health`, {
715
+ signal: AbortSignal.timeout(5000)
716
+ });
717
+ const elapsed = Date.now() - start;
718
+
719
+ if (response.ok) {
720
+ console.log(' ' + c.green('✅') + ` Reachable (${elapsed}ms)`);
721
+ try {
722
+ const health = await response.json();
723
+ if (health.version) {
724
+ console.log(' Version: ' + c.cyan(health.version));
725
+ }
726
+ } catch (e) {
727
+ // Ignore JSON parse errors
728
+ }
729
+ } else {
730
+ console.log(' ' + c.yellow('⚠️') + ` Returned status ${response.status}`);
731
+ warnings.push(`Relay returned HTTP ${response.status}`);
732
+ }
733
+ } catch (e) {
734
+ if (e.name === 'TimeoutError') {
735
+ console.log(' ' + c.red('❌') + ' Connection timed out');
736
+ issues.push('Relay is not responding. Check your internet connection.');
737
+ } else {
738
+ console.log(' ' + c.red('❌') + ` Cannot reach: ${e.message}`);
739
+ issues.push('Relay is unreachable. Check the URL and your internet connection.');
740
+ }
741
+ }
742
+
743
+ // Validate API key if we have one
744
+ if (creds.RELAY_API_KEY) {
745
+ try {
746
+ const apiKeyPath = path.join(TELEPORTATION_DIR, 'lib', 'auth', 'api-key.js');
747
+ const { validateApiKey } = await import('file://' + apiKeyPath);
748
+ const result = await validateApiKey(creds.RELAY_API_KEY, relayUrl);
749
+
750
+ if (result.valid) {
751
+ console.log(' ' + c.green('✅') + ' API key validated');
752
+ } else {
753
+ console.log(' ' + c.red('❌') + ` API key invalid: ${result.error}`);
754
+ issues.push('API key is invalid. Create a new one at app.teleportation.dev/api-keys');
755
+ }
756
+ } catch (e) {
757
+ console.log(' ' + c.yellow('⚠️') + ' Could not validate API key');
758
+ }
759
+ }
760
+ }
761
+
762
+ // Hooks status
763
+ console.log('\n' + c.yellow('Hooks:'));
764
+ const hooksConfigured = config.isConfigured();
765
+
766
+ if (hooksConfigured) {
767
+ console.log(' ' + c.green('✅') + ' Enabled in Claude Code settings');
768
+ const hookFiles = fs.readdirSync(config.globalHooks).filter(f => f.endsWith('.mjs'));
769
+ console.log(' ' + c.green('✅') + ` ${hookFiles.length} hook files installed`);
770
+ console.log(' Directory: ~/.claude/hooks/');
771
+ } else {
772
+ console.log(' ' + c.red('❌') + ' Hooks not installed');
773
+ issues.push('Run `teleportation setup` to install hooks');
774
+ }
775
+
776
+ // Config/credentials sync check
777
+ console.log('\n' + c.yellow('Configuration:'));
778
+ try {
779
+ const configManagerPath = path.join(TELEPORTATION_DIR, 'lib', 'config', 'manager.js');
780
+ const { getConfigValue } = await import('file://' + configManagerPath);
781
+ const configRelayUrl = await getConfigValue('relay.url');
782
+
783
+ if (configRelayUrl && relayUrl && configRelayUrl !== relayUrl) {
784
+ console.log(' ' + c.yellow('⚠️') + ' Config/credentials mismatch');
785
+ console.log(' Config relay.url: ' + c.cyan(configRelayUrl));
786
+ console.log(' Credentials URL: ' + c.cyan(relayUrl));
787
+ warnings.push('Config and credentials have different relay URLs. Run `teleportation setup` to fix.');
788
+ } else {
789
+ console.log(' ' + c.green('✅') + ' Config and credentials in sync');
790
+ }
791
+ } catch (e) {
792
+ console.log(' ' + c.yellow('⚠️') + ' Could not check config sync');
793
+ }
794
+
795
+ // Session/restart check
796
+ const sessionMarkerPath = path.join(HOME_DIR, '.teleportation', '.session_marker');
797
+ const credentialsPath = path.join(HOME_DIR, '.teleportation', 'credentials');
798
+
799
+ if (fs.existsSync(sessionMarkerPath) && fs.existsSync(credentialsPath)) {
800
+ try {
801
+ const markerMtime = fs.statSync(sessionMarkerPath).mtimeMs;
802
+ const credsMtime = fs.statSync(credentialsPath).mtimeMs;
803
+
804
+ if (credsMtime > markerMtime) {
805
+ console.log(' ' + c.yellow('⚠️') + ' Credentials updated since session started');
806
+ console.log(' Session started: ' + c.cyan(new Date(markerMtime).toLocaleTimeString()));
807
+ console.log(' Credentials updated: ' + c.cyan(new Date(credsMtime).toLocaleTimeString()));
808
+ warnings.push('Credentials changed after session started. Restart Claude Code to apply changes.');
809
+ }
810
+ } catch (e) {
811
+ // Ignore errors checking session marker
812
+ }
813
+ }
814
+
815
+ // Local services (only show if relevant)
816
+ const localRelayRunning = checkService('relay', 3030);
817
+ const localStorageRunning = checkService('storage', 3040);
818
+
819
+ if (localRelayRunning || localStorageRunning) {
820
+ console.log('\n' + c.yellow('Local Services:'));
821
+ if (localRelayRunning) {
822
+ console.log(' ' + c.green('✅') + ' Local relay running (port 3030)');
823
+ }
824
+ if (localStorageRunning) {
825
+ console.log(' ' + c.green('✅') + ' Local storage running (port 3040)');
826
+ }
827
+ }
828
+
829
+ // Overall status
830
+ console.log('\n' + c.yellow('─────────────────────────────────────────'));
831
+
832
+ if (issues.length === 0 && warnings.length === 0) {
833
+ console.log(c.green('\n🎉 All systems operational!'));
834
+ console.log(c.cyan('\nOpen https://app.teleportation.dev on your phone to approve actions.\n'));
835
+ } else if (issues.length === 0) {
836
+ console.log(c.yellow('\n⚠️ System operational with warnings:\n'));
837
+ warnings.forEach(w => console.log(' • ' + w));
838
+ console.log();
839
+ } else {
840
+ console.log(c.red('\n❌ Issues found:\n'));
841
+ issues.forEach(i => console.log(' • ' + i));
842
+ if (warnings.length > 0) {
843
+ console.log(c.yellow('\nWarnings:'));
844
+ warnings.forEach(w => console.log(' • ' + w));
845
+ }
846
+ console.log(c.cyan('\n→ Run: teleportation setup\n'));
847
+ }
848
+ }
849
+
850
+ function commandStart() {
851
+ console.log(c.yellow('🚀 Starting Teleportation services...\n'));
852
+
853
+ // Note: Relay and storage APIs are separate services
854
+ // These paths are for local development only
855
+ const internalRelayDir = path.join(TELEPORTATION_DIR, 'relay');
856
+ const relayDir = process.env.RELAY_DIR || (fs.existsSync(internalRelayDir) ? internalRelayDir : path.join(TELEPORTATION_DIR, '..', 'detach', 'relay'));
857
+
858
+ // Storage API is expected to be in the 'storage-api' directory within the teleportation project
859
+ const storageDir = process.env.STORAGE_DIR || path.join(TELEPORTATION_DIR, 'storage-api');
860
+ const logDir = path.join(HOME_DIR, 'Library', 'Logs');
861
+
862
+ // Check if already running
863
+ if (checkService('relay', 3030)) {
864
+ console.log(c.yellow('⚠️ Relay API already running'));
865
+ } else {
866
+ // Start relay - use environment file instead of command line to avoid credential exposure
867
+ try {
868
+ const { spawn } = require('child_process');
869
+ const envFile = path.join(relayDir, '.env.relay');
870
+ const envContent = `RELAY_API_KEY=dev-key-123\nPORT=3030\n`;
871
+
872
+ // Write env file with secure permissions
873
+ fs.writeFileSync(envFile, envContent, { mode: 0o600 });
874
+
875
+ // Use spawn with env file instead of execSync with command line
876
+ const child = spawn('node', ['server.js'], {
877
+ cwd: relayDir,
878
+ env: { ...process.env, RELAY_API_KEY: 'dev-key-123', PORT: '3030' },
879
+ detached: true,
880
+ stdio: ['ignore', 'ignore', 'ignore']
881
+ });
882
+
883
+ // Write PID and redirect output
884
+ const logFile = path.join(logDir, 'teleportation-relay.log');
885
+ const logStream = fs.createWriteStream(logFile, { flags: 'a' });
886
+ child.unref();
887
+
888
+ console.log(c.green('✅ Relay API started on port 3030'));
889
+ } catch (e) {
890
+ console.log(c.red('❌ Failed to start Relay API: ' + e.message));
891
+ }
892
+ }
893
+
894
+ if (checkService('storage', 3040)) {
895
+ console.log(c.yellow('⚠️ Storage API already running'));
896
+ } else {
897
+ // Start storage - use proper env parsing instead of shell injection
898
+ try {
899
+ const { spawn } = require('child_process');
900
+ const envFile = path.join(storageDir, '.env.local');
901
+
902
+ if (fs.existsSync(envFile)) {
903
+ // Parse .env file safely
904
+ const envContent = fs.readFileSync(envFile, 'utf8');
905
+ const envVars = {};
906
+
907
+ // Parse env file line by line (simple parser, no shell execution)
908
+ for (const line of envContent.split('\n')) {
909
+ const trimmed = line.trim();
910
+ if (!trimmed || trimmed.startsWith('#')) continue;
911
+
912
+ const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
913
+ if (match) {
914
+ const key = match[1];
915
+ let value = match[2];
916
+
917
+ // Remove quotes if present
918
+ if ((value.startsWith('"') && value.endsWith('"')) ||
919
+ (value.startsWith("'") && value.endsWith("'"))) {
920
+ value = value.slice(1, -1);
921
+ }
922
+
923
+ envVars[key] = value;
924
+ }
925
+ }
926
+
927
+ // Use spawn with parsed env vars instead of shell command
928
+ const child = spawn('node', ['server.js'], {
929
+ cwd: storageDir,
930
+ env: { ...process.env, ...envVars },
931
+ detached: true,
932
+ stdio: ['ignore', 'ignore', 'ignore']
933
+ });
934
+
935
+ child.unref();
936
+ console.log(c.green('✅ Storage API started on port 3040'));
937
+ } else {
938
+ console.log(c.red('❌ Storage API .env.local not found'));
939
+ }
940
+ } catch (e) {
941
+ console.log(c.red('❌ Failed to start Storage API: ' + e.message));
942
+ }
943
+ }
944
+
945
+ console.log(c.cyan('\nWait 2 seconds, then check: ./teleportation status\n'));
946
+ }
947
+
948
+ function commandStop() {
949
+ console.log(c.yellow('🛑 Stopping Teleportation services...\n'));
950
+
951
+ try {
952
+ execSync('pkill -f "relay/server.js"', { stdio: 'ignore' });
953
+ console.log(c.green('✅ Stopped Relay API'));
954
+ } catch (e) {
955
+ console.log(c.yellow('⚠️ Relay API not running'));
956
+ }
957
+
958
+ try {
959
+ execSync('pkill -f "storage-api/server.js"', { stdio: 'ignore' });
960
+ console.log(c.green('✅ Stopped Storage API'));
961
+ } catch (e) {
962
+ console.log(c.yellow('⚠️ Storage API not running'));
963
+ }
964
+
965
+ console.log(c.cyan('\nServices stopped\n'));
966
+ }
967
+
968
+ function commandRestart() {
969
+ commandStop();
970
+ setTimeout(() => {
971
+ commandStart();
972
+ }, 1000);
973
+ }
974
+
975
+ async function commandEnv(args) {
976
+ const subcommand = args[0];
977
+
978
+ if (subcommand === 'set') {
979
+ console.log(c.yellow('Setting environment variables...\n'));
980
+ console.log(c.yellow('Note: Consider using "teleportation login" for encrypted credential storage instead.\n'));
981
+
982
+ const envContent = `
983
+ # Teleportation Remote Control Environment Variables
984
+ export RELAY_API_URL="http://localhost:3030"
985
+ export RELAY_API_KEY="dev-key-123"
986
+ export SLACK_WEBHOOK_URL=""
987
+ `;
988
+
989
+ // Check if already in .zshrc
990
+ if (fs.existsSync(config.zshrc)) {
991
+ const content = fs.readFileSync(config.zshrc, 'utf8');
992
+ if (content.includes('RELAY_API_URL')) {
993
+ console.log(c.yellow('⚠️ Environment variables already in ~/.zshrc'));
994
+ } else {
995
+ fs.appendFileSync(config.zshrc, envContent);
996
+ console.log(c.green('✅ Added to ~/.zshrc'));
997
+ }
998
+ }
999
+
1000
+ console.log(c.cyan('\nTo apply now, run:'));
1001
+ console.log(c.green(' source ~/.zshrc\n'));
1002
+ console.log(c.cyan('Or restart your terminal\n'));
1003
+ } else {
1004
+ // Show current credentials (from file or env)
1005
+ const creds = await getCredentials();
1006
+ const hasFileCreds = await loadCredentials();
1007
+
1008
+ console.log(c.yellow('Credentials:\n'));
1009
+ if (hasFileCreds) {
1010
+ console.log(' Source:', c.green('Encrypted file (~/.teleportation/credentials)'));
1011
+ } else {
1012
+ console.log(' Source:', c.yellow('Environment variables'));
1013
+ }
1014
+ console.log(' RELAY_API_URL:', creds.RELAY_API_URL || c.red('not set'));
1015
+ console.log(' RELAY_API_KEY:', creds.RELAY_API_KEY ? '***' + creds.RELAY_API_KEY.slice(-4) : c.red('not set'));
1016
+ console.log(' SLACK_WEBHOOK_URL:', creds.SLACK_WEBHOOK_URL || c.yellow('not set (optional)'));
1017
+ console.log();
1018
+ }
1019
+ }
1020
+
1021
+ async function commandTest() {
1022
+ console.log(c.purple('Running Teleportation Diagnostics...\n'));
1023
+
1024
+ let passed = 0;
1025
+ let failed = 0;
1026
+
1027
+ // Test 1: Hooks configured
1028
+ console.log(c.yellow('Test 1: Hooks Configuration'));
1029
+ if (config.isConfigured()) {
1030
+ console.log(c.green(' ✅ PASS - Hooks configured in ~/.claude/\n'));
1031
+ passed++;
1032
+ } else {
1033
+ console.log(c.red(' ❌ FAIL - Hooks not configured\n'));
1034
+ failed++;
1035
+ }
1036
+
1037
+ // Test 2: Credentials (file or env vars)
1038
+ console.log(c.yellow('Test 2: Credentials'));
1039
+ const creds = await getCredentials();
1040
+ if (creds.RELAY_API_URL && creds.RELAY_API_KEY) {
1041
+ const source = await loadCredentials() ? 'encrypted file' : 'environment variables';
1042
+ console.log(c.green(` ✅ PASS - Credentials loaded from ${source}\n`));
1043
+ passed++;
1044
+ } else {
1045
+ console.log(c.red(' ❌ FAIL - Credentials missing\n'));
1046
+ failed++;
1047
+ }
1048
+
1049
+ // Test 3: Relay service
1050
+ console.log(c.yellow('Test 3: Relay API Service'));
1051
+ const relayUrl = creds.RELAY_API_URL || 'https://api.teleportation.dev';
1052
+ if (checkService('relay', 3030) && checkServiceHealth(relayUrl)) {
1053
+ console.log(c.green(' ✅ PASS - Relay API running and healthy\n'));
1054
+ passed++;
1055
+ } else {
1056
+ console.log(c.red(' ❌ FAIL - Relay API not running or unhealthy\n'));
1057
+ failed++;
1058
+ }
1059
+
1060
+ // Test 4: Storage service
1061
+ console.log(c.yellow('Test 4: Storage API Service'));
1062
+ if (checkService('storage', 3040) && checkServiceHealth('http://localhost:3040')) {
1063
+ console.log(c.green(' ✅ PASS - Storage API running and healthy\n'));
1064
+ passed++;
1065
+ } else {
1066
+ console.log(c.red(' ❌ FAIL - Storage API not running or unhealthy\n'));
1067
+ failed++;
1068
+ }
1069
+
1070
+ // Test 5: Hook execution
1071
+ console.log(c.yellow('Test 5: Hook Execution'));
1072
+ try {
1073
+ const testHook = path.join(config.globalHooks, 'pre_tool_use.mjs');
1074
+ if (fs.existsSync(testHook)) {
1075
+ const testInput = '{"session_id":"test","tool_name":"Read","tool_input":{}}';
1076
+ const envVars = `RELAY_API_URL="${creds.RELAY_API_URL || 'http://localhost:3030'}" RELAY_API_KEY="${creds.RELAY_API_KEY || 'dev-key-123'}"`;
1077
+ execSync(`echo '${testInput}' | ${envVars} node ${testHook}`, { stdio: 'ignore' });
1078
+ console.log(c.green(' ✅ PASS - Hook executes successfully\n'));
1079
+ passed++;
1080
+ } else {
1081
+ console.log(c.red(' ❌ FAIL - Hook file not found\n'));
1082
+ failed++;
1083
+ }
1084
+ } catch (e) {
1085
+ console.log(c.red(' ❌ FAIL - Hook execution error\n'));
1086
+ failed++;
1087
+ }
1088
+
1089
+ // Summary
1090
+ console.log(c.purple('Test Summary:'));
1091
+ console.log(` Passed: ${c.green(passed)}`);
1092
+ console.log(` Failed: ${c.red(failed)}`);
1093
+
1094
+ if (failed === 0) {
1095
+ console.log(c.green('\n🎉 All tests passed! System is ready.\n'));
1096
+ } else {
1097
+ console.log(c.yellow('\n⚠️ Some tests failed. Run ./teleportation status for details.\n'));
1098
+ }
1099
+ }
1100
+
1101
+ async function commandDoctor() {
1102
+ console.log(c.purple('🔍 Teleportation Doctor - System Diagnostics\n'));
1103
+
1104
+ const issues = [];
1105
+ const recommendations = [];
1106
+ let checksPassed = 0;
1107
+ let checksFailed = 0;
1108
+
1109
+ // Check 1: Claude Code installation
1110
+ console.log(c.yellow('1. Claude Code Installation'));
1111
+ try {
1112
+ const claudeCodePath = execSync('which claude', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
1113
+ if (claudeCodePath) {
1114
+ console.log(c.green(` ✅ Found: ${claudeCodePath}\n`));
1115
+ checksPassed++;
1116
+ } else {
1117
+ console.log(c.yellow(' ⚠️ Claude Code not found in PATH\n'));
1118
+ issues.push('Claude Code not found');
1119
+ recommendations.push('Install Claude Code or add it to your PATH');
1120
+ checksFailed++;
1121
+ }
1122
+ } catch (e) {
1123
+ console.log(c.yellow(' ⚠️ Could not detect Claude Code installation\n'));
1124
+ issues.push('Claude Code detection failed');
1125
+ checksFailed++;
1126
+ }
1127
+
1128
+ // Check 2: Hooks installation
1129
+ console.log(c.yellow('2. Hooks Installation'));
1130
+ const hooksConfigured = config.isConfigured();
1131
+ if (hooksConfigured) {
1132
+ const hookFiles = fs.readdirSync(config.globalHooks).filter(f => f.endsWith('.mjs'));
1133
+ console.log(c.green(` ✅ ${hookFiles.length} hooks installed\n`));
1134
+ hookFiles.forEach(f => {
1135
+ const hookPath = path.join(config.globalHooks, f);
1136
+ const stats = fs.statSync(hookPath);
1137
+ const isExecutable = (stats.mode & parseInt('111', 8)) !== 0;
1138
+ if (isExecutable) {
1139
+ console.log(c.green(` • ${f} (executable)\n`));
1140
+ } else {
1141
+ console.log(c.yellow(` • ${f} (not executable)\n`));
1142
+ issues.push(`Hook ${f} is not executable`);
1143
+ recommendations.push(`Run: chmod +x ${hookPath}`);
1144
+ }
1145
+ });
1146
+ checksPassed++;
1147
+ } else {
1148
+ console.log(c.red(' ❌ Hooks not configured\n'));
1149
+ issues.push('Hooks not installed');
1150
+ recommendations.push('Run: teleportation on');
1151
+ checksFailed++;
1152
+ }
1153
+
1154
+ // Check 3: Credentials
1155
+ console.log(c.yellow('3. Credentials'));
1156
+ const manager = await loadCredentialManager();
1157
+ if (manager) {
1158
+ const credentials = await manager.load();
1159
+ if (credentials) {
1160
+ console.log(c.green(' ✅ Credentials found (encrypted file)\n'));
1161
+
1162
+ // Check if expired
1163
+ const isExpired = await manager.isExpired();
1164
+ if (isExpired) {
1165
+ console.log(c.red(' ❌ Credentials expired\n'));
1166
+ issues.push('Credentials expired');
1167
+ recommendations.push('Run: teleportation login');
1168
+ checksFailed++;
1169
+ } else {
1170
+ const daysUntil = await manager.daysUntilExpiry();
1171
+ if (daysUntil !== null) {
1172
+ if (daysUntil < 7) {
1173
+ console.log(c.yellow(` ⚠️ Credentials expire in ${daysUntil} days\n`));
1174
+ recommendations.push('Consider refreshing credentials soon');
1175
+ } else {
1176
+ console.log(c.green(` ✅ Credentials valid for ${daysUntil} more days\n`));
1177
+ }
1178
+ }
1179
+ checksPassed++;
1180
+ }
1181
+ } else {
1182
+ // Check environment variables
1183
+ const envCreds = {
1184
+ RELAY_API_URL: process.env.RELAY_API_URL,
1185
+ RELAY_API_KEY: process.env.RELAY_API_KEY
1186
+ };
1187
+ if (envCreds.RELAY_API_URL && envCreds.RELAY_API_KEY) {
1188
+ console.log(c.yellow(' ⚠️ Using environment variables (not encrypted)\n'));
1189
+ recommendations.push('Consider using: teleportation login');
1190
+ checksPassed++;
1191
+ } else {
1192
+ console.log(c.red(' ❌ No credentials found\n'));
1193
+ issues.push('No credentials');
1194
+ recommendations.push('Run: teleportation login');
1195
+ checksFailed++;
1196
+ }
1197
+ }
1198
+ } else {
1199
+ console.log(c.red(' ❌ Credential manager unavailable\n'));
1200
+ checksFailed++;
1201
+ }
1202
+
1203
+ // Check 4: Relay API connection
1204
+ console.log(c.yellow('4. Relay API Connection'));
1205
+ const creds = await getCredentials();
1206
+ const relayUrl = creds.RELAY_API_URL || 'https://api.teleportation.dev';
1207
+
1208
+ if (!relayUrl) {
1209
+ console.log(c.red(' ❌ Relay API URL not configured\n'));
1210
+ issues.push('Relay API URL missing');
1211
+ recommendations.push('Set RELAY_API_URL or run: teleportation login');
1212
+ checksFailed++;
1213
+ } else {
1214
+ const startTime = Date.now();
1215
+ try {
1216
+ const response = await fetch(`${relayUrl}/health`, {
1217
+ method: 'GET',
1218
+ signal: AbortSignal.timeout(5000)
1219
+ });
1220
+ const latency = Date.now() - startTime;
1221
+
1222
+ if (response.ok) {
1223
+ console.log(c.green(` ✅ Connected (${latency}ms latency)\n`));
1224
+ checksPassed++;
1225
+
1226
+ if (latency > 1000) {
1227
+ console.log(c.yellow(` ⚠️ High latency detected (${latency}ms)\n`));
1228
+ recommendations.push('Consider using a relay API closer to your location');
1229
+ }
1230
+ } else {
1231
+ console.log(c.red(` ❌ API returned status ${response.status}\n`));
1232
+ issues.push(`Relay API unhealthy (status ${response.status})`);
1233
+ checksFailed++;
1234
+ }
1235
+ } catch (error) {
1236
+ const latency = Date.now() - startTime;
1237
+ console.log(c.red(` ❌ Connection failed: ${error.message}\n`));
1238
+ issues.push(`Cannot connect to relay API at ${relayUrl}`);
1239
+ recommendations.push('Check if relay API is running: teleportation start');
1240
+ recommendations.push(`Verify URL is correct: ${relayUrl}`);
1241
+ checksFailed++;
1242
+ }
1243
+ }
1244
+
1245
+ // Check 5: Environment variables
1246
+ console.log(c.yellow('5. Environment Variables'));
1247
+ const envVars = {
1248
+ RELAY_API_URL: process.env.RELAY_API_URL,
1249
+ RELAY_API_KEY: process.env.RELAY_API_KEY ? '***' + process.env.RELAY_API_KEY.slice(-4) : undefined,
1250
+ EDITOR: process.env.EDITOR,
1251
+ HOME: process.env.HOME
1252
+ };
1253
+
1254
+ const envSet = Object.entries(envVars).filter(([_, v]) => v).length;
1255
+ console.log(c.cyan(` ${envSet} environment variables set\n`));
1256
+ if (envVars.EDITOR) {
1257
+ console.log(c.green(` ✅ Editor: ${envVars.EDITOR}\n`));
1258
+ } else {
1259
+ console.log(c.yellow(' ⚠️ EDITOR not set (needed for config edit)\n'));
1260
+ recommendations.push('Set EDITOR environment variable for config editing');
1261
+ }
1262
+ checksPassed++;
1263
+
1264
+ // Check 6: File permissions
1265
+ console.log(c.yellow('6. File Permissions'));
1266
+ try {
1267
+ const credsPath = path.join(HOME_DIR, '.teleportation', 'credentials');
1268
+ if (fs.existsSync(credsPath)) {
1269
+ const stats = fs.statSync(credsPath);
1270
+ const mode = stats.mode & parseInt('777', 8);
1271
+ if (mode === parseInt('600', 8)) {
1272
+ console.log(c.green(' ✅ Credentials file permissions correct (600)\n'));
1273
+ checksPassed++;
1274
+ } else {
1275
+ console.log(c.yellow(` ⚠️ Credentials file permissions: ${mode.toString(8)}\n`));
1276
+ issues.push('Credentials file permissions not secure');
1277
+ recommendations.push(`Run: chmod 600 ${credsPath}`);
1278
+ checksPassed++; // Not critical, just a warning
1279
+ }
1280
+ } else {
1281
+ console.log(c.yellow(' ⚠️ Credentials file does not exist\n'));
1282
+ checksPassed++; // Not an error if using env vars
1283
+ }
1284
+ } catch (e) {
1285
+ console.log(c.yellow(` ⚠️ Could not check permissions: ${e.message}\n`));
1286
+ checksPassed++;
1287
+ }
1288
+
1289
+ // Summary
1290
+ console.log(c.purple('\n📊 Diagnostic Summary\n'));
1291
+ console.log(` Checks passed: ${c.green(checksPassed)}`);
1292
+ console.log(` Checks failed: ${checksFailed > 0 ? c.red(checksFailed) : c.green(checksFailed)}`);
1293
+
1294
+ if (issues.length > 0) {
1295
+ console.log(c.red('\n⚠️ Issues Found:\n'));
1296
+ issues.forEach((issue, i) => {
1297
+ console.log(` ${i + 1}. ${issue}`);
1298
+ });
1299
+ }
1300
+
1301
+ if (recommendations.length > 0) {
1302
+ console.log(c.cyan('\n💡 Recommendations:\n'));
1303
+ recommendations.forEach((rec, i) => {
1304
+ console.log(` ${i + 1}. ${rec}`);
1305
+ });
1306
+ }
1307
+
1308
+ if (checksFailed === 0 && issues.length === 0) {
1309
+ console.log(c.green('\n🎉 All checks passed! System is healthy.\n'));
1310
+ } else {
1311
+ console.log(c.yellow('\n⚠️ Some issues detected. Review recommendations above.\n'));
1312
+ }
1313
+ }
1314
+
1315
+ async function commandUninstall() {
1316
+ console.log(c.purple('🗑️ Teleportation Uninstall\n'));
1317
+
1318
+ const readline = require('readline').createInterface({
1319
+ input: process.stdin,
1320
+ output: process.stdout
1321
+ });
1322
+
1323
+ return new Promise((resolve) => {
1324
+ readline.question('Are you sure you want to uninstall Teleportation? (y/N): ', async (answer) => {
1325
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
1326
+ console.log(c.yellow('Uninstall cancelled.\n'));
1327
+ readline.close();
1328
+ resolve();
1329
+ return;
1330
+ }
1331
+
1332
+ console.log(c.yellow('\nUninstalling Teleportation...\n'));
1333
+
1334
+ let removed = 0;
1335
+ let kept = 0;
1336
+
1337
+ // 1. Remove hooks
1338
+ console.log(c.yellow('1. Removing hooks...'));
1339
+ if (fs.existsSync(config.globalHooks)) {
1340
+ const hookFiles = fs.readdirSync(config.globalHooks).filter(f =>
1341
+ f.endsWith('.mjs') && ['pre_tool_use.mjs', 'permission_request.mjs', 'post_tool_use.mjs', 'session_start.mjs', 'session_end.mjs', 'stop.mjs', 'notification.mjs', 'config-loader.mjs'].includes(f)
1342
+ );
1343
+
1344
+ hookFiles.forEach(hook => {
1345
+ try {
1346
+ fs.unlinkSync(path.join(config.globalHooks, hook));
1347
+ console.log(c.green(` ✅ Removed ${hook}`));
1348
+ removed++;
1349
+ } catch (e) {
1350
+ console.log(c.red(` ❌ Failed to remove ${hook}: ${e.message}`));
1351
+ }
1352
+ });
1353
+ }
1354
+ console.log();
1355
+
1356
+ // 2. Remove settings.json
1357
+ console.log(c.yellow('2. Removing Claude Code settings...'));
1358
+ if (fs.existsSync(config.globalSettings)) {
1359
+ try {
1360
+ fs.unlinkSync(config.globalSettings);
1361
+ console.log(c.green(' ✅ Removed ~/.claude/settings.json'));
1362
+ removed++;
1363
+ } catch (e) {
1364
+ console.log(c.red(` ❌ Failed to remove settings: ${e.message}`));
1365
+ }
1366
+ } else {
1367
+ console.log(c.yellow(' ⚠️ Settings file not found'));
1368
+ }
1369
+ console.log();
1370
+
1371
+ // 3. Ask about credentials
1372
+ console.log(c.yellow('3. Credentials...'));
1373
+ const manager = await loadCredentialManager();
1374
+ if (manager && await manager.exists()) {
1375
+ readline.question(' Delete saved credentials? (y/N): ', async (answer) => {
1376
+ if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
1377
+ try {
1378
+ await manager.delete();
1379
+ console.log(c.green(' ✅ Credentials deleted'));
1380
+ removed++;
1381
+ } catch (e) {
1382
+ console.log(c.red(` ❌ Failed to delete credentials: ${e.message}`));
1383
+ }
1384
+ } else {
1385
+ console.log(c.yellow(' ⚠️ Credentials kept'));
1386
+ kept++;
1387
+ }
1388
+
1389
+ // 4. Ask about config
1390
+ console.log(c.yellow('\n4. Configuration...'));
1391
+ const configPath = path.join(TELEPORTATION_DIR, 'lib', 'config', 'manager.js');
1392
+ try {
1393
+ const { configExists, DEFAULT_CONFIG_PATH } = await import('file://' + configPath);
1394
+ if (await configExists()) {
1395
+ readline.question(' Delete config file? (y/N): ', async (answer) => {
1396
+ if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
1397
+ try {
1398
+ fs.unlinkSync(DEFAULT_CONFIG_PATH);
1399
+ console.log(c.green(' ✅ Config deleted'));
1400
+ removed++;
1401
+ } catch (e) {
1402
+ console.log(c.red(` ❌ Failed to delete config: ${e.message}`));
1403
+ }
1404
+ } else {
1405
+ console.log(c.yellow(' ⚠️ Config kept'));
1406
+ kept++;
1407
+ }
1408
+
1409
+ // 5. Remove CLI binary (if installed globally)
1410
+ console.log(c.yellow('\n5. CLI binary...'));
1411
+ const cliPaths = [
1412
+ '/usr/local/bin/teleportation',
1413
+ '/usr/bin/teleportation',
1414
+ path.join(HOME_DIR, '.local', 'bin', 'teleportation')
1415
+ ];
1416
+
1417
+ let cliRemoved = false;
1418
+ for (const cliPath of cliPaths) {
1419
+ if (fs.existsSync(cliPath)) {
1420
+ try {
1421
+ fs.unlinkSync(cliPath);
1422
+ console.log(c.green(` ✅ Removed ${cliPath}`));
1423
+ removed++;
1424
+ cliRemoved = true;
1425
+ break;
1426
+ } catch (e) {
1427
+ // Need sudo, inform user
1428
+ console.log(c.yellow(` ⚠️ ${cliPath} exists but requires sudo to remove`));
1429
+ console.log(c.cyan(` Run: sudo rm ${cliPath}`));
1430
+ }
1431
+ }
1432
+ }
1433
+
1434
+ if (!cliRemoved) {
1435
+ console.log(c.yellow(' ⚠️ CLI binary not found in standard locations'));
1436
+ console.log(c.cyan(' If installed elsewhere, remove manually'));
1437
+ }
1438
+
1439
+ // 6. Deregister from relay API (if credentials available)
1440
+ console.log(c.yellow('\n6. Relay API deregistration...'));
1441
+ const creds = await getCredentials();
1442
+ if (creds.RELAY_API_URL && creds.RELAY_API_KEY) {
1443
+ try {
1444
+ // Try to deregister (if endpoint exists)
1445
+ await fetch(`${creds.RELAY_API_URL}/api/sessions/deregister`, {
1446
+ method: 'POST',
1447
+ headers: {
1448
+ 'Authorization': `Bearer ${creds.RELAY_API_KEY}`,
1449
+ 'Content-Type': 'application/json'
1450
+ },
1451
+ body: JSON.stringify({ session_id: 'uninstall' })
1452
+ }).catch(() => {}); // Ignore errors
1453
+ console.log(c.green(' ✅ Attempted deregistration'));
1454
+ } catch (e) {
1455
+ console.log(c.yellow(' ⚠️ Could not deregister (non-critical)'));
1456
+ }
1457
+ } else {
1458
+ console.log(c.yellow(' ⚠️ No credentials available for deregistration'));
1459
+ }
1460
+
1461
+ // Summary
1462
+ console.log(c.purple('\n📊 Uninstall Summary\n'));
1463
+ console.log(` Removed: ${c.green(removed)} items`);
1464
+ if (kept > 0) {
1465
+ console.log(` Kept: ${c.yellow(kept)} items`);
1466
+ }
1467
+ console.log(c.green('\n✅ Uninstall complete!\n'));
1468
+ console.log(c.cyan('Note: Environment variables in ~/.zshrc were not removed.'));
1469
+ console.log(c.cyan(' Remove them manually if desired.\n'));
1470
+
1471
+ readline.close();
1472
+ resolve();
1473
+ });
1474
+ } else {
1475
+ console.log(c.yellow(' ⚠️ Config file does not exist'));
1476
+ readline.close();
1477
+ resolve();
1478
+ }
1479
+ } catch (e) {
1480
+ console.log(c.yellow(' ⚠️ Could not check config'));
1481
+ readline.close();
1482
+ resolve();
1483
+ }
1484
+ });
1485
+ } else {
1486
+ console.log(c.yellow(' ⚠️ No credentials found'));
1487
+ readline.close();
1488
+ resolve();
1489
+ }
1490
+ });
1491
+ });
1492
+ }
1493
+
1494
+ function commandInfo() {
1495
+ console.log(c.purple('Teleportation System Information\n'));
1496
+
1497
+ console.log(c.yellow('Project:'));
1498
+ console.log(' Location:', c.cyan(TELEPORTATION_DIR));
1499
+ console.log(' Home:', c.cyan(HOME_DIR));
1500
+
1501
+ console.log('\n' + c.yellow('Configuration:'));
1502
+ console.log(' Global hooks:', c.cyan(config.globalHooksDir));
1503
+ console.log(' Settings:', c.cyan(config.globalSettings));
1504
+ console.log(' Status:', config.isConfigured() ? c.green('CONFIGURED') : c.red('NOT CONFIGURED'));
1505
+
1506
+ console.log('\n' + c.yellow('Services:'));
1507
+ console.log(' Relay API:', checkService('relay', 3030) ? c.green('RUNNING') : c.red('STOPPED'));
1508
+ console.log(' Storage API:', checkService('storage', 3040) ? c.green('RUNNING') : c.red('STOPPED'));
1509
+
1510
+ console.log('\n' + c.yellow('Logs:'));
1511
+ console.log(' Relay:', c.cyan(path.join(HOME_DIR, 'Library/Logs/teleportation-relay.log')));
1512
+ console.log(' Storage:', c.cyan(path.join(HOME_DIR, 'Library/Logs/teleportation-storage.log')));
1513
+
1514
+ console.log();
1515
+ }
1516
+
1517
+ function commandLogs(args) {
1518
+ const service = args[0] || 'relay';
1519
+ const logFile = path.join(HOME_DIR, 'Library/Logs', `teleportation-${service}.log`);
1520
+
1521
+ if (fs.existsSync(logFile)) {
1522
+ console.log(c.yellow(`Showing logs for ${service}:\n`));
1523
+ try {
1524
+ const logs = execSync(`tail -20 ${logFile}`, { encoding: 'utf8' });
1525
+ console.log(logs);
1526
+ } catch (e) {
1527
+ console.log(c.red('Error reading logs'));
1528
+ }
1529
+ } else {
1530
+ console.log(c.red(`Log file not found: ${logFile}`));
1531
+ }
1532
+ }
1533
+
1534
+ async function commandLogin(args) {
1535
+ const { flags, positional } = parseFlags(args);
1536
+
1537
+ console.log(c.purple('Teleportation Login\n'));
1538
+
1539
+ // Load credential manager
1540
+ const manager = await loadCredentialManager();
1541
+ if (!manager) {
1542
+ console.log(c.red('❌ Failed to load credential manager'));
1543
+ process.exit(1);
1544
+ }
1545
+
1546
+ // Check for existing credentials
1547
+ const existing = await manager.load();
1548
+ if (existing) {
1549
+ console.log(c.yellow('⚠️ You are already logged in.'));
1550
+ console.log(c.cyan(' Run "teleportation logout" to clear existing credentials.\n'));
1551
+
1552
+ const readline = require('readline').createInterface({
1553
+ input: process.stdin,
1554
+ output: process.stdout
1555
+ });
1556
+
1557
+ return new Promise((resolve) => {
1558
+ readline.question('Do you want to overwrite existing credentials? (y/N): ', async (answer) => {
1559
+ readline.close();
1560
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
1561
+ console.log(c.yellow('Login cancelled.\n'));
1562
+ resolve();
1563
+ return;
1564
+ }
1565
+ await performLogin(manager, flags, positional);
1566
+ resolve();
1567
+ });
1568
+ });
1569
+ }
1570
+
1571
+ await performLogin(manager, flags, positional);
1572
+ }
1573
+
1574
+ async function performLogin(manager, flags, positional) {
1575
+ let apiKey = flags['api-key'] || flags.k;
1576
+ let token = flags.token || flags.t;
1577
+ const relayApiUrl = flags['relay-url'] || flags.r || process.env.RELAY_API_URL || 'https://api.teleportation.dev';
1578
+
1579
+ // Create backup before modifying credentials (only if credentials exist)
1580
+ const existingCreds = await manager.load().catch(() => null);
1581
+ if (existingCreds) {
1582
+ try {
1583
+ const backupManagerPath = path.join(TELEPORTATION_DIR, 'lib', 'backup', 'manager.js');
1584
+ const { BackupManager } = await import('file://' + backupManagerPath);
1585
+ const backupMgr = new BackupManager();
1586
+ await backupMgr.createBackup('teleportation login');
1587
+ console.log(c.dim('Backup created before credential update.\n'));
1588
+ } catch (backupErr) {
1589
+ // Non-fatal - warn but continue
1590
+ if (process.env.DEBUG) {
1591
+ console.log(c.yellow(`⚠️ Could not create backup: ${backupErr.message}`));
1592
+ }
1593
+ }
1594
+ }
1595
+
1596
+ // If API key provided via flag
1597
+ if (apiKey) {
1598
+ console.log(c.yellow('Authenticating with API key...\n'));
1599
+
1600
+ try {
1601
+ const apiKeyPath = path.join(TELEPORTATION_DIR, 'lib', 'auth', 'api-key.js');
1602
+ const { validateApiKey } = await import('file://' + apiKeyPath);
1603
+ const result = await validateApiKey(apiKey, relayApiUrl);
1604
+
1605
+ if (!result.valid) {
1606
+ console.log(c.red(`❌ ${result.error}\n`));
1607
+ process.exit(1);
1608
+ }
1609
+
1610
+ // Save credentials
1611
+ const credentials = {
1612
+ apiKey: apiKey,
1613
+ relayApiUrl: relayApiUrl,
1614
+ authenticatedAt: Date.now(),
1615
+ method: 'api-key'
1616
+ };
1617
+
1618
+ await manager.save(credentials);
1619
+
1620
+ // Also sync config to match credentials
1621
+ try {
1622
+ const configManagerPath = path.join(TELEPORTATION_DIR, 'lib', 'config', 'manager.js');
1623
+ const { setConfigValue } = await import('file://' + configManagerPath);
1624
+ await setConfigValue('relay.url', relayApiUrl);
1625
+ } catch (configErr) {
1626
+ // Non-fatal - just warn
1627
+ console.log(c.yellow(`⚠️ Could not sync config: ${configErr.message}`));
1628
+ }
1629
+
1630
+ console.log(c.green('✅ Successfully authenticated with API key!\n'));
1631
+ console.log(c.cyan('Credentials saved to ~/.teleportation/credentials\n'));
1632
+ console.log(c.yellow('⚠️ Restart Claude Code to apply changes to current session.\n'));
1633
+ return;
1634
+ } catch (error) {
1635
+ console.log(c.red(`❌ Error: ${error.message}\n`));
1636
+ process.exit(1);
1637
+ }
1638
+ }
1639
+
1640
+ // If token provided via flag
1641
+ if (token) {
1642
+ console.log(c.yellow('Authenticating with token...\n'));
1643
+
1644
+ try {
1645
+ // Save credentials with token
1646
+ const credentials = {
1647
+ accessToken: token,
1648
+ relayApiUrl: relayApiUrl,
1649
+ authenticatedAt: Date.now(),
1650
+ method: 'token'
1651
+ };
1652
+
1653
+ await manager.save(credentials);
1654
+
1655
+ // Also sync config to match credentials
1656
+ try {
1657
+ const configManagerPath = path.join(TELEPORTATION_DIR, 'lib', 'config', 'manager.js');
1658
+ const { setConfigValue } = await import('file://' + configManagerPath);
1659
+ await setConfigValue('relay.url', relayApiUrl);
1660
+ } catch (configErr) {
1661
+ // Non-fatal - just warn
1662
+ console.log(c.yellow(`⚠️ Could not sync config: ${configErr.message}`));
1663
+ }
1664
+
1665
+ console.log(c.green('✅ Successfully authenticated with token!\n'));
1666
+ console.log(c.cyan('Credentials saved to ~/.teleportation/credentials\n'));
1667
+ console.log(c.yellow('⚠️ Restart Claude Code to apply changes to current session.\n'));
1668
+ return;
1669
+ } catch (error) {
1670
+ console.log(c.red(`❌ Error: ${error.message}\n`));
1671
+ process.exit(1);
1672
+ }
1673
+ }
1674
+
1675
+ // Interactive mode - prompt for API key
1676
+ console.log(c.cyan('Interactive login mode\n'));
1677
+ console.log(c.yellow('Options:'));
1678
+ console.log(' 1. API Key authentication (recommended)');
1679
+ console.log(' 2. OAuth device code flow (coming soon)\n');
1680
+
1681
+ const readline = require('readline').createInterface({
1682
+ input: process.stdin,
1683
+ output: process.stdout
1684
+ });
1685
+
1686
+ return new Promise((resolve) => {
1687
+ readline.question('Enter your API key (or press Enter to skip): ', async (input) => {
1688
+ readline.close();
1689
+
1690
+ if (!input || input.trim() === '') {
1691
+ console.log(c.yellow('\n⚠️ No API key provided.'));
1692
+ console.log(c.cyan(' Use --api-key flag or --token flag for non-interactive login.\n'));
1693
+ console.log(c.cyan(' Example: teleportation login --api-key YOUR_KEY\n'));
1694
+ resolve();
1695
+ return;
1696
+ }
1697
+
1698
+ apiKey = input.trim();
1699
+ console.log(c.yellow('\nValidating API key...\n'));
1700
+
1701
+ try {
1702
+ const apiKeyPath = path.join(TELEPORTATION_DIR, 'lib', 'auth', 'api-key.js');
1703
+ const { validateApiKey } = await import('file://' + apiKeyPath);
1704
+ const result = await validateApiKey(apiKey, relayApiUrl);
1705
+
1706
+ if (!result.valid) {
1707
+ console.log(c.red(`❌ ${result.error}\n`));
1708
+ process.exit(1);
1709
+ }
1710
+
1711
+ // Save credentials
1712
+ const credentials = {
1713
+ apiKey: apiKey,
1714
+ relayApiUrl: relayApiUrl,
1715
+ authenticatedAt: Date.now(),
1716
+ method: 'api-key'
1717
+ };
1718
+
1719
+ await manager.save(credentials);
1720
+ console.log(c.green('✅ Successfully authenticated!\n'));
1721
+ console.log(c.cyan('Credentials saved to ~/.teleportation/credentials\n'));
1722
+ resolve();
1723
+ } catch (error) {
1724
+ console.log(c.red(`❌ Error: ${error.message}\n`));
1725
+ process.exit(1);
1726
+ }
1727
+ });
1728
+ });
1729
+ }
1730
+
1731
+ async function commandConfig(args) {
1732
+ const subcommand = args[0] || 'list';
1733
+
1734
+ try {
1735
+ const configPath = path.join(TELEPORTATION_DIR, 'lib', 'config', 'manager.js');
1736
+ const { loadConfig, getConfigValue, setConfigValue, configExists, DEFAULT_CONFIG_PATH } = await import('file://' + configPath);
1737
+
1738
+ if (subcommand === 'list') {
1739
+ console.log(c.purple('Teleportation Configuration\n'));
1740
+
1741
+ const config = await loadConfig();
1742
+ const exists = await configExists();
1743
+
1744
+ if (!exists) {
1745
+ console.log(c.yellow('⚠️ Config file does not exist. Using defaults.\n'));
1746
+ }
1747
+
1748
+ console.log(c.cyan('Relay Settings:'));
1749
+ console.log(` URL: ${c.green(config.relay?.url || 'not set')}`);
1750
+ console.log(` Timeout: ${c.green((config.relay?.timeout || 0) + 'ms')}`);
1751
+
1752
+ console.log(c.cyan('\nHook Settings:'));
1753
+ console.log(` Auto-update: ${config.hooks?.autoUpdate ? c.green('enabled') : c.yellow('disabled')}`);
1754
+ console.log(` Update check interval: ${c.green((config.hooks?.updateCheckInterval || 0) / 1000 / 60 + ' minutes')}`);
1755
+
1756
+ console.log(c.cyan('\nSession Settings:'));
1757
+ console.log(` Timeout: ${c.green((config.session?.timeout || 0) / 1000 / 60 + ' minutes')}`);
1758
+ console.log(` Mute timeout: ${c.green((config.session?.muteTimeout || 0) / 1000 / 60 + ' minutes')}`);
1759
+
1760
+ console.log(c.cyan('\nNotification Settings:'));
1761
+ console.log(` Enabled: ${config.notifications?.enabled ? c.green('yes') : c.yellow('no')}`);
1762
+ console.log(` Sound: ${config.notifications?.sound ? c.green('enabled') : c.yellow('disabled')}`);
1763
+
1764
+ console.log(c.cyan(`\nConfig file: ${DEFAULT_CONFIG_PATH}\n`));
1765
+
1766
+ } else if (subcommand === 'get') {
1767
+ const key = args[1];
1768
+ if (!key) {
1769
+ console.log(c.red('❌ Error: Please specify a config key\n'));
1770
+ console.log(c.cyan('Example: teleportation config get relay.url\n'));
1771
+ return;
1772
+ }
1773
+
1774
+ const value = await getConfigValue(key);
1775
+ if (value === null) {
1776
+ console.log(c.yellow(`⚠️ Config key "${key}" not found\n`));
1777
+ } else {
1778
+ console.log(c.green(JSON.stringify(value, null, 2) + '\n'));
1779
+ }
1780
+
1781
+ } else if (subcommand === 'set') {
1782
+ const key = args[1];
1783
+ const valueStr = args[2];
1784
+
1785
+ if (!key || valueStr === undefined) {
1786
+ console.log(c.red('❌ Error: Please specify key and value\n'));
1787
+ console.log(c.cyan('Example: teleportation config set relay.url http://example.com:3030\n'));
1788
+ return;
1789
+ }
1790
+
1791
+ // Try to parse value as JSON, number, or boolean
1792
+ let value = valueStr;
1793
+ try {
1794
+ value = JSON.parse(valueStr);
1795
+ } catch {
1796
+ // Not JSON, try boolean or number
1797
+ if (valueStr === 'true') value = true;
1798
+ else if (valueStr === 'false') value = false;
1799
+ else if (/^\d+$/.test(valueStr)) value = parseInt(valueStr, 10);
1800
+ else if (/^\d+\.\d+$/.test(valueStr)) value = parseFloat(valueStr);
1801
+ }
1802
+
1803
+ await setConfigValue(key, value);
1804
+ console.log(c.green(`✅ Set ${key} = ${JSON.stringify(value)}\n`));
1805
+
1806
+ } else if (subcommand === 'edit') {
1807
+ const editor = process.env.EDITOR || 'vi';
1808
+ const exists = await configExists();
1809
+
1810
+ if (!exists) {
1811
+ // Create default config first
1812
+ const { saveConfig, loadConfig } = await import('file://' + configPath);
1813
+ const config = await loadConfig();
1814
+ await saveConfig(config);
1815
+ console.log(c.yellow('Created default config file.\n'));
1816
+ }
1817
+
1818
+ console.log(c.cyan(`Opening config in ${editor}...\n`));
1819
+ try {
1820
+ execSync(`${editor} ${DEFAULT_CONFIG_PATH}`, { stdio: 'inherit' });
1821
+ console.log(c.green('✅ Config file saved\n'));
1822
+ } catch (e) {
1823
+ console.log(c.red(`❌ Error opening editor: ${e.message}\n`));
1824
+ }
1825
+
1826
+ } else {
1827
+ console.log(c.red(`❌ Unknown config subcommand: ${subcommand}\n`));
1828
+ console.log(c.yellow('Available subcommands:'));
1829
+ console.log(' list - Show all settings');
1830
+ console.log(' get - Get specific setting');
1831
+ console.log(' set - Update setting');
1832
+ console.log(' edit - Open config in editor\n');
1833
+ }
1834
+ } catch (error) {
1835
+ console.log(c.red(`❌ Error: ${error.message}\n`));
1836
+ throw error;
1837
+ }
1838
+ }
1839
+
1840
+ async function commandLogout() {
1841
+ console.log(c.purple('Teleportation Logout\n'));
1842
+
1843
+ const manager = await loadCredentialManager();
1844
+ if (!manager) {
1845
+ console.log(c.red('❌ Failed to load credential manager'));
1846
+ process.exit(1);
1847
+ }
1848
+
1849
+ const exists = await manager.exists();
1850
+ if (!exists) {
1851
+ console.log(c.yellow('⚠️ No credentials found. You are not logged in.\n'));
1852
+ return;
1853
+ }
1854
+
1855
+ const readline = require('readline').createInterface({
1856
+ input: process.stdin,
1857
+ output: process.stdout
1858
+ });
1859
+
1860
+ return new Promise((resolve) => {
1861
+ readline.question('Are you sure you want to log out? (y/N): ', async (answer) => {
1862
+ readline.close();
1863
+
1864
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
1865
+ console.log(c.yellow('Logout cancelled.\n'));
1866
+ resolve();
1867
+ return;
1868
+ }
1869
+
1870
+ try {
1871
+ await manager.delete();
1872
+ console.log(c.green('✅ Successfully logged out!\n'));
1873
+ console.log(c.cyan('Credentials cleared from ~/.teleportation/credentials\n'));
1874
+ resolve();
1875
+ } catch (error) {
1876
+ console.log(c.red(`❌ Error: ${error.message}\n`));
1877
+ process.exit(1);
1878
+ }
1879
+ });
1880
+ });
1881
+ }
1882
+
1883
+ // Worktree/Snapshot/Session command handlers
1884
+ async function commandWorktree(args) {
1885
+ const cliPath = path.join(TELEPORTATION_DIR, 'lib', 'cli', 'index.js');
1886
+ const { parseArgs, routeCommand, printHelp } = await import('file://' + cliPath);
1887
+
1888
+ if (args.length === 0 || args[0] === 'help' || args[0] === '--help') {
1889
+ printHelp();
1890
+ return;
1891
+ }
1892
+
1893
+ const parsed = parseArgs(['worktree', ...args]);
1894
+ await routeCommand(parsed);
1895
+ }
1896
+
1897
+ async function commandSnapshot(args) {
1898
+ const cliPath = path.join(TELEPORTATION_DIR, 'lib', 'cli', 'index.js');
1899
+ const { parseArgs, routeCommand, printHelp } = await import('file://' + cliPath);
1900
+
1901
+ if (args.length === 0 || args[0] === 'help' || args[0] === '--help') {
1902
+ printHelp();
1903
+ return;
1904
+ }
1905
+
1906
+ const parsed = parseArgs(['snapshot', ...args]);
1907
+ await routeCommand(parsed);
1908
+ }
1909
+
1910
+ async function commandSession(args) {
1911
+ const cliPath = path.join(TELEPORTATION_DIR, 'lib', 'cli', 'index.js');
1912
+ const { parseArgs, routeCommand, printHelp } = await import('file://' + cliPath);
1913
+
1914
+ if (args.length === 0 || args[0] === 'help' || args[0] === '--help') {
1915
+ printHelp();
1916
+ return;
1917
+ }
1918
+
1919
+ const parsed = parseArgs(['session', ...args]);
1920
+ await routeCommand(parsed);
1921
+ }
1922
+
1923
+ async function commandDaemon(args) {
1924
+ const subCommand = args[0] || 'status';
1925
+
1926
+ // Get daemon port from env with default (consistent with daemon itself)
1927
+ const DAEMON_PORT = process.env.TELEPORTATION_DAEMON_PORT || '3050';
1928
+
1929
+ try {
1930
+ // Dynamically import lifecycle module
1931
+ const lifecyclePath = path.join(TELEPORTATION_DIR, 'lib', 'daemon', 'lifecycle.js');
1932
+ const { startDaemon, stopDaemon, restartDaemon, getDaemonStatus } = await import('file://' + lifecyclePath);
1933
+
1934
+ switch (subCommand) {
1935
+ case 'start':
1936
+ console.log(c.yellow('Starting Teleportation Daemon...\n'));
1937
+ try {
1938
+ const result = await startDaemon();
1939
+ console.log(c.green(`✅ Daemon started successfully (PID: ${result.pid})\n`));
1940
+ console.log(c.cyan(`Daemon is running at http://127.0.0.1:${DAEMON_PORT}\n`));
1941
+ } catch (error) {
1942
+ console.log(c.red(`❌ Failed to start daemon: ${error.message}\n`));
1943
+ process.exit(1);
1944
+ }
1945
+ break;
1946
+
1947
+ case 'stop':
1948
+ console.log(c.yellow('Stopping Teleportation Daemon...\n'));
1949
+ try {
1950
+ const result = await stopDaemon();
1951
+ if (result.success) {
1952
+ console.log(c.green(`✅ Daemon stopped successfully${result.forced ? ' (forced)' : ''}\n`));
1953
+ } else {
1954
+ console.log(c.red('❌ Failed to stop daemon\n'));
1955
+ process.exit(1);
1956
+ }
1957
+ } catch (error) {
1958
+ console.log(c.red(`❌ Error: ${error.message}\n`));
1959
+ process.exit(1);
1960
+ }
1961
+ break;
1962
+
1963
+ case 'restart':
1964
+ console.log(c.yellow('Restarting Teleportation Daemon...\n'));
1965
+ try {
1966
+ const result = await restartDaemon();
1967
+ console.log(c.green(`✅ Daemon restarted successfully (PID: ${result.pid})\n`));
1968
+ console.log(c.cyan(`Previous daemon ${result.wasRunning ? 'was running' : 'was not running'}\n`));
1969
+ } catch (error) {
1970
+ console.log(c.red(`❌ Failed to restart daemon: ${error.message}\n`));
1971
+ process.exit(1);
1972
+ }
1973
+ break;
1974
+
1975
+ case 'status':
1976
+ const status = await getDaemonStatus();
1977
+ console.log(c.purple('Teleportation Daemon Status\n'));
1978
+ if (status.running) {
1979
+ console.log(c.green(`✅ Running (PID: ${status.pid})`));
1980
+ console.log(c.cyan(` HTTP server: http://127.0.0.1:${DAEMON_PORT}`));
1981
+ if (status.uptime) {
1982
+ console.log(c.cyan(` Uptime: ${Math.round(status.uptime / 60000)}m`));
1983
+ }
1984
+ } else {
1985
+ console.log(c.red('❌ Not running'));
1986
+ }
1987
+ console.log('');
1988
+ break;
1989
+
1990
+ case 'health':
1991
+ console.log(c.yellow('Checking daemon health...\n'));
1992
+ try {
1993
+ const response = await fetch(`http://127.0.0.1:${DAEMON_PORT}/health`);
1994
+ if (response.ok) {
1995
+ const data = await response.json();
1996
+ console.log(c.green('✅ Daemon is healthy\n'));
1997
+ console.log(c.cyan('Health Report:'));
1998
+ console.log(` Status: ${c.green(data.status)}`);
1999
+ console.log(` Uptime: ${c.cyan(Math.round(data.uptime) + 's')}`);
2000
+ console.log(` Sessions: ${c.cyan(data.sessions)}`);
2001
+ console.log(` Queue: ${c.cyan(data.queue)}`);
2002
+ console.log(` Executions: ${c.cyan(data.executions)}\n`);
2003
+ } else {
2004
+ console.log(c.red('❌ Daemon is unhealthy\n'));
2005
+ process.exit(1);
2006
+ }
2007
+ } catch (error) {
2008
+ console.log(c.red(`❌ Cannot reach daemon: ${error.message}\n`));
2009
+ process.exit(1);
2010
+ }
2011
+ break;
2012
+
2013
+ default:
2014
+ console.log(c.red(`Unknown daemon command: ${subCommand}\n`));
2015
+ console.log(c.cyan('Available commands:'));
2016
+ console.log(' teleportation daemon start - Start the daemon');
2017
+ console.log(' teleportation daemon stop - Stop the daemon');
2018
+ console.log(' teleportation daemon restart - Restart the daemon');
2019
+ console.log(' teleportation daemon status - Show daemon status');
2020
+ console.log(' teleportation daemon health - Check daemon health\n');
2021
+ process.exit(1);
2022
+ }
2023
+ } catch (error) {
2024
+ console.log(c.red(`❌ Daemon command failed: ${error.message}\n`));
2025
+ process.exit(1);
2026
+ }
2027
+ }
2028
+
2029
+ // Away Mode Commands (Task 9.0)
2030
+ async function commandAwayMode() {
2031
+ console.log(c.yellow('🚀 Marking session as away and starting daemon...\n'));
2032
+
2033
+ const sessionId = process.env.TELEPORTATION_SESSION_ID;
2034
+ if (!sessionId) {
2035
+ console.log(c.red('❌ Error: TELEPORTATION_SESSION_ID not set\n'));
2036
+ console.log(c.cyan('Set the environment variable: export TELEPORTATION_SESSION_ID=<session-id>\n'));
2037
+ process.exit(1);
2038
+ }
2039
+
2040
+ const relayUrl = process.env.RELAY_API_URL || '';
2041
+ const relayKey = process.env.RELAY_API_KEY || '';
2042
+
2043
+ if (!relayUrl || !relayKey) {
2044
+ console.log(c.red('❌ Error: RELAY_API_URL or RELAY_API_KEY not set\n'));
2045
+ process.exit(1);
2046
+ }
2047
+
2048
+ try {
2049
+ // Update session daemon state
2050
+ const res = await fetch(`${relayUrl}/api/sessions/${encodeURIComponent(sessionId)}/daemon-state`, {
2051
+ method: 'PATCH',
2052
+ headers: {
2053
+ 'Content-Type': 'application/json',
2054
+ 'Authorization': `Bearer ${relayKey}`,
2055
+ },
2056
+ body: JSON.stringify({
2057
+ is_away: true,
2058
+ status: 'running',
2059
+ started_reason: 'cli_away',
2060
+ }),
2061
+ });
2062
+
2063
+ if (!res.ok) {
2064
+ console.log(c.yellow('⚠️ Warning: Could not update session state via Relay API\n'));
2065
+ } else {
2066
+ console.log(c.green('✅ Session marked as away in Relay API\n'));
2067
+ }
2068
+
2069
+ console.log(c.green('✅ Session marked as away. Daemon is ready.\n'));
2070
+ console.log(c.cyan('When you return, run: teleportation back\n'));
2071
+ } catch (error) {
2072
+ console.log(c.red('❌ Error: ' + error.message + '\n'));
2073
+ process.exit(1);
2074
+ }
2075
+ }
2076
+
2077
+ async function commandBackMode() {
2078
+ console.log(c.yellow('🔙 Marking session as back...\n'));
2079
+
2080
+ const sessionId = process.env.TELEPORTATION_SESSION_ID;
2081
+ if (!sessionId) {
2082
+ console.log(c.red('❌ Error: TELEPORTATION_SESSION_ID not set\n'));
2083
+ console.log(c.cyan('Set the environment variable: export TELEPORTATION_SESSION_ID=<session-id>\n'));
2084
+ process.exit(1);
2085
+ }
2086
+
2087
+ const relayUrl = process.env.RELAY_API_URL || '';
2088
+ const relayKey = process.env.RELAY_API_KEY || '';
2089
+
2090
+ if (!relayUrl || !relayKey) {
2091
+ console.log(c.red('❌ Error: RELAY_API_URL or RELAY_API_KEY not set\n'));
2092
+ process.exit(1);
2093
+ }
2094
+
2095
+ try {
2096
+ // Update session daemon state
2097
+ const res = await fetch(`${relayUrl}/api/sessions/${encodeURIComponent(sessionId)}/daemon-state`, {
2098
+ method: 'PATCH',
2099
+ headers: {
2100
+ 'Content-Type': 'application/json',
2101
+ 'Authorization': `Bearer ${relayKey}`,
2102
+ },
2103
+ body: JSON.stringify({
2104
+ is_away: false,
2105
+ status: 'stopped',
2106
+ started_reason: null,
2107
+ }),
2108
+ });
2109
+
2110
+ if (!res.ok) {
2111
+ console.log(c.yellow('⚠️ Warning: Could not update session state via Relay API\n'));
2112
+ } else {
2113
+ console.log(c.green('✅ Session marked as back in Relay API\n'));
2114
+ }
2115
+
2116
+ console.log(c.green('✅ Session marked as back.\n'));
2117
+ } catch (error) {
2118
+ console.log(c.red('❌ Error: ' + error.message + '\n'));
2119
+ process.exit(1);
2120
+ }
2121
+ }
2122
+
2123
+ async function commandDaemonStatusDisplay() {
2124
+ console.log(c.cyan('\n📊 Daemon Status\n'));
2125
+
2126
+ const sessionId = process.env.TELEPORTATION_SESSION_ID;
2127
+ const relayUrl = process.env.RELAY_API_URL || '';
2128
+ const relayKey = process.env.RELAY_API_KEY || '';
2129
+
2130
+ if (sessionId && relayUrl && relayKey) {
2131
+ try {
2132
+ const res = await fetch(`${relayUrl}/api/sessions/${encodeURIComponent(sessionId)}`, {
2133
+ headers: { 'Authorization': `Bearer ${relayKey}` },
2134
+ });
2135
+
2136
+ if (res.ok) {
2137
+ const session = await res.json();
2138
+ const daemonState = session.daemon_state;
2139
+
2140
+ if (daemonState) {
2141
+ console.log(c.yellow('Session State:'));
2142
+ console.log(` Status: ${daemonState.status === 'running' ? c.green('Running') : c.red('Stopped')}`);
2143
+ console.log(` Away: ${daemonState.is_away ? c.yellow('Yes') : c.green('No')}`);
2144
+
2145
+ if (daemonState.started_at) {
2146
+ const startedDate = new Date(daemonState.started_at);
2147
+ console.log(` Started: ${startedDate.toLocaleString()}`);
2148
+ }
2149
+
2150
+ if (daemonState.started_reason) {
2151
+ console.log(` Started Reason: ${daemonState.started_reason}`);
2152
+ }
2153
+
2154
+ if (daemonState.last_approval_location) {
2155
+ console.log(` Last Approval: ${daemonState.last_approval_location}`);
2156
+ }
2157
+
2158
+ if (daemonState.stopped_reason) {
2159
+ console.log(` Stopped Reason: ${daemonState.stopped_reason}`);
2160
+ }
2161
+ }
2162
+ }
2163
+ } catch (err) {
2164
+ console.log(c.yellow('⚠️ Could not fetch session state from Relay API\n'));
2165
+ }
2166
+ } else {
2167
+ console.log(c.yellow('⚠️ Session ID or Relay API not configured\n'));
2168
+ }
2169
+
2170
+ console.log();
2171
+ }
2172
+
2173
+ async function commandInbox() {
2174
+ const sessionId = process.env.TELEPORTATION_SESSION_ID;
2175
+ if (!sessionId) {
2176
+ console.log(c.red('❌ Error: TELEPORTATION_SESSION_ID not set\n'));
2177
+ console.log(c.cyan('Set the environment variable: export TELEPORTATION_SESSION_ID=<session-id>\n'));
2178
+ process.exit(1);
2179
+ }
2180
+
2181
+ const creds = await getCredentials();
2182
+ const relayUrl = creds.RELAY_API_URL;
2183
+ const relayKey = creds.RELAY_API_KEY;
2184
+
2185
+ if (!relayUrl || !relayKey) {
2186
+ console.log(c.red('❌ Error: RELAY_API_URL or RELAY_API_KEY not configured\n'));
2187
+ process.exit(1);
2188
+ }
2189
+
2190
+ try {
2191
+ const url = `${relayUrl}/api/messages/pending?session_id=${encodeURIComponent(sessionId)}&agent_id=main`;
2192
+ const res = await fetch(url, {
2193
+ headers: {
2194
+ 'Authorization': `Bearer ${relayKey}`,
2195
+ },
2196
+ });
2197
+
2198
+ if (!res.ok) {
2199
+ console.log(c.red(`❌ Error: Inbox request failed with status ${res.status}\n`));
2200
+ process.exit(1);
2201
+ }
2202
+
2203
+ const data = await res.json();
2204
+ const keys = data && typeof data === 'object' ? Object.keys(data) : [];
2205
+ if (!keys.length) {
2206
+ console.log(c.cyan('📭 No pending inbox messages for this session\n'));
2207
+ return;
2208
+ }
2209
+
2210
+ console.log(c.cyan('📨 Next inbox message:\n'));
2211
+ console.log(' ID: ' + c.green(data.id));
2212
+ console.log(' Text: ' + data.text + '\n');
2213
+ console.log(c.cyan('Use `teleportation inbox-ack ' + data.id + '` to acknowledge this message.\n'));
2214
+ } catch (error) {
2215
+ console.log(c.red('❌ Error: ' + error.message + '\n'));
2216
+ process.exit(1);
2217
+ }
2218
+ }
2219
+
2220
+ async function commandInboxAck(id) {
2221
+ if (!id) {
2222
+ console.log(c.red('❌ Error: Message id is required\n'));
2223
+ console.log(c.cyan('Usage: teleportation inbox-ack <id>\n'));
2224
+ process.exit(1);
2225
+ }
2226
+
2227
+ const creds = await getCredentials();
2228
+ const relayUrl = creds.RELAY_API_URL;
2229
+ const relayKey = creds.RELAY_API_KEY;
2230
+
2231
+ if (!relayUrl || !relayKey) {
2232
+ console.log(c.red('❌ Error: RELAY_API_URL or RELAY_API_KEY not configured\n'));
2233
+ process.exit(1);
2234
+ }
2235
+
2236
+ try {
2237
+ const url = `${relayUrl}/api/messages/${encodeURIComponent(id)}/ack`;
2238
+ const res = await fetch(url, {
2239
+ method: 'POST',
2240
+ headers: {
2241
+ 'Authorization': `Bearer ${relayKey}`,
2242
+ },
2243
+ });
2244
+
2245
+ if (!res.ok) {
2246
+ console.log(c.red(`❌ Error: Acknowledge failed with status ${res.status}\n`));
2247
+ process.exit(1);
2248
+ }
2249
+
2250
+ console.log(c.green('✅ Message acknowledged\n'));
2251
+ } catch (error) {
2252
+ console.log(c.red('❌ Error: ' + error.message + '\n'));
2253
+ process.exit(1);
2254
+ }
2255
+ }
2256
+
2257
+ /**
2258
+ * Install hooks globally to ~/.claude/hooks/
2259
+ * This command copies hooks from the teleportation project to the global location
2260
+ * and merges settings into ~/.claude/settings.json
2261
+ */
2262
+ async function commandInstallHooks() {
2263
+ console.log(c.purple('🔧 Installing Teleportation Hooks Globally\n'));
2264
+
2265
+ const globalHooksDir = path.join(HOME_DIR, '.claude', 'hooks');
2266
+ const globalSettings = path.join(HOME_DIR, '.claude', 'settings.json');
2267
+ const sourceHooksDir = path.join(TELEPORTATION_DIR, '.claude', 'hooks');
2268
+
2269
+ // List of hooks to install
2270
+ const hooks = [
2271
+ 'pre_tool_use.mjs',
2272
+ 'post_tool_use.mjs',
2273
+ 'permission_request.mjs',
2274
+ 'stop.mjs',
2275
+ 'session_start.mjs',
2276
+ 'session_end.mjs',
2277
+ 'notification.mjs',
2278
+ 'user_prompt_submit.mjs',
2279
+ 'config-loader.mjs',
2280
+ 'session-register.mjs',
2281
+ 'heartbeat.mjs' // Spawned by session-register.mjs, needs to be in hooks directory
2282
+ ];
2283
+
2284
+ // Step 1: Ensure directories exist
2285
+ console.log(c.yellow('Step 1: Creating directories...\n'));
2286
+ try {
2287
+ if (!fs.existsSync(path.join(HOME_DIR, '.claude'))) {
2288
+ fs.mkdirSync(path.join(HOME_DIR, '.claude'), { recursive: true });
2289
+ }
2290
+ if (!fs.existsSync(globalHooksDir)) {
2291
+ fs.mkdirSync(globalHooksDir, { recursive: true });
2292
+ }
2293
+ console.log(c.green(' ✅ ~/.claude/hooks/ directory ready\n'));
2294
+ } catch (e) {
2295
+ console.log(c.red(` ❌ Failed to create directories: ${e.message}\n`));
2296
+ process.exit(1);
2297
+ }
2298
+
2299
+ // Step 2: Copy hook files
2300
+ console.log(c.yellow('Step 2: Copying hooks...\n'));
2301
+ let installed = 0;
2302
+ let failed = 0;
2303
+
2304
+ for (const hook of hooks) {
2305
+ const src = path.join(sourceHooksDir, hook);
2306
+ const dest = path.join(globalHooksDir, hook);
2307
+
2308
+ try {
2309
+ if (!fs.existsSync(src)) {
2310
+ console.log(c.yellow(` ⚠️ ${hook} not found in source, skipping`));
2311
+ continue;
2312
+ }
2313
+
2314
+ fs.copyFileSync(src, dest);
2315
+ fs.chmodSync(dest, 0o755); // Make executable
2316
+ console.log(c.green(` ✅ ${hook}`));
2317
+ installed++;
2318
+ } catch (e) {
2319
+ console.log(c.red(` ❌ ${hook}: ${e.message}`));
2320
+ failed++;
2321
+ }
2322
+ }
2323
+
2324
+ console.log(`\n Installed: ${c.green(installed)}, Failed: ${failed > 0 ? c.red(failed) : '0'}\n`);
2325
+
2326
+ // Step 2.5: Install lib files that hooks depend on (metadata.js, etc.)
2327
+ console.log(c.yellow('Step 2.5: Installing lib files...\n'));
2328
+ try {
2329
+ const installerPath = path.join(TELEPORTATION_DIR, 'lib', 'install', 'installer.js');
2330
+ if (fs.existsSync(installerPath)) {
2331
+ const installer = await import('file://' + installerPath);
2332
+ const libResult = await installer.installLibFiles();
2333
+ if (libResult.installed.length > 0) {
2334
+ libResult.installed.forEach(file => {
2335
+ console.log(c.green(` ✅ ${file}`));
2336
+ });
2337
+ }
2338
+ if (libResult.failed.length > 0) {
2339
+ libResult.failed.forEach(({ file, error }) => {
2340
+ console.log(c.yellow(` ⚠️ ${file}: ${error}`));
2341
+ });
2342
+ }
2343
+ console.log(`\n Lib files installed: ${c.green(libResult.installed.length)}, Failed: ${libResult.failed.length > 0 ? c.yellow(libResult.failed.length) : '0'}\n`);
2344
+ } else {
2345
+ console.log(c.yellow(' ⚠️ Installer module not found, skipping lib files\n'));
2346
+ }
2347
+ } catch (e) {
2348
+ console.log(c.yellow(` ⚠️ Failed to install lib files: ${e.message}\n`));
2349
+ }
2350
+
2351
+ // Step 3: Update settings.json
2352
+ console.log(c.yellow('Step 3: Updating Claude Code settings...\n'));
2353
+
2354
+ // Safely quote paths - JSON.stringify escapes special chars to prevent command injection
2355
+ const quotePath = (p) => JSON.stringify(p);
2356
+
2357
+ const hooksConfig = {
2358
+ PreToolUse: [{
2359
+ matcher: ".*",
2360
+ hooks: [{
2361
+ type: "command",
2362
+ command: `node ${quotePath(path.join(globalHooksDir, 'pre_tool_use.mjs'))}`
2363
+ }]
2364
+ }],
2365
+ PostToolUse: [{
2366
+ matcher: ".*",
2367
+ hooks: [{
2368
+ type: "command",
2369
+ command: `node ${quotePath(path.join(globalHooksDir, 'post_tool_use.mjs'))}`
2370
+ }]
2371
+ }],
2372
+ PermissionRequest: [{
2373
+ matcher: ".*",
2374
+ hooks: [{
2375
+ type: "command",
2376
+ command: `node ${quotePath(path.join(globalHooksDir, 'permission_request.mjs'))}`
2377
+ }]
2378
+ }],
2379
+ Stop: [{
2380
+ matcher: ".*",
2381
+ hooks: [{
2382
+ type: "command",
2383
+ command: `node ${quotePath(path.join(globalHooksDir, 'stop.mjs'))}`
2384
+ }]
2385
+ }],
2386
+ SessionStart: [{
2387
+ matcher: ".*",
2388
+ hooks: [{
2389
+ type: "command",
2390
+ command: `node ${quotePath(path.join(globalHooksDir, 'session_start.mjs'))}`
2391
+ }]
2392
+ }],
2393
+ SessionEnd: [{
2394
+ matcher: ".*",
2395
+ hooks: [{
2396
+ type: "command",
2397
+ command: `node ${quotePath(path.join(globalHooksDir, 'session_end.mjs'))}`
2398
+ }]
2399
+ }],
2400
+ Notification: [{
2401
+ matcher: ".*",
2402
+ hooks: [{
2403
+ type: "command",
2404
+ command: `node ${quotePath(path.join(globalHooksDir, 'notification.mjs'))}`
2405
+ }]
2406
+ }],
2407
+ UserPromptSubmit: [{
2408
+ matcher: ".*",
2409
+ hooks: [{
2410
+ type: "command",
2411
+ command: `node ${quotePath(path.join(globalHooksDir, 'user_prompt_submit.mjs'))}`
2412
+ }]
2413
+ }]
2414
+ };
2415
+
2416
+ try {
2417
+ let existingSettings = {};
2418
+
2419
+ // Load existing settings if present
2420
+ if (fs.existsSync(globalSettings)) {
2421
+ try {
2422
+ const content = fs.readFileSync(globalSettings, 'utf8');
2423
+ existingSettings = JSON.parse(content);
2424
+ console.log(c.cyan(' Found existing settings, merging...\n'));
2425
+ } catch (e) {
2426
+ console.log(c.yellow(` ⚠️ Could not parse existing settings, creating new...\n`));
2427
+ }
2428
+ }
2429
+
2430
+ // Merge hooks intelligently - preserve user hooks, avoid duplicates
2431
+ const mergeHookArrays = (existing, incoming) => {
2432
+ if (!existing || !Array.isArray(existing)) return incoming;
2433
+ if (!incoming || !Array.isArray(incoming)) return existing;
2434
+
2435
+ // Get commands from incoming hooks to check for duplicates
2436
+ const incomingCommands = new Set(
2437
+ incoming.flatMap(h => (h.hooks || []).map(hh => hh.command))
2438
+ );
2439
+
2440
+ // Filter out existing hooks that have the same command (will be replaced)
2441
+ const filteredExisting = existing.filter(h =>
2442
+ !(h.hooks || []).some(hh => incomingCommands.has(hh.command))
2443
+ );
2444
+
2445
+ // Combine: existing (non-duplicate) + incoming (teleportation hooks)
2446
+ return [...filteredExisting, ...incoming];
2447
+ };
2448
+
2449
+ // Merge all hook types with warnings about user hooks
2450
+ const mergedHooks = { ...(existingSettings.hooks || {}) };
2451
+ let hasUserHooks = false;
2452
+
2453
+ for (const [hookType, hookConfig] of Object.entries(hooksConfig)) {
2454
+ const existingHooksForType = existingSettings.hooks?.[hookType] || [];
2455
+
2456
+ // Find user-defined hooks (not from teleportation)
2457
+ const userHooks = existingHooksForType.filter(h => {
2458
+ const commands = (h.hooks || []).map(hh => hh.command || '');
2459
+ return !commands.some(cmd =>
2460
+ cmd.includes('teleportation') ||
2461
+ cmd.includes('.claude/hooks') ||
2462
+ cmd.includes('~/.claude/hooks')
2463
+ );
2464
+ });
2465
+
2466
+ if (userHooks.length > 0) {
2467
+ hasUserHooks = true;
2468
+ console.log(c.yellow(` ⚠️ Preserving ${userHooks.length} custom ${hookType} hook(s):`));
2469
+ userHooks.forEach(h => {
2470
+ const cmds = (h.hooks || []).map(hh => hh.command || 'unknown');
2471
+ cmds.forEach(cmd => console.log(c.dim(` • ${cmd}`)));
2472
+ });
2473
+ console.log('');
2474
+ }
2475
+
2476
+ mergedHooks[hookType] = mergeHookArrays(existingHooksForType, hookConfig);
2477
+ }
2478
+
2479
+ if (hasUserHooks) {
2480
+ console.log(c.cyan(' Your custom hooks will continue to work alongside Teleportation hooks.\n'));
2481
+ }
2482
+
2483
+ const mergedSettings = {
2484
+ ...existingSettings,
2485
+ hooks: mergedHooks
2486
+ };
2487
+
2488
+ // Write settings
2489
+ fs.writeFileSync(globalSettings, JSON.stringify(mergedSettings, null, 2));
2490
+ console.log(c.green(' ✅ ~/.claude/settings.json updated\n'));
2491
+ } catch (e) {
2492
+ console.log(c.red(` ❌ Failed to update settings: ${e.message}\n`));
2493
+ process.exit(1);
2494
+ }
2495
+
2496
+ // Summary
2497
+ console.log(c.cyan('╭─────────────────────────────────────────────────────╮'));
2498
+ console.log(c.cyan('│ │'));
2499
+ console.log(c.cyan('│ 🎉 ') + c.green('Hooks Installed Successfully!') + c.cyan(' │'));
2500
+ console.log(c.cyan('│ │'));
2501
+ console.log(c.cyan('│ Hooks location: ~/.claude/hooks/ │'));
2502
+ console.log(c.cyan('│ Settings: ~/.claude/settings.json │'));
2503
+ console.log(c.cyan('│ │'));
2504
+ console.log(c.cyan('│ ') + c.yellow('⚠️ Restart Claude Code') + c.cyan(' to activate hooks. │'));
2505
+ console.log(c.cyan('│ │'));
2506
+ console.log(c.cyan('╰─────────────────────────────────────────────────────╯\n'));
2507
+
2508
+ console.log(c.cyan('Next steps:'));
2509
+ console.log(' 1. If not logged in: teleportation login');
2510
+ console.log(' 2. Check status: teleportation status');
2511
+ console.log(' 3. Restart Claude Code to apply hooks\n');
2512
+ }
2513
+
2514
+ async function commandCommand() {
2515
+ const sessionId = process.env.TELEPORTATION_SESSION_ID;
2516
+ if (!sessionId) {
2517
+ console.log(c.red('❌ Error: TELEPORTATION_SESSION_ID not set\n'));
2518
+ console.log(c.cyan('Set the environment variable: export TELEPORTATION_SESSION_ID=<session-id>\n'));
2519
+ process.exit(1);
2520
+ }
2521
+
2522
+ const text = process.argv.slice(3).join(' ');
2523
+ if (!text) {
2524
+ console.log(c.red('❌ Error: Command text is required\n'));
2525
+ console.log(c.cyan('Usage: teleportation command "<text>"\n'));
2526
+ process.exit(1);
2527
+ }
2528
+
2529
+ const creds = await getCredentials();
2530
+ const relayUrl = creds.RELAY_API_URL;
2531
+ const relayKey = creds.RELAY_API_KEY;
2532
+
2533
+ if (!relayUrl || !relayKey) {
2534
+ console.log(c.red('❌ Error: RELAY_API_URL or RELAY_API_KEY not configured\n'));
2535
+ process.exit(1);
2536
+ }
2537
+
2538
+ try {
2539
+ const res = await fetch(`${relayUrl}/api/messages`, {
2540
+ method: 'POST',
2541
+ headers: {
2542
+ 'Content-Type': 'application/json',
2543
+ 'Authorization': `Bearer ${relayKey}`,
2544
+ },
2545
+ body: JSON.stringify({
2546
+ session_id: sessionId,
2547
+ text,
2548
+ meta: {
2549
+ type: 'command',
2550
+ from: 'user',
2551
+ source: 'teleportation-cli',
2552
+ target_agent_id: 'daemon',
2553
+ },
2554
+ }),
2555
+ });
2556
+
2557
+ if (!res.ok) {
2558
+ console.log(c.red(`❌ Error: Failed to enqueue command (status ${res.status})\n`));
2559
+ process.exit(1);
2560
+ }
2561
+
2562
+ const data = await res.json();
2563
+ console.log(c.green('✅ Command enqueued successfully\n'));
2564
+ if (data.id) {
2565
+ console.log(c.cyan('Message ID: ') + data.id + '\n');
2566
+ }
2567
+ } catch (error) {
2568
+ console.log(c.red('❌ Error: ' + error.message + '\n'));
2569
+ process.exit(1);
2570
+ }
2571
+ }
2572
+
2573
+ /**
2574
+ * Update command - pulls latest code and reinstalls hooks
2575
+ */
2576
+ async function commandUpdate() {
2577
+ console.log(c.cyan('\n⚡ Teleportation Update\n'));
2578
+
2579
+ // Validate HOME_DIR exists
2580
+ if (!HOME_DIR || !fs.existsSync(HOME_DIR)) {
2581
+ console.log(c.red('❌ Error: HOME directory not found\n'));
2582
+ console.log(c.yellow('Set HOME environment variable and try again.\n'));
2583
+ return;
2584
+ }
2585
+
2586
+ const installDir = path.join(HOME_DIR, '.teleportation-cli');
2587
+
2588
+ // Check if installed via git
2589
+ if (!fs.existsSync(path.join(installDir, '.git'))) {
2590
+ console.log(c.yellow('⚠️ Not installed via git. Reinstall with:\n'));
2591
+ console.log(c.green(' curl -fsSL https://get.teleportation.dev | bash\n'));
2592
+ return;
2593
+ }
2594
+
2595
+ console.log(c.yellow('Step 1: Checking for local changes...\n'));
2596
+
2597
+ const { execSync, execFile } = require('child_process');
2598
+ let stashedChanges = false;
2599
+ let stashHash = null;
2600
+
2601
+ // Helper to check if command exists (POSIX compliant)
2602
+ // Whitelist prevents command injection if cmd parameter is compromised
2603
+ const commandExists = (cmd) => {
2604
+ // Whitelist of allowed commands to prevent command injection
2605
+ const validCommands = ['bun', 'npm', 'git', 'node'];
2606
+ if (!validCommands.includes(cmd)) {
2607
+ return false;
2608
+ }
2609
+ try {
2610
+ execSync(`command -v ${cmd}`, { stdio: 'pipe' });
2611
+ return true;
2612
+ } catch {
2613
+ return false;
2614
+ }
2615
+ };
2616
+
2617
+ // Helper to restore stashed changes using specific stash hash
2618
+ const restoreStash = () => {
2619
+ if (stashedChanges && stashHash !== null) {
2620
+ try {
2621
+ console.log(c.yellow(' Restoring stashed changes...\n'));
2622
+ // Use execFile with args array for safety
2623
+ execFile('git', ['stash', 'pop', `stash@{${stashHash}}`], {
2624
+ cwd: installDir,
2625
+ stdio: 'pipe'
2626
+ });
2627
+ console.log(c.green(' ✅ Stashed changes restored\n'));
2628
+ } catch (e) {
2629
+ console.log(c.yellow(` ⚠️ Could not restore stash: ${e.message}\n`));
2630
+ console.log(c.yellow(` Run \`git stash pop stash@{${stashHash}}\` manually to restore.\n`));
2631
+ }
2632
+ } else if (stashedChanges) {
2633
+ // Fallback if stash hash wasn't captured
2634
+ console.log(c.yellow(' ⚠️ Stash hash not available. Run `git stash pop` manually.\n'));
2635
+ }
2636
+ };
2637
+
2638
+ // Get current version before update
2639
+ let versionFrom = 'unknown';
2640
+ try {
2641
+ const pkgBefore = JSON.parse(fs.readFileSync(path.join(installDir, 'package.json'), 'utf8'));
2642
+ versionFrom = pkgBefore.version || 'unknown';
2643
+ } catch (e) {
2644
+ // Ignore - will show unknown
2645
+ }
2646
+
2647
+ // Check for uncommitted changes (including untracked files)
2648
+ try {
2649
+ const status = execSync('git status --porcelain', { cwd: installDir, encoding: 'utf8' });
2650
+ if (status.trim()) {
2651
+ console.log(c.yellow(' ⚠️ Uncommitted changes detected. Stashing...\n'));
2652
+ try {
2653
+ // Use -u to include untracked files, capture stash hash from output
2654
+ const stashOutput = execSync('git stash push -u -m "teleportation-update-stash"', {
2655
+ cwd: installDir,
2656
+ encoding: 'utf8',
2657
+ stdio: 'pipe'
2658
+ });
2659
+
2660
+ // Extract stash hash from output (e.g., "Saved working directory and index state On main: stash@{0}")
2661
+ const stashMatch = stashOutput.match(/stash@\{(\d+)\}/);
2662
+ if (stashMatch) {
2663
+ stashHash = stashMatch[1];
2664
+ }
2665
+
2666
+ console.log(c.green(` ✅ Changes stashed (will restore on failure)\n`));
2667
+ stashedChanges = true;
2668
+ } catch (stashErr) {
2669
+ console.log(c.red(` ❌ Failed to stash: ${stashErr.message}`));
2670
+ console.log(c.yellow(' Please commit or discard changes manually.\n'));
2671
+ return;
2672
+ }
2673
+ } else {
2674
+ console.log(c.green(' ✅ Working directory clean\n'));
2675
+ }
2676
+ } catch (e) {
2677
+ console.log(c.yellow(` ⚠️ Could not check git status: ${e.message}\n`));
2678
+ }
2679
+
2680
+ console.log(c.yellow('Step 2: Pulling latest changes...\n'));
2681
+
2682
+ try {
2683
+ // Detect current branch dynamically
2684
+ const branchRaw = execSync('git rev-parse --abbrev-ref HEAD', { cwd: installDir, encoding: 'utf8' }).trim();
2685
+
2686
+ // Sanitize branch name - only allow alphanumeric, dots, underscores, slashes, hyphens
2687
+ // Also enforce max length for safety
2688
+ if (!/^[a-zA-Z0-9._\/-]{1,100}$/.test(branchRaw)) {
2689
+ console.log(c.red(` ❌ Invalid branch name detected: ${branchRaw}\n`));
2690
+ console.log(c.yellow(' Branch names must contain only: letters, numbers, dots, underscores, slashes, hyphens (max 100 chars)\n'));
2691
+ restoreStash();
2692
+ return;
2693
+ }
2694
+
2695
+ const branch = branchRaw;
2696
+ console.log(c.dim(` Branch: ${branch}\n`));
2697
+
2698
+ // Use execFile with args array to prevent command injection
2699
+ execFile('git', ['pull', 'origin', branch], {
2700
+ cwd: installDir,
2701
+ stdio: 'inherit'
2702
+ });
2703
+ console.log(c.green('\n ✅ Code updated\n'));
2704
+ } catch (e) {
2705
+ console.log(c.red(` ❌ Failed to pull: ${e.message}\n`));
2706
+ restoreStash();
2707
+ return;
2708
+ }
2709
+
2710
+ console.log(c.yellow('Step 3: Installing dependencies...\n'));
2711
+
2712
+ let depsInstalled = false;
2713
+ try {
2714
+ // Check which package manager is available
2715
+ if (commandExists('bun')) {
2716
+ // Don't use --silent so errors are visible
2717
+ execSync('bun install', { cwd: installDir, stdio: 'inherit' });
2718
+ console.log(c.green('\n ✅ Dependencies updated (bun)\n'));
2719
+ depsInstalled = true;
2720
+ } else if (commandExists('npm')) {
2721
+ // Don't use --silent so errors are visible
2722
+ execSync('npm install', { cwd: installDir, stdio: 'inherit' });
2723
+ console.log(c.green('\n ✅ Dependencies updated (npm)\n'));
2724
+ depsInstalled = true;
2725
+ } else {
2726
+ console.log(c.red(' ❌ No package manager found (bun or npm required)\n'));
2727
+ console.log(c.yellow(' Install Node.js or Bun and run: teleportation update\n'));
2728
+ }
2729
+ } catch (e) {
2730
+ console.log(c.red(`\n ❌ Dependency installation failed: ${e.message}\n`));
2731
+ console.log(c.yellow(' Hooks may not work correctly. Run manually:\n'));
2732
+ console.log(c.dim(` cd ${installDir} && npm install\n`));
2733
+ }
2734
+
2735
+ console.log(c.yellow('Step 4: Updating hooks...\n'));
2736
+
2737
+ // Run install-hooks with error handling
2738
+ let hooksInstalled = false;
2739
+ try {
2740
+ await commandInstallHooks();
2741
+ hooksInstalled = true;
2742
+ } catch (e) {
2743
+ console.log(c.red(` ❌ Hook installation failed: ${e.message}\n`));
2744
+ console.log(c.yellow(' Run `teleportation install-hooks` manually to retry.\n'));
2745
+ // Don't restore stash here - code was updated successfully, hooks can be fixed separately
2746
+ }
2747
+
2748
+ // Get version after update
2749
+ let versionTo = 'unknown';
2750
+ try {
2751
+ const pkg = JSON.parse(fs.readFileSync(path.join(installDir, 'package.json'), 'utf8'));
2752
+ versionTo = pkg.version || 'unknown';
2753
+ } catch (e) {
2754
+ console.log(c.yellow(` ⚠️ Could not read version: ${e.message}\n`));
2755
+ }
2756
+
2757
+ // Calculate dynamic padding (box width is 55 chars, need space for labels and borders)
2758
+ const formatVersionLine = (label, version) => {
2759
+ const maxVersionLen = 30; // Reasonable max for version strings
2760
+ const versionDisplay = version.length > maxVersionLen ? version.slice(0, maxVersionLen - 3) + '...' : version;
2761
+ const padding = 55 - label.length - versionDisplay.length - 4; // 4 for borders and spaces
2762
+ return `│ ${label}${' '.repeat(Math.max(1, padding))}${versionDisplay}│`;
2763
+ };
2764
+
2765
+ // Show appropriate completion message
2766
+ console.log('');
2767
+ if (hooksInstalled && depsInstalled) {
2768
+ console.log(c.green('╭─────────────────────────────────────────────────────╮'));
2769
+ console.log(c.green('│ ✅ Update complete! │'));
2770
+ console.log(c.green('│ │'));
2771
+ if (versionFrom !== 'unknown' || versionTo !== 'unknown') {
2772
+ console.log(c.green(formatVersionLine('From:', versionFrom)));
2773
+ console.log(c.green(formatVersionLine('To: ', versionTo)));
2774
+ } else {
2775
+ console.log(c.green(formatVersionLine('Version:', versionTo)));
2776
+ }
2777
+ console.log(c.green('│ │'));
2778
+ console.log(c.green('│ ') + c.yellow('⚠️ Restart Claude Code to apply changes.') + c.green(' │'));
2779
+ console.log(c.green('╰─────────────────────────────────────────────────────╯'));
2780
+ } else if (hooksInstalled) {
2781
+ console.log(c.yellow('╭─────────────────────────────────────────────────────╮'));
2782
+ console.log(c.yellow('│ ⚠️ Update partially complete │'));
2783
+ console.log(c.yellow('│ │'));
2784
+ if (versionFrom !== 'unknown' || versionTo !== 'unknown') {
2785
+ console.log(c.yellow(formatVersionLine('From:', versionFrom)));
2786
+ console.log(c.yellow(formatVersionLine('To: ', versionTo)));
2787
+ } else {
2788
+ console.log(c.yellow(formatVersionLine('Version:', versionTo)));
2789
+ }
2790
+ console.log(c.yellow('│ Hooks: ✅ Dependencies: ❌ │'));
2791
+ console.log(c.yellow('│ │'));
2792
+ console.log(c.yellow('│ Run: cd ~/.teleportation-cli && npm install │'));
2793
+ console.log(c.yellow('╰─────────────────────────────────────────────────────╯'));
2794
+ } else {
2795
+ console.log(c.red('╭─────────────────────────────────────────────────────╮'));
2796
+ console.log(c.red('│ ❌ Update failed │'));
2797
+ console.log(c.red('│ │'));
2798
+ console.log(c.red('│ Code was updated but hooks failed to install. │'));
2799
+ console.log(c.red('│ Run: teleportation install-hooks │'));
2800
+ console.log(c.red('╰─────────────────────────────────────────────────────╯'));
2801
+ }
2802
+ console.log('');
2803
+ }
2804
+
2805
+ // Main
2806
+ const command = process.argv[2] || 'help';
2807
+ const args = process.argv.slice(3);
2808
+
2809
+ // Handle async commands that need to complete before exit
2810
+ const asyncCommands = ['login', 'logout', 'status', 'test', 'env', 'config', 'daemon', 'away', 'back', 'daemon-status', 'command', 'inbox', 'inbox-ack', 'install-hooks', 'update'];
2811
+ if (asyncCommands.includes(command)) {
2812
+ // These commands handle their own async execution
2813
+ }
2814
+
2815
+ try {
2816
+ switch (command) {
2817
+ case 'setup':
2818
+ commandSetup().catch(err => {
2819
+ console.error(c.red('❌ Error:'), err.message);
2820
+ process.exit(1);
2821
+ });
2822
+ break;
2823
+ case 'backup':
2824
+ commandBackup(args).catch(err => {
2825
+ console.error(c.red('❌ Error:'), err.message);
2826
+ process.exit(1);
2827
+ });
2828
+ break;
2829
+ case 'on':
2830
+ commandOn().catch(err => {
2831
+ console.error(c.red('❌ Error:'), err.message);
2832
+ process.exit(1);
2833
+ });
2834
+ break;
2835
+ case 'install-hooks':
2836
+ commandInstallHooks().catch(err => {
2837
+ console.error(c.red('❌ Error:'), err.message);
2838
+ process.exit(1);
2839
+ });
2840
+ break;
2841
+ case 'off':
2842
+ commandOff();
2843
+ break;
2844
+ case 'status':
2845
+ commandStatus().catch(err => {
2846
+ console.error(c.red('❌ Error:'), err.message);
2847
+ process.exit(1);
2848
+ });
2849
+ break;
2850
+ case 'start':
2851
+ commandStart();
2852
+ break;
2853
+ case 'stop':
2854
+ commandStop();
2855
+ break;
2856
+ case 'restart':
2857
+ commandRestart();
2858
+ break;
2859
+ case 'test':
2860
+ commandTest().catch(err => {
2861
+ console.error(c.red('❌ Error:'), err.message);
2862
+ process.exit(1);
2863
+ });
2864
+ break;
2865
+ case 'doctor':
2866
+ commandDoctor().catch(err => {
2867
+ console.error(c.red('❌ Error:'), err.message);
2868
+ process.exit(1);
2869
+ });
2870
+ break;
2871
+ case 'uninstall':
2872
+ commandUninstall().catch(err => {
2873
+ console.error(c.red('❌ Error:'), err.message);
2874
+ process.exit(1);
2875
+ });
2876
+ break;
2877
+ case 'env':
2878
+ commandEnv(args).catch(err => {
2879
+ console.error(c.red('❌ Error:'), err.message);
2880
+ process.exit(1);
2881
+ });
2882
+ break;
2883
+ case 'config':
2884
+ commandConfig(args).catch(err => {
2885
+ console.error(c.red('❌ Error:'), err.message);
2886
+ process.exit(1);
2887
+ });
2888
+ break;
2889
+ case 'info':
2890
+ commandInfo();
2891
+ break;
2892
+ case 'logs':
2893
+ commandLogs(args);
2894
+ break;
2895
+ case 'login':
2896
+ commandLogin(args).catch(err => {
2897
+ console.error(c.red('❌ Error:'), err.message);
2898
+ process.exit(1);
2899
+ });
2900
+ break;
2901
+ case 'daemon':
2902
+ commandDaemon(args).catch(err => {
2903
+ console.error(c.red('❌ Error:'), err.message);
2904
+ process.exit(1);
2905
+ });
2906
+ break;
2907
+ case 'away':
2908
+ commandAwayMode().catch(err => {
2909
+ console.error(c.red('❌ Error:'), err.message);
2910
+ process.exit(1);
2911
+ });
2912
+ break;
2913
+ case 'back':
2914
+ commandBackMode().catch(err => {
2915
+ console.error(c.red('❌ Error:'), err.message);
2916
+ process.exit(1);
2917
+ });
2918
+ break;
2919
+ case 'daemon-status':
2920
+ commandDaemonStatusDisplay().catch(err => {
2921
+ console.error(c.red('❌ Error:'), err.message);
2922
+ process.exit(1);
2923
+ });
2924
+ break;
2925
+ case 'command':
2926
+ commandCommand().catch(err => {
2927
+ console.error(c.red('❌ Error:'), err.message);
2928
+ process.exit(1);
2929
+ });
2930
+ break;
2931
+ case 'inbox':
2932
+ commandInbox().catch(err => {
2933
+ console.error(c.red('❌ Error:'), err.message);
2934
+ process.exit(1);
2935
+ });
2936
+ break;
2937
+ case 'inbox-ack':
2938
+ commandInboxAck(args[0]).catch(err => {
2939
+ console.error(c.red('❌ Error:'), err.message);
2940
+ process.exit(1);
2941
+ });
2942
+ break;
2943
+ case 'logout':
2944
+ commandLogout().catch(err => {
2945
+ console.error(c.red('❌ Error:'), err.message);
2946
+ process.exit(1);
2947
+ });
2948
+ break;
2949
+ case 'worktree':
2950
+ commandWorktree(args).catch(err => {
2951
+ console.error(c.red('❌ Error:'), err.message);
2952
+ process.exit(1);
2953
+ });
2954
+ break;
2955
+ case 'snapshot':
2956
+ commandSnapshot(args).catch(err => {
2957
+ console.error(c.red('❌ Error:'), err.message);
2958
+ process.exit(1);
2959
+ });
2960
+ break;
2961
+ case 'session':
2962
+ commandSession(args).catch(err => {
2963
+ console.error(c.red('❌ Error:'), err.message);
2964
+ process.exit(1);
2965
+ });
2966
+ break;
2967
+ case 'version':
2968
+ case '--version':
2969
+ case '-v':
2970
+ commandVersion();
2971
+ break;
2972
+ case 'update':
2973
+ commandUpdate().catch(err => {
2974
+ console.error(c.red('❌ Error:'), err.message);
2975
+ process.exit(1);
2976
+ });
2977
+ break;
2978
+ case 'help':
2979
+ case '--help':
2980
+ case '-h':
2981
+ default:
2982
+ commandHelp();
2983
+ }
2984
+ } catch (error) {
2985
+ console.error(c.red('❌ Error:'), error.message);
2986
+ process.exit(1);
2987
+ }