teleportation-cli 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { execSync } = require('child_process');
7
+ const { execSync, spawnSync } = require('child_process');
8
8
  const os = require('os');
9
9
 
10
10
  const CLI_VERSION = '1.2.2';
@@ -246,6 +246,14 @@ function commandHelp() {
246
246
  console.log(' ' + c.green('daemon restart') + ' Restart the daemon');
247
247
  console.log(' ' + c.green('daemon status') + ' Show daemon status');
248
248
  console.log(' ' + c.green('daemon health') + ' Check daemon health\n');
249
+
250
+ console.log(c.yellow('Transcript Sync:'));
251
+ console.log(' ' + c.green('transcript-sync start') + ' Start transcript sync worker');
252
+ console.log(' ' + c.green('transcript-sync stop') + ' Stop transcript sync worker');
253
+ console.log(' ' + c.green('transcript-sync restart') + ' Restart transcript sync worker');
254
+ console.log(' ' + c.green('transcript-sync status') + ' Show transcript sync status');
255
+ console.log(' ' + c.green('transcript-sync run-now') + ' Run transcript sync immediately');
256
+ console.log(' ' + c.green('transcript-sync health') + ' Check transcript sync health\n');
249
257
 
250
258
  console.log(c.yellow('Inbox & Messaging:'));
251
259
  console.log(' ' + c.green('command "<text>"') + ' Enqueue a command message for this session');
@@ -315,14 +323,24 @@ async function commandOn() {
315
323
  const hooksDir = path.join(TELEPORTATION_DIR, '.claude', 'hooks');
316
324
  const uhrResult = await installViaUhr(manifestPath, hooksDir);
317
325
 
318
- if (uhrResult.success) {
326
+ // Verify UHR actually wrote the Claude settings file — it can return
327
+ // success=true while silently skipping the write when the manifest
328
+ // contains platforms (gemini-cli, cursor) not in its lockfile.
329
+ const globalSettings = path.join(os.homedir(), '.claude', 'settings.json');
330
+ const uhrActuallyWorked = uhrResult.success && fs.existsSync(globalSettings);
331
+
332
+ if (uhrActuallyWorked) {
319
333
  console.log(c.green('\nšŸŽ‰ Teleportation Remote Control ENABLED!'));
320
334
  console.log(c.cyan('\nInstalled via UHR (Universal Hook Registry)'));
321
335
  if (uhrResult.warnings.length > 0) {
322
336
  uhrResult.warnings.forEach(w => console.log(c.yellow(` āš ļø ${w}`)));
323
337
  }
324
338
  } else {
325
- console.log(c.yellow(` āš ļø UHR install failed: ${uhrResult.reason}`));
339
+ if (uhrResult.success && !fs.existsSync(globalSettings)) {
340
+ console.log(c.yellow(' āš ļø UHR reported success but did not write ~/.claude/settings.json'));
341
+ } else {
342
+ console.log(c.yellow(` āš ļø UHR install failed: ${uhrResult.reason}`));
343
+ }
326
344
  console.log(c.dim(' Falling back to legacy installer...'));
327
345
  await _legacyInstall();
328
346
  }
@@ -797,6 +815,18 @@ async function commandOff() {
797
815
  }
798
816
  }
799
817
 
818
+ // Remove Teleportation-managed Cursor hooks while preserving third-party entries
819
+ try {
820
+ const installerPath = path.join(TELEPORTATION_DIR, 'lib', 'install', 'installer.js');
821
+ const { uninstallCursorHooks } = await import('file://' + installerPath);
822
+ const result = await uninstallCursorHooks();
823
+ if (result?.success) {
824
+ console.log(c.green('āœ… Removed Teleportation hooks from ~/.cursor/hooks.json'));
825
+ }
826
+ } catch (e) {
827
+ console.log(c.yellow(`āš ļø Could not update Cursor hooks: ${e.message}`));
828
+ }
829
+
800
830
  console.log(c.yellow('\nšŸ›‘ Teleportation Remote Control DISABLED'));
801
831
  console.log(c.cyan('Services are still running. Stop with: ./teleportation stop\n'));
802
832
  }
@@ -2262,6 +2292,12 @@ async function commandConfig(args) {
2262
2292
  console.log(c.cyan('\nNotification Settings:'));
2263
2293
  console.log(` Enabled: ${config.notifications?.enabled ? c.green('yes') : c.yellow('no')}`);
2264
2294
  console.log(` Sound: ${config.notifications?.sound ? c.green('enabled') : c.yellow('disabled')}`);
2295
+
2296
+ console.log(c.cyan('\nTranscript Sync Settings:'));
2297
+ console.log(` Enabled: ${config.transcriptSync?.enabled ? c.green('yes') : c.yellow('no')}`);
2298
+ console.log(` Interval: ${c.green(((config.transcriptSync?.intervalMs || 0) / 1000) + ' seconds')}`);
2299
+ console.log(` Mode: ${c.green(config.transcriptSync?.mode || 'local')}`);
2300
+ console.log(` Scope: ${c.green('auto (current git repo)')}`);
2265
2301
 
2266
2302
  console.log(c.cyan(`\nConfig file: ${DEFAULT_CONFIG_PATH}\n`));
2267
2303
 
@@ -2289,6 +2325,13 @@ async function commandConfig(args) {
2289
2325
  console.log(c.cyan('Example: teleportation config set notifications.sound true\n'));
2290
2326
  return;
2291
2327
  }
2328
+
2329
+ // Transcript sync is repo-driven; users should only choose mode.
2330
+ if (key.startsWith('transcriptSync.') && key !== 'transcriptSync.mode') {
2331
+ console.log(c.red('āŒ Only transcriptSync.mode is user-configurable\n'));
2332
+ console.log(c.cyan('Use: teleportation config set transcriptSync.mode local|push|pull|bidirectional\n'));
2333
+ return;
2334
+ }
2292
2335
 
2293
2336
  // Try to parse value as JSON, number, or boolean
2294
2337
  let value = valueStr;
@@ -2927,6 +2970,248 @@ async function commandDaemon(args) {
2927
2970
  }
2928
2971
  }
2929
2972
 
2973
+ async function commandTranscriptSync(args) {
2974
+ const subCommand = args[0] || 'status';
2975
+ const statePath = path.join(HOME_DIR, '.teleportation', 'transcript-sync-state.json');
2976
+
2977
+ try {
2978
+ const lifecyclePath = path.join(TELEPORTATION_DIR, 'lib', 'transcript-sync', 'lifecycle.js');
2979
+ const configPath = path.join(TELEPORTATION_DIR, 'lib', 'config', 'manager.js');
2980
+ const repoContextPath = path.join(TELEPORTATION_DIR, 'lib', 'transcript-sync', 'repo-context.js');
2981
+ const syncScriptPath = path.join(TELEPORTATION_DIR, 'scripts', 'sync-transcripts.sh');
2982
+ const { loadConfig, setConfigValue } = await import('file://' + configPath);
2983
+ const { getRepoSyncContext } = await import('file://' + repoContextPath);
2984
+ const loadLifecycle = async () => {
2985
+ try {
2986
+ return await import('file://' + lifecyclePath);
2987
+ } catch (error) {
2988
+ if (error.message.includes('@derivativelabs/agent-process')) {
2989
+ throw new Error('Missing dependency: @derivativelabs/agent-process. Run `bun install` and retry.');
2990
+ }
2991
+ throw error;
2992
+ }
2993
+ };
2994
+
2995
+ switch (subCommand) {
2996
+ case 'start': {
2997
+ console.log(c.yellow('Starting transcript sync worker...\n'));
2998
+ const { startTranscriptSync } = await loadLifecycle();
2999
+ const result = await startTranscriptSync();
3000
+ try {
3001
+ await setConfigValue('transcriptSync.enabled', true);
3002
+ } catch (e) {
3003
+ console.log(c.yellow(`āš ļø Could not update transcriptSync.enabled: ${e.message}`));
3004
+ }
3005
+ console.log(c.green(`āœ… Transcript sync started (PID: ${result.pid})\n`));
3006
+ break;
3007
+ }
3008
+
3009
+ case 'stop': {
3010
+ console.log(c.yellow('Stopping transcript sync worker...\n'));
3011
+ const { stopTranscriptSync } = await loadLifecycle();
3012
+ const result = await stopTranscriptSync();
3013
+ try {
3014
+ await setConfigValue('transcriptSync.enabled', false);
3015
+ } catch (e) {
3016
+ console.log(c.yellow(`āš ļø Could not update transcriptSync.enabled: ${e.message}`));
3017
+ }
3018
+ if (result.success) {
3019
+ console.log(c.green(`āœ… Transcript sync stopped${result.forced ? ' (forced)' : ''}\n`));
3020
+ } else {
3021
+ console.log(c.red('āŒ Failed to stop transcript sync\n'));
3022
+ process.exit(1);
3023
+ }
3024
+ break;
3025
+ }
3026
+
3027
+ case 'restart': {
3028
+ console.log(c.yellow('Restarting transcript sync worker...\n'));
3029
+ const { restartTranscriptSync } = await loadLifecycle();
3030
+ const result = await restartTranscriptSync();
3031
+ try {
3032
+ await setConfigValue('transcriptSync.enabled', true);
3033
+ } catch (e) {
3034
+ console.log(c.yellow(`āš ļø Could not update transcriptSync.enabled: ${e.message}`));
3035
+ }
3036
+ console.log(c.green(`āœ… Transcript sync restarted (PID: ${result.pid})`));
3037
+ console.log(c.cyan(`Previous worker ${result.wasRunning ? 'was running' : 'was not running'}\n`));
3038
+ break;
3039
+ }
3040
+
3041
+ case 'run-now': {
3042
+ const cfg = await loadConfig();
3043
+ const ts = cfg.transcriptSync || {};
3044
+ const repoContext = getRepoSyncContext(TELEPORTATION_DIR);
3045
+ const machineId = repoContext.machineId || os.hostname().split('.')[0];
3046
+ const mode = ts.mode || 'local';
3047
+ const mirrorDir = repoContext.mirrorDir;
3048
+ const peer = repoContext.peer || '';
3049
+ const peerMirrorDir = repoContext.peerMirrorDir || '';
3050
+ const now = new Date().toISOString();
3051
+ let previousState = {};
3052
+ if (fs.existsSync(statePath)) {
3053
+ try {
3054
+ previousState = JSON.parse(fs.readFileSync(statePath, 'utf8'));
3055
+ } catch {
3056
+ previousState = {};
3057
+ }
3058
+ }
3059
+
3060
+ if (['push', 'pull', 'bidirectional'].includes(mode) && !peer) {
3061
+ console.log(c.red(`āŒ transcriptSync.mode is "${mode}" but no auto peer is configured for this repo\n`));
3062
+ console.log(c.cyan('Set repo peer once: git config teleportation.transcriptSyncPeer "<user@host>"\n'));
3063
+ process.exit(1);
3064
+ }
3065
+
3066
+ const syncArgs = ['--machine-id', machineId, '--mirror-dir', mirrorDir, '--mode', mode];
3067
+ if (peer) syncArgs.push('--peer', peer);
3068
+ if (peerMirrorDir) syncArgs.push('--peer-mirror-dir', peerMirrorDir);
3069
+
3070
+ console.log(c.yellow('Running transcript sync now...\n'));
3071
+ const result = spawnSync(syncScriptPath, syncArgs, {
3072
+ env: process.env,
3073
+ encoding: 'utf8'
3074
+ });
3075
+ if (result.stdout) process.stdout.write(result.stdout);
3076
+ if (result.stderr) process.stderr.write(result.stderr);
3077
+ const stderrText = result.stderr || '';
3078
+ const hasRsyncErrors = /rsync\([^)]+\): error:/i.test(stderrText);
3079
+ if (result.status !== 0) {
3080
+ fs.writeFileSync(statePath, JSON.stringify({
3081
+ ...previousState,
3082
+ running: previousState.running ?? false,
3083
+ lastRunAt: now,
3084
+ lastExitCode: result.status || 1,
3085
+ lastMode: mode,
3086
+ lastPeer: peer || null,
3087
+ machineId,
3088
+ lastSuccessAt: previousState.lastSuccessAt || null,
3089
+ lastErrorAt: now,
3090
+ skipped: false,
3091
+ skipReason: null,
3092
+ lastOutput: (result.stdout || '').slice(-4000),
3093
+ lastError: (result.stderr || '').slice(-4000)
3094
+ }, null, 2), { mode: 0o600 });
3095
+ console.log(c.red('\nāŒ transcript-sync run-now failed\n'));
3096
+ process.exit(result.status || 1);
3097
+ }
3098
+ if (hasRsyncErrors) {
3099
+ fs.writeFileSync(statePath, JSON.stringify({
3100
+ ...previousState,
3101
+ running: previousState.running ?? false,
3102
+ lastRunAt: now,
3103
+ lastExitCode: 1,
3104
+ lastMode: mode,
3105
+ lastPeer: peer || null,
3106
+ machineId,
3107
+ lastSuccessAt: previousState.lastSuccessAt || null,
3108
+ lastErrorAt: now,
3109
+ skipped: false,
3110
+ skipReason: null,
3111
+ lastOutput: (result.stdout || '').slice(-4000),
3112
+ lastError: (result.stderr || '').slice(-4000)
3113
+ }, null, 2), { mode: 0o600 });
3114
+ console.log(c.red('\nāŒ transcript-sync run-now detected rsync errors in output\n'));
3115
+ process.exit(1);
3116
+ }
3117
+ fs.writeFileSync(statePath, JSON.stringify({
3118
+ ...previousState,
3119
+ running: previousState.running ?? false,
3120
+ lastRunAt: now,
3121
+ lastExitCode: 0,
3122
+ lastMode: mode,
3123
+ lastPeer: peer || null,
3124
+ machineId,
3125
+ lastSuccessAt: now,
3126
+ lastErrorAt: previousState.lastErrorAt || null,
3127
+ skipped: false,
3128
+ skipReason: null,
3129
+ lastOutput: (result.stdout || '').slice(-4000),
3130
+ lastError: (result.stderr || '').slice(-4000)
3131
+ }, null, 2), { mode: 0o600 });
3132
+ console.log(c.green('\nāœ… transcript-sync run-now completed\n'));
3133
+ break;
3134
+ }
3135
+
3136
+ case 'status':
3137
+ case 'health': {
3138
+ let status = { running: false, pid: null, uptime: null };
3139
+ try {
3140
+ const { getTranscriptSyncStatus } = await loadLifecycle();
3141
+ status = await getTranscriptSyncStatus();
3142
+ } catch (error) {
3143
+ if (subCommand === 'health') {
3144
+ throw error;
3145
+ }
3146
+ console.log(c.yellow(`āš ļø ${error.message}`));
3147
+ }
3148
+ const cfg = await loadConfig();
3149
+ const ts = cfg.transcriptSync || {};
3150
+ const repoContext = getRepoSyncContext(TELEPORTATION_DIR);
3151
+ let state = null;
3152
+
3153
+ if (fs.existsSync(statePath)) {
3154
+ try {
3155
+ state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
3156
+ } catch {
3157
+ state = null;
3158
+ }
3159
+ }
3160
+
3161
+ console.log(c.purple('Transcript Sync Status\n'));
3162
+ console.log(`Worker: ${status.running ? c.green(`running (PID: ${status.pid})`) : c.red('stopped')}`);
3163
+ if (status.uptime) {
3164
+ console.log(`Uptime: ${c.cyan(Math.round(status.uptime / 60000) + 'm')}`);
3165
+ }
3166
+ console.log(`Enabled: ${ts.enabled ? c.green('yes') : c.yellow('no')}`);
3167
+ console.log(`Interval: ${c.cyan(((ts.intervalMs || 300000) / 1000) + 's')}`);
3168
+ console.log(`Mode: ${c.cyan(ts.mode || 'local')}`);
3169
+ console.log(`Repo key: ${c.cyan(repoContext.repoKey)}`);
3170
+ console.log(`Machine ID: ${c.cyan(repoContext.machineId || os.hostname().split('.')[0])}`);
3171
+ console.log(`Peer: ${c.cyan(repoContext.peer || '(auto-unset)')}`);
3172
+ console.log(`Mirror dir: ${c.cyan(repoContext.mirrorDir)}`);
3173
+
3174
+ if (state) {
3175
+ if (state.lastRunAt) console.log(`Last run: ${c.cyan(state.lastRunAt)}`);
3176
+ if (state.lastSuccessAt) console.log(`Last success: ${c.green(state.lastSuccessAt)}`);
3177
+ if (state.lastErrorAt) console.log(`Last error: ${c.red(state.lastErrorAt)}`);
3178
+ if (state.skipReason) console.log(`Last skip: ${c.yellow(state.skipReason)}`);
3179
+ } else {
3180
+ console.log(c.yellow('State: no transcript sync state file yet'));
3181
+ }
3182
+ console.log('');
3183
+
3184
+ if (subCommand === 'health') {
3185
+ if (!status.running) {
3186
+ console.log(c.red('āŒ transcript-sync is not running\n'));
3187
+ process.exit(1);
3188
+ }
3189
+ if (state && state.lastErrorAt && !state.lastSuccessAt) {
3190
+ console.log(c.red('āŒ transcript-sync has errors and no successful run yet\n'));
3191
+ process.exit(1);
3192
+ }
3193
+ console.log(c.green('āœ… transcript-sync health check passed\n'));
3194
+ }
3195
+ break;
3196
+ }
3197
+
3198
+ default:
3199
+ console.log(c.red(`Unknown transcript-sync command: ${subCommand}\n`));
3200
+ console.log(c.cyan('Available commands:'));
3201
+ console.log(' teleportation transcript-sync start');
3202
+ console.log(' teleportation transcript-sync stop');
3203
+ console.log(' teleportation transcript-sync restart');
3204
+ console.log(' teleportation transcript-sync status');
3205
+ console.log(' teleportation transcript-sync run-now');
3206
+ console.log(' teleportation transcript-sync health\n');
3207
+ process.exit(1);
3208
+ }
3209
+ } catch (error) {
3210
+ console.log(c.red(`āŒ transcript-sync command failed: ${error.message}\n`));
3211
+ process.exit(1);
3212
+ }
3213
+ }
3214
+
2930
3215
  // Away Mode Commands (Task 9.0)
