@tongil_kim/clautunnel 1.6.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -31,6 +31,7 @@ declare class Config {
31
31
  requireConfiguration(): void;
32
32
  getMachineId(): string | undefined;
33
33
  setMachineId(machineId: string): void;
34
+ clearMachineId(): void;
34
35
  getSessionTokens(): ConfigData['sessionTokens'] | undefined;
35
36
  setSessionTokens(tokens: ConfigData['sessionTokens']): void;
36
37
  setSession(tokens: {
package/dist/index.js CHANGED
@@ -202,7 +202,7 @@ var require_dist = __commonJS({
202
202
  // src/index.ts
203
203
  import { config } from "dotenv";
204
204
  import { resolve as resolve2, dirname as dirname3 } from "path";
205
- import { readFileSync as readFileSync5 } from "fs";
205
+ import { readFileSync as readFileSync6 } from "fs";
206
206
  import { fileURLToPath } from "url";
207
207
 
208
208
  // src/utils/config.ts
@@ -293,6 +293,10 @@ var Config = class {
293
293
  this.data.machineId = machineId;
294
294
  this.saveConfig();
295
295
  }
296
+ clearMachineId() {
297
+ delete this.data.machineId;
298
+ this.saveConfig();
299
+ }
296
300
  getSessionTokens() {
297
301
  return this.data.sessionTokens;
298
302
  }
@@ -311,8 +315,8 @@ var Config = class {
311
315
  getMobileProjectPath() {
312
316
  return this.data.mobileProjectPath;
313
317
  }
314
- setMobileProjectPath(path4) {
315
- this.data.mobileProjectPath = path4;
318
+ setMobileProjectPath(path5) {
319
+ this.data.mobileProjectPath = path5;
316
320
  this.saveConfig();
317
321
  }
318
322
  };
@@ -419,10 +423,11 @@ var RealtimeClient = class extends EventEmitter {
419
423
  this.sessionId = options.sessionId;
420
424
  }
421
425
  async connect() {
426
+ const privateConfig = { config: { private: true } };
422
427
  const outputChannelName = import_clautunnel_shared.REALTIME_CHANNELS.sessionOutput(this.sessionId);
423
- this.outputChannel = this.supabase.channel(outputChannelName);
428
+ this.outputChannel = this.supabase.channel(outputChannelName, privateConfig);
424
429
  const inputChannelName = import_clautunnel_shared.REALTIME_CHANNELS.sessionInput(this.sessionId);
425
- this.inputChannel = this.supabase.channel(inputChannelName);
430
+ this.inputChannel = this.supabase.channel(inputChannelName, privateConfig);
426
431
  this.inputChannel.on("broadcast", { event: "input" }, (payload) => {
427
432
  this.emit("input", payload.payload);
428
433
  });
@@ -433,7 +438,7 @@ var RealtimeClient = class extends EventEmitter {
433
438
  this.realtimeEnabled = results.every((success) => success);
434
439
  if (this.realtimeEnabled) {
435
440
  const presenceChannelName = import_clautunnel_shared.REALTIME_CHANNELS.sessionPresence(this.sessionId);
436
- this.presenceChannel = this.supabase.channel(presenceChannelName);
441
+ this.presenceChannel = this.supabase.channel(presenceChannelName, privateConfig);
437
442
  this.presenceChannel.subscribe(async (status, err) => {
438
443
  if (status === "SUBSCRIBED" && this.presenceChannel) {
439
444
  try {
@@ -1884,23 +1889,23 @@ var ConfigManager = class {
1884
1889
  getProjectSettingsPath() {
1885
1890
  return join3(this.cwd, ".claude", "settings.json");
1886
1891
  }
1887
- readSettingsFile(path4) {
1892
+ readSettingsFile(path5) {
1888
1893
  try {
1889
- if (existsSync3(path4)) {
1890
- const content = readFileSync3(path4, "utf-8");
1894
+ if (existsSync3(path5)) {
1895
+ const content = readFileSync3(path5, "utf-8");
1891
1896
  return JSON.parse(content);
1892
1897
  }
1893
1898
  } catch {
1894
1899
  }
1895
1900
  return null;
1896
1901
  }
1897
- writeSettingsFile(path4, settings) {
1902
+ writeSettingsFile(path5, settings) {
1898
1903
  try {
1899
- const dir = dirname(path4);
1904
+ const dir = dirname(path5);
1900
1905
  if (!existsSync3(dir)) {
1901
1906
  mkdirSync2(dir, { recursive: true });
1902
1907
  }
1903
- writeFileSync2(path4, JSON.stringify(settings, null, 2));
1908
+ writeFileSync2(path5, JSON.stringify(settings, null, 2));
1904
1909
  return true;
1905
1910
  } catch {
1906
1911
  return false;
@@ -2099,7 +2104,7 @@ var ConfigManager = class {
2099
2104
  };
2100
2105
  }
2101
2106
  }
2102
- applyConfigChange(settings, path4, payload) {
2107
+ applyConfigChange(settings, path5, payload) {
2103
2108
  if (!settings.preferences) {
2104
2109
  settings.preferences = {};
2105
2110
  }
@@ -2119,33 +2124,33 @@ var ConfigManager = class {
2119
2124
  default:
2120
2125
  return { success: false, message: `Unknown config key: ${payload.key}` };
2121
2126
  }
2122
- if (this.writeSettingsFile(path4, settings)) {
2127
+ if (this.writeSettingsFile(path5, settings)) {
2123
2128
  return { success: true, message: `Config ${payload.key} updated` };
2124
2129
  }
2125
2130
  return { success: false, message: "Failed to write settings file" };
2126
2131
  }
2127
- applyPermissionsChange(settings, path4, payload) {
2132
+ applyPermissionsChange(settings, path5, payload) {
2128
2133
  if (!settings.permissions) {
2129
2134
  settings.permissions = {};
2130
2135
  }
2131
2136
  settings.permissions.mode = payload.value;
2132
- if (this.writeSettingsFile(path4, settings)) {
2137
+ if (this.writeSettingsFile(path5, settings)) {
2133
2138
  return { success: true, message: `Permission mode set to ${payload.value}` };
2134
2139
  }
2135
2140
  return { success: false, message: "Failed to write settings file" };
2136
2141
  }
2137
- applyVimChange(settings, path4, payload) {
2142
+ applyVimChange(settings, path5, payload) {
2138
2143
  if (payload.action === "toggle") {
2139
2144
  settings.vim = !settings.vim;
2140
2145
  } else {
2141
2146
  settings.vim = payload.value;
2142
2147
  }
2143
- if (this.writeSettingsFile(path4, settings)) {
2148
+ if (this.writeSettingsFile(path5, settings)) {
2144
2149
  return { success: true, message: `Vim mode ${settings.vim ? "enabled" : "disabled"}` };
2145
2150
  }
2146
2151
  return { success: false, message: "Failed to write settings file" };
2147
2152
  }
2148
- applyAllowedToolsChange(settings, path4, payload) {
2153
+ applyAllowedToolsChange(settings, path5, payload) {
2149
2154
  if (!settings.permissions) {
2150
2155
  settings.permissions = {};
2151
2156
  }
@@ -2177,7 +2182,7 @@ var ConfigManager = class {
2177
2182
  }
2178
2183
  break;
2179
2184
  }
2180
- if (this.writeSettingsFile(path4, settings)) {
2185
+ if (this.writeSettingsFile(path5, settings)) {
2181
2186
  return { success: true, message: "Allowed tools updated" };
2182
2187
  }
2183
2188
  return { success: false, message: "Failed to write settings file" };
@@ -2644,7 +2649,7 @@ ${confirmationMsg}
2644
2649
  };
2645
2650
 
2646
2651
  // src/index.ts
2647
- import { Command as Command9 } from "commander";
2652
+ import { Command as Command10 } from "commander";
2648
2653
 
2649
2654
  // src/commands/start.ts
2650
2655
  import { Command } from "commander";
@@ -2665,13 +2670,14 @@ var MachineRealtimeClient = class extends EventEmitter4 {
2665
2670
  this.machineId = options.machineId;
2666
2671
  }
2667
2672
  async connect() {
2673
+ const privateConfig = { config: { private: true } };
2668
2674
  const inputChannelName = import_clautunnel_shared4.REALTIME_CHANNELS.machineInput(this.machineId);
2669
- this.inputChannel = this.supabase.channel(inputChannelName);
2675
+ this.inputChannel = this.supabase.channel(inputChannelName, privateConfig);
2670
2676
  this.inputChannel.on("broadcast", { event: "machine-command" }, (payload) => {
2671
2677
  this.emit("command", payload.payload);
2672
2678
  });
2673
2679
  const outputChannelName = import_clautunnel_shared4.REALTIME_CHANNELS.machineOutput(this.machineId);
2674
- this.outputChannel = this.supabase.channel(outputChannelName);
2680
+ this.outputChannel = this.supabase.channel(outputChannelName, privateConfig);
2675
2681
  const results = await Promise.all([
2676
2682
  subscribeWithTimeout(this.inputChannel, "machine-input"),
2677
2683
  subscribeWithTimeout(this.outputChannel, "machine-output")
@@ -2679,7 +2685,7 @@ var MachineRealtimeClient = class extends EventEmitter4 {
2679
2685
  const connected = results.every((success) => success);
2680
2686
  if (connected) {
2681
2687
  const presenceChannelName = import_clautunnel_shared4.REALTIME_CHANNELS.machinePresence(this.machineId);
2682
- this.presenceChannel = this.supabase.channel(presenceChannelName);
2688
+ this.presenceChannel = this.supabase.channel(presenceChannelName, privateConfig);
2683
2689
  this.presenceChannel.subscribe(async (status) => {
2684
2690
  if (status === "SUBSCRIBED" && this.presenceChannel) {
2685
2691
  try {
@@ -3089,12 +3095,12 @@ var MobileServerManager = class {
3089
3095
  }
3090
3096
  ensureEnvFile() {
3091
3097
  const envPath = join5(this.mobileProjectPath, ".env");
3092
- const envContent = [
3098
+ const lines = [
3093
3099
  `EXPO_PUBLIC_SUPABASE_URL=${this.options.supabaseUrl}`,
3094
3100
  `EXPO_PUBLIC_SUPABASE_ANON_KEY=${this.options.supabaseAnonKey}`,
3095
3101
  ""
3096
- ].join("\n");
3097
- writeFileSync3(envPath, envContent);
3102
+ ];
3103
+ writeFileSync3(envPath, lines.join("\n"));
3098
3104
  }
3099
3105
  installDependencies() {
3100
3106
  const nodeModulesPath = join5(this.mobileProjectPath, "node_modules");
@@ -3153,7 +3159,7 @@ var MobileServerManager = class {
3153
3159
  }
3154
3160
  }
3155
3161
  this.ngrokError = this.diagnoseNgrokFailure(stderrData);
3156
- this.killProcess(this.ngrokProcess);
3162
+ this.killProcessTree(this.ngrokProcess);
3157
3163
  this.ngrokProcess = null;
3158
3164
  return null;
3159
3165
  }
@@ -3176,12 +3182,14 @@ var MobileServerManager = class {
3176
3182
  }
3177
3183
  async startExpo(tunnelUrl) {
3178
3184
  this.ensureLogDir();
3185
+ this.killProcessOnPort(this.expoPort);
3179
3186
  this.expoLogStream = createWriteStream(join5(this.logDir, "expo.log"));
3180
- this.expoProcess = spawn2("npx", ["expo", "start", "--port", String(this.expoPort)], {
3187
+ this.expoProcess = spawn2("npx", ["expo", "start", "--clear", "--port", String(this.expoPort)], {
3181
3188
  cwd: this.mobileProjectPath,
3182
3189
  env: {
3183
3190
  ...process.env,
3184
- EXPO_PACKAGER_PROXY_URL: tunnelUrl
3191
+ EXPO_PACKAGER_PROXY_URL: tunnelUrl,
3192
+ EXPO_PUBLIC_TUNNEL_URL: tunnelUrl
3185
3193
  },
3186
3194
  stdio: ["ignore", "pipe", "pipe"],
3187
3195
  detached: false
@@ -3259,13 +3267,13 @@ var MobileServerManager = class {
3259
3267
  return { started: false, error: "Failed to install mobile dependencies" };
3260
3268
  }
3261
3269
  }
3262
- this.onProgress("Syncing credentials...");
3263
- this.ensureEnvFile();
3264
3270
  this.onProgress("Starting ngrok tunnel...");
3265
3271
  const tunnelUrl = await this.startNgrok();
3266
3272
  if (!tunnelUrl) {
3267
3273
  return { started: false, error: this.ngrokError ?? "Failed to start ngrok tunnel" };
3268
3274
  }
3275
+ this.onProgress("Syncing credentials...");
3276
+ this.ensureEnvFile();
3269
3277
  this.onProgress("Starting Expo server...");
3270
3278
  const expoStarted = await this.startExpo(tunnelUrl);
3271
3279
  if (!expoStarted) {
@@ -3273,14 +3281,8 @@ var MobileServerManager = class {
3273
3281
  return { started: false, error: "Failed to start Expo server" };
3274
3282
  }
3275
3283
  const host = tunnelUrl.replace(/^https?:\/\//, "");
3276
- const expoUrl = `exp://${host}:443`;
3277
- console.log("");
3278
- console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
3279
- console.log(" \u2502 Expo Go is required to open this QR code. \u2502");
3280
- console.log(" \u2502 iOS: https://apps.apple.com/app/id982107779\u2502");
3281
- console.log(" \u2502 Android: https://play.google.com/store/apps/ \u2502");
3282
- console.log(" \u2502 details?id=host.exp.exponent \u2502");
3283
- console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
3284
+ const pairingParam = this.options.pairingCode ? `/--/pair?code=${this.options.pairingCode}` : "";
3285
+ const expoUrl = `exp://${host}:443${pairingParam}`;
3284
3286
  console.log("");
3285
3287
  console.log(" Scan with Expo Go:");
3286
3288
  qrcode.generate(expoUrl, { small: true }, (code) => {
@@ -3290,17 +3292,25 @@ var MobileServerManager = class {
3290
3292
  });
3291
3293
  console.log(` ${expoUrl}`);
3292
3294
  console.log("");
3295
+ console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
3296
+ console.log(" \u2502 Expo Go is required to open this QR code. \u2502");
3297
+ console.log(" \u2502 iOS: https://apps.apple.com/app/id982107779\u2502");
3298
+ console.log(" \u2502 Android: https://play.google.com/store/apps/ \u2502");
3299
+ console.log(" \u2502 details?id=host.exp.exponent \u2502");
3300
+ console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
3301
+ console.log("");
3293
3302
  return { started: true, tunnelUrl };
3294
3303
  }
3295
3304
  async stop() {
3296
3305
  if (this.expoProcess) {
3297
- this.killProcess(this.expoProcess);
3306
+ this.killProcessTree(this.expoProcess);
3298
3307
  this.expoProcess = null;
3299
3308
  }
3300
3309
  if (this.ngrokProcess) {
3301
- this.killProcess(this.ngrokProcess);
3310
+ this.killProcessTree(this.ngrokProcess);
3302
3311
  this.ngrokProcess = null;
3303
3312
  }
3313
+ this.killProcessOnPort(this.expoPort);
3304
3314
  if (this.expoLogStream) {
3305
3315
  this.expoLogStream.end();
3306
3316
  this.expoLogStream = null;
@@ -3344,15 +3354,21 @@ var MobileServerManager = class {
3344
3354
  });
3345
3355
  });
3346
3356
  }
3347
- killProcess(proc) {
3357
+ killProcessTree(proc) {
3358
+ const pid = proc.pid;
3359
+ if (!pid) return;
3348
3360
  try {
3349
3361
  proc.kill("SIGTERM");
3350
- setTimeout(() => {
3362
+ } catch {
3363
+ }
3364
+ try {
3365
+ const children = execSync2(`pgrep -P ${pid}`, { stdio: "pipe" }).toString().trim();
3366
+ for (const childPid of children.split("\n").filter(Boolean)) {
3351
3367
  try {
3352
- proc.kill("SIGKILL");
3368
+ process.kill(parseInt(childPid, 10), "SIGTERM");
3353
3369
  } catch {
3354
3370
  }
3355
- }, 3e3);
3371
+ }
3356
3372
  } catch {
3357
3373
  }
3358
3374
  }
@@ -3361,6 +3377,20 @@ var MobileServerManager = class {
3361
3377
  mkdirSync3(this.logDir, { recursive: true });
3362
3378
  }
3363
3379
  }
3380
+ killProcessOnPort(port) {
3381
+ try {
3382
+ const output = execSync2(`lsof -ti tcp:${port}`, { stdio: "pipe" }).toString().trim();
3383
+ if (output) {
3384
+ for (const pid of output.split("\n")) {
3385
+ try {
3386
+ process.kill(parseInt(pid, 10), "SIGTERM");
3387
+ } catch {
3388
+ }
3389
+ }
3390
+ }
3391
+ } catch {
3392
+ }
3393
+ }
3364
3394
  sleep(ms) {
3365
3395
  return new Promise((resolve3) => setTimeout(resolve3, ms));
3366
3396
  }
@@ -3631,11 +3661,23 @@ function createStartCommand() {
3631
3661
  }
3632
3662
  let mobileServer = null;
3633
3663
  if (options.mobile !== false) {
3664
+ const { data: { session: currentSession } } = await supabase.auth.getSession();
3665
+ const { data: pairingData, error: pairingError } = await supabase.functions.invoke("create-mobile-pairing", {
3666
+ headers: { Authorization: `Bearer ${currentSession?.access_token}` }
3667
+ });
3668
+ if (pairingError || !pairingData?.code) {
3669
+ spinner.fail("Failed to create mobile pairing code");
3670
+ logger.error(pairingError?.message ?? "No pairing code returned");
3671
+ removePidFile();
3672
+ process.exit(1);
3673
+ }
3674
+ const pairingCode = pairingData.code;
3634
3675
  const mobileProjectPath = config2.getMobileProjectPath();
3635
3676
  mobileServer = new MobileServerManager({
3636
3677
  mobileProjectPath,
3637
3678
  supabaseUrl: config2.getSupabaseUrl(),
3638
3679
  supabaseAnonKey: config2.getSupabaseAnonKey(),
3680
+ pairingCode,
3639
3681
  onProgress: (msg) => spinner.update(msg)
3640
3682
  });
3641
3683
  const result = await mobileServer.start();
@@ -4069,7 +4111,7 @@ function createLoginCommand() {
4069
4111
  import { Command as Command5 } from "commander";
4070
4112
  function createLogoutCommand() {
4071
4113
  const command = new Command5("logout");
4072
- command.description("Log out of ClauTunnel").action(async () => {
4114
+ command.description("Log out of ClauTunnel and revoke all device sessions").action(async () => {
4073
4115
  const config2 = new Config();
4074
4116
  const logger = new Logger();
4075
4117
  const session = config2.getSessionTokens();
@@ -4077,7 +4119,26 @@ function createLogoutCommand() {
4077
4119
  logger.info("Not currently logged in.");
4078
4120
  return;
4079
4121
  }
4122
+ try {
4123
+ const supabase = createSupabaseClient(
4124
+ config2.getSupabaseUrl(),
4125
+ config2.getSupabaseAnonKey()
4126
+ );
4127
+ const restored = await restoreSession(supabase, config2);
4128
+ if (restored) {
4129
+ const { error } = await supabase.auth.signOut({ scope: "global" });
4130
+ if (error) {
4131
+ logger.warn(`Warning: failed to revoke remote sessions: ${error.message}`);
4132
+ logger.warn("Local credentials cleared, but mobile devices may remain active until their tokens expire.");
4133
+ } else {
4134
+ logger.info("All device sessions revoked.");
4135
+ }
4136
+ }
4137
+ } catch {
4138
+ logger.warn("Warning: could not reach server to revoke sessions.");
4139
+ }
4080
4140
  config2.clearSessionTokens();
4141
+ config2.clearMachineId();
4081
4142
  logger.info("Logged out successfully.");
4082
4143
  });
4083
4144
  return command;
@@ -4276,15 +4337,246 @@ function createMobileSetupCommand() {
4276
4337
  return command;
4277
4338
  }
4278
4339
 
4340
+ // src/commands/reset.ts
4341
+ import { Command as Command9 } from "commander";
4342
+ import * as fs4 from "fs";
4343
+ import * as path4 from "path";
4344
+ import * as os5 from "os";
4345
+ import { execSync as execSync4 } from "child_process";
4346
+ function createResetCommand() {
4347
+ const command = new Command9("reset");
4348
+ command.description("Reset to fresh user state (uninstall CLI, clean config & DB)").option("--skip-db", "Skip Supabase DB cleanup").option("--skip-ngrok", "Keep ngrok installed and configured").option("-y, --yes", "Skip confirmation prompt").action(async (options) => {
4349
+ const logger = new Logger();
4350
+ if (!options.yes) {
4351
+ logger.info("This will:");
4352
+ logger.info(" - Stop any running clautunnel processes");
4353
+ logger.info(" - Restore macOS sleep settings");
4354
+ if (!options.skipDb) {
4355
+ logger.info(" - Delete all your data from Supabase (machines, sessions, messages)");
4356
+ }
4357
+ if (!options.skipNgrok) {
4358
+ logger.info(" - Uninstall ngrok and remove its config");
4359
+ }
4360
+ logger.info(" - Delete ~/.clautunnel/ config directory");
4361
+ logger.info(" - Uninstall clautunnel (npm & Homebrew)");
4362
+ logger.info("");
4363
+ const confirmed = await promptYesNo("Are you sure? [y/N]: ");
4364
+ if (!confirmed) {
4365
+ logger.info("Aborted.");
4366
+ return;
4367
+ }
4368
+ logger.info("");
4369
+ }
4370
+ logger.info("[1/7] Stopping running processes...");
4371
+ const pid = readPidFile();
4372
+ if (pid !== null && isProcessAlive(pid)) {
4373
+ try {
4374
+ process.kill(pid, "SIGTERM");
4375
+ logger.info(` - clautunnel daemon stopped (PID ${pid})`);
4376
+ await new Promise((resolve3) => setTimeout(resolve3, 1e3));
4377
+ } catch {
4378
+ logger.info(" - could not stop daemon");
4379
+ }
4380
+ } else {
4381
+ logger.info(" - no running daemon");
4382
+ }
4383
+ try {
4384
+ fs4.unlinkSync(PID_FILE);
4385
+ } catch {
4386
+ }
4387
+ try {
4388
+ execSync4('pkill -f "ngrok.*tunnel" 2>/dev/null', { stdio: "ignore" });
4389
+ logger.info(" - ngrok tunnel process killed");
4390
+ } catch {
4391
+ }
4392
+ try {
4393
+ execSync4('pkill -f "expo start" 2>/dev/null', { stdio: "ignore" });
4394
+ logger.info(" - expo process killed");
4395
+ } catch {
4396
+ }
4397
+ try {
4398
+ const pids = execSync4("lsof -ti tcp:8081", { stdio: "pipe" }).toString().trim();
4399
+ if (pids) {
4400
+ for (const pid2 of pids.split("\n")) {
4401
+ try {
4402
+ process.kill(parseInt(pid2, 10), "SIGTERM");
4403
+ } catch {
4404
+ }
4405
+ }
4406
+ logger.info(" - port 8081 freed");
4407
+ }
4408
+ } catch {
4409
+ }
4410
+ logger.info("[2/7] Restoring macOS sleep settings...");
4411
+ if (isMacOS()) {
4412
+ try {
4413
+ const pmsetOutput = execSync4("sudo pmset -g 2>/dev/null", { encoding: "utf-8" });
4414
+ if (pmsetOutput.includes("disablesleep") && pmsetOutput.includes("1")) {
4415
+ execSync4("sudo pmset -a disablesleep 0", { stdio: "inherit" });
4416
+ logger.info(" - lid-close sleep restored");
4417
+ } else {
4418
+ logger.info(" - already normal");
4419
+ }
4420
+ } catch {
4421
+ logger.info(" - already normal");
4422
+ }
4423
+ try {
4424
+ execSync4("pkill -f caffeinate 2>/dev/null", { stdio: "ignore" });
4425
+ logger.info(" - caffeinate stopped");
4426
+ } catch {
4427
+ }
4428
+ } else {
4429
+ logger.info(" - not macOS, skipped");
4430
+ }
4431
+ if (options.skipDb) {
4432
+ logger.info("[3/7] Skipping DB cleanup (--skip-db)");
4433
+ } else {
4434
+ await cleanSupabaseDb(logger);
4435
+ }
4436
+ if (options.skipNgrok) {
4437
+ logger.info("[4/7] Skipping ngrok cleanup (--skip-ngrok)");
4438
+ } else {
4439
+ logger.info("[4/7] Uninstalling ngrok...");
4440
+ try {
4441
+ execSync4("brew list ngrok", { stdio: "ignore" });
4442
+ execSync4("brew uninstall ngrok", { stdio: "inherit" });
4443
+ logger.info(" - ngrok removed");
4444
+ } catch {
4445
+ try {
4446
+ execSync4("which ngrok", { stdio: "ignore" });
4447
+ logger.info(" - ngrok found but not installed via Homebrew, remove manually");
4448
+ } catch {
4449
+ logger.info(" - not installed, skipped");
4450
+ }
4451
+ }
4452
+ const ngrokConfigDir = path4.join(os5.homedir(), ".config", "ngrok");
4453
+ const ngrokLegacyDir = path4.join(os5.homedir(), ".ngrok2");
4454
+ if (fs4.existsSync(ngrokConfigDir)) {
4455
+ fs4.rmSync(ngrokConfigDir, { recursive: true, force: true });
4456
+ logger.info(" - ngrok config removed (~/.config/ngrok)");
4457
+ }
4458
+ if (fs4.existsSync(ngrokLegacyDir)) {
4459
+ fs4.rmSync(ngrokLegacyDir, { recursive: true, force: true });
4460
+ logger.info(" - ngrok legacy config removed (~/.ngrok2)");
4461
+ }
4462
+ }
4463
+ logger.info("[5/7] Removing local data...");
4464
+ const configDir = path4.join(os5.homedir(), ".clautunnel");
4465
+ const legacyDir = path4.join(os5.homedir(), ".termbridge");
4466
+ if (fs4.existsSync(configDir)) {
4467
+ fs4.rmSync(configDir, { recursive: true, force: true });
4468
+ logger.info(" - ~/.clautunnel removed (config, logs, repo)");
4469
+ } else {
4470
+ logger.info(" - ~/.clautunnel already clean");
4471
+ }
4472
+ if (fs4.existsSync(legacyDir)) {
4473
+ fs4.rmSync(legacyDir, { recursive: true, force: true });
4474
+ logger.info(" - ~/.termbridge removed (legacy)");
4475
+ }
4476
+ logger.info("[6/7] Uninstalling CLI (npm)...");
4477
+ try {
4478
+ execSync4("npm list -g @tongil_kim/clautunnel", { stdio: "ignore" });
4479
+ execSync4("npm uninstall -g @tongil_kim/clautunnel", { stdio: "inherit" });
4480
+ logger.info(" - npm package removed");
4481
+ } catch {
4482
+ logger.info(" - not installed via npm, skipped");
4483
+ }
4484
+ logger.info("[7/7] Uninstalling CLI (Homebrew)...");
4485
+ try {
4486
+ execSync4("brew list clautunnel", { stdio: "ignore" });
4487
+ execSync4("brew uninstall clautunnel", { stdio: "inherit" });
4488
+ logger.info(" - Homebrew package removed");
4489
+ } catch {
4490
+ logger.info(" - not installed via Homebrew, skipped");
4491
+ }
4492
+ logger.info("");
4493
+ logger.info("Done! Fresh user state restored.");
4494
+ logger.info("");
4495
+ logger.info("Next steps:");
4496
+ logger.info(" 1. npm install -g @tongil_kim/clautunnel");
4497
+ logger.info(" 2. clautunnel setup");
4498
+ logger.info(" 3. clautunnel login");
4499
+ logger.info(" 4. clautunnel start");
4500
+ });
4501
+ return command;
4502
+ }
4503
+ async function cleanSupabaseDb(logger) {
4504
+ const configDir = path4.join(os5.homedir(), ".clautunnel");
4505
+ const configFile = path4.join(configDir, "config.json");
4506
+ if (!fs4.existsSync(configFile)) {
4507
+ logger.info("[3/7] Skipping DB cleanup (no config file found)");
4508
+ return;
4509
+ }
4510
+ let configData;
4511
+ try {
4512
+ configData = JSON.parse(fs4.readFileSync(configFile, "utf-8"));
4513
+ } catch {
4514
+ logger.info("[3/7] Skipping DB cleanup (invalid config file)");
4515
+ return;
4516
+ }
4517
+ const supabaseUrl = configData.supabaseUrl;
4518
+ const anonKey = configData.supabaseAnonKey;
4519
+ const accessToken = configData.sessionTokens?.accessToken;
4520
+ const refreshToken = configData.sessionTokens?.refreshToken;
4521
+ if (!supabaseUrl || !anonKey || !accessToken) {
4522
+ logger.info("[3/7] Skipping DB cleanup (missing credentials)");
4523
+ return;
4524
+ }
4525
+ logger.info("[3/7] Cleaning Supabase DB data...");
4526
+ const supabase = createSupabaseClient(supabaseUrl, anonKey);
4527
+ let token = accessToken;
4528
+ if (refreshToken) {
4529
+ const { data } = await supabase.auth.setSession({
4530
+ access_token: accessToken,
4531
+ refresh_token: refreshToken
4532
+ });
4533
+ if (data?.session) {
4534
+ token = data.session.access_token;
4535
+ }
4536
+ }
4537
+ const headers = {
4538
+ apikey: anonKey,
4539
+ Authorization: `Bearer ${token}`,
4540
+ Prefer: "return=minimal"
4541
+ };
4542
+ try {
4543
+ const res = await fetch(`${supabaseUrl}/rest/v1/push_tokens?select=*`, {
4544
+ method: "DELETE",
4545
+ headers
4546
+ });
4547
+ logger.info(res.ok ? " - push_tokens cleared" : " - push_tokens: skipped");
4548
+ } catch {
4549
+ logger.info(" - push_tokens: skipped");
4550
+ }
4551
+ try {
4552
+ const res = await fetch(`${supabaseUrl}/rest/v1/machines?select=*`, {
4553
+ method: "DELETE",
4554
+ headers
4555
+ });
4556
+ logger.info(res.ok ? " - machines cleared (sessions + messages cascade)" : " - machines: skipped");
4557
+ } catch {
4558
+ logger.info(" - machines: skipped");
4559
+ }
4560
+ try {
4561
+ const res = await fetch(`${supabaseUrl}/rest/v1/mobile_pairings?select=*`, {
4562
+ method: "DELETE",
4563
+ headers
4564
+ });
4565
+ logger.info(res.ok ? " - mobile_pairings cleared" : " - mobile_pairings: skipped");
4566
+ } catch {
4567
+ logger.info(" - mobile_pairings: skipped");
4568
+ }
4569
+ }
4570
+
4279
4571
  // src/index.ts
4280
4572
  var __filename = fileURLToPath(import.meta.url);
4281
4573
  var __dirname = dirname3(__filename);
4282
4574
  config({ path: resolve2(__dirname, "../.env"), quiet: true });
4283
4575
  var packageJson = JSON.parse(
4284
- readFileSync5(resolve2(__dirname, "../package.json"), "utf-8")
4576
+ readFileSync6(resolve2(__dirname, "../package.json"), "utf-8")
4285
4577
  );
4286
4578
  var version = packageJson.version || "0.0.0";
4287
- var program = new Command9();
4579
+ var program = new Command10();
4288
4580
  program.name("clautunnel").description("Remote control for Claude Code CLI").version(version);
4289
4581
  program.addCommand(createSetupCommand());
4290
4582
  program.addCommand(createStartCommand());
@@ -4294,6 +4586,7 @@ program.addCommand(createLoginCommand());
4294
4586
  program.addCommand(createLogoutCommand());
4295
4587
  program.addCommand(createSignupCommand());
4296
4588
  program.addCommand(createMobileSetupCommand());
4589
+ program.addCommand(createResetCommand());
4297
4590
  if (process.argv[1]?.includes("clautunnel") || process.argv[1]?.endsWith("/index.js") || process.argv[1]?.endsWith("/index.ts")) {
4298
4591
  program.parse();
4299
4592
  }