2931
3216
  async function commandAwayMode() {
2932
3217
  console.log(c.yellow('šŸš€ Marking session as away and starting daemon...\n'));
@@ -3523,7 +3808,7 @@ const command = process.argv[2] || 'help';
3523
3808
  const args = process.argv.slice(3);
3524
3809
 
3525
3810
  // Handle async commands that need to complete before exit
3526
- const asyncCommands = ['login', 'logout', 'status', 'test', 'env', 'config', 'daemon', 'away', 'back', 'daemon-status', 'command', 'inbox', 'inbox-ack', 'install-hooks', 'update', 'remote', 'off'];
3811
+ const asyncCommands = ['login', 'logout', 'status', 'test', 'env', 'config', 'daemon', 'away', 'back', 'daemon-status', 'command', 'inbox', 'inbox-ack', 'install-hooks', 'update', 'remote', 'off', 'transcript-sync'];
3527
3812
  // Keep this list in sync with switch cases below
3528
3813
  asyncCommands.push('github');
3529
3814
  if (asyncCommands.includes(command)) {
@@ -3637,6 +3922,12 @@ try {
3637
3922
  process.exit(1);
3638
3923
  });
3639
3924
  break;
3925
+ case 'transcript-sync':
3926
+ commandTranscriptSync(args).catch(err => {
3927
+ console.error(c.red('āŒ Error:'), err.message);
3928
+ process.exit(1);
3929
+ });
3930
+ break;
3640
3931
  case 'away':
3641
3932
  commandAwayMode().catch(err => {
3642
3933
  console.error(c.red('āŒ Error:'), err.message);