@tongil_kim/clautunnel 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,15 +26,21 @@ clautunnel setup
26
26
  ```
27
27
 
28
28
  You'll need:
29
- - **Supabase Project URL**: Dashboard → Settings → API (e.g., `https://xxxx.supabase.co`)
30
- - **Supabase Anon Key**: Dashboard → Settings → API → `anon` `public` key
29
+ - **Supabase Project ID**: Dashboard → Settings → General Project ID
30
+ - **Supabase Anon Key**: Dashboard → Settings → API Keys Legacy anon Tab → Copy anon key
31
31
 
32
32
  ## Usage
33
33
 
34
34
  ```bash
35
- # Authenticate
35
+ # Create account (first time)
36
+ clautunnel signup
37
+
38
+ # Login (returning user)
36
39
  clautunnel login
37
40
 
41
+ # Logout
42
+ clautunnel logout
43
+
38
44
  # Start a session
39
45
  clautunnel start
40
46
 
package/dist/index.js CHANGED
@@ -155,7 +155,7 @@ var require_dist = __commonJS({
155
155
 
156
156
  // src/index.ts
157
157
  import { config } from "dotenv";
158
- import { resolve, dirname as dirname2 } from "path";
158
+ import { resolve as resolve2, dirname as dirname2 } from "path";
159
159
  import { readFileSync as readFileSync5 } from "fs";
160
160
  import { fileURLToPath } from "url";
161
161
 
@@ -236,7 +236,7 @@ var Config = class {
236
236
  requireConfiguration() {
237
237
  if (!this.isConfigured()) {
238
238
  throw new ConfigurationError(
239
- 'ClauTunnel is not configured.\n\nPlease run "clautunnel setup" to configure your Supabase credentials,\nor set the following environment variables:\n - SUPABASE_URL\n - SUPABASE_ANON_KEY'
239
+ 'ClauTunnel is not configured.\n\nRun "clautunnel setup" to configure your Supabase credentials.\n\nOr set environment variables in your shell profile (~/.zshrc or ~/.bashrc):\n export SUPABASE_URL=https://<project-id>.supabase.co\n export SUPABASE_ANON_KEY=<your-anon-key>'
240
240
  );
241
241
  }
242
242
  }
@@ -324,26 +324,28 @@ import { EventEmitter } from "events";
324
324
  // src/realtime/utils.ts
325
325
  var DEFAULT_TIMEOUT = 1e4;
326
326
  function subscribeWithTimeout(channel, channelName, timeout = DEFAULT_TIMEOUT) {
327
- return new Promise((resolve2) => {
327
+ return new Promise((resolve3) => {
328
+ let lastStatus = "unknown";
328
329
  const timer = setTimeout(() => {
329
330
  console.warn(
330
- `[WARN] Realtime subscription timeout for ${channelName}. Mobile sync disabled.`
331
+ `[WARN] Realtime subscription timeout for ${channelName} (last status: ${lastStatus}).`
331
332
  );
332
- resolve2(false);
333
+ resolve3(false);
333
334
  }, timeout);
334
335
  channel.subscribe((status, err) => {
336
+ lastStatus = status;
335
337
  if (status === "SUBSCRIBED") {
336
338
  clearTimeout(timer);
337
- resolve2(true);
339
+ resolve3(true);
338
340
  } else if (status === "CHANNEL_ERROR" || status === "CLOSED" || status === "TIMED_OUT") {
339
341
  clearTimeout(timer);
340
342
  console.warn(
341
- `[WARN] Channel ${channelName} ${status.toLowerCase()}. Mobile sync disabled.`
343
+ `[WARN] Channel ${channelName} ${status.toLowerCase()}.`
342
344
  );
343
345
  if (err) {
344
346
  console.warn(`[WARN] Error details: ${err.message || err}`);
345
347
  }
346
- resolve2(false);
348
+ resolve3(false);
347
349
  }
348
350
  });
349
351
  });
@@ -1042,8 +1044,8 @@ var SdkSession = class extends EventEmitter2 {
1042
1044
  this.pendingQuestionData = questionData;
1043
1045
  this.emit("user-question", questionData);
1044
1046
  const answers = await new Promise(
1045
- (resolve2, reject) => {
1046
- this.pendingAnswerResolve = resolve2;
1047
+ (resolve3, reject) => {
1048
+ this.pendingAnswerResolve = resolve3;
1047
1049
  options.signal.addEventListener("abort", () => {
1048
1050
  this.pendingAnswerResolve = null;
1049
1051
  this.pendingQuestionData = null;
@@ -1098,9 +1100,9 @@ var SdkSession = class extends EventEmitter2 {
1098
1100
  };
1099
1101
  this.pendingPermissionData = requestData;
1100
1102
  this.emit("permission-request", requestData);
1101
- return new Promise((resolve2, reject) => {
1103
+ return new Promise((resolve3, reject) => {
1102
1104
  this.pendingPermissionRequests.set(requestId, {
1103
- resolve: resolve2,
1105
+ resolve: resolve3,
1104
1106
  reject,
1105
1107
  signal: options.signal
1106
1108
  });
@@ -2301,7 +2303,7 @@ ${confirmationMsg}
2301
2303
  };
2302
2304
 
2303
2305
  // src/index.ts
2304
- import { Command as Command6 } from "commander";
2306
+ import { Command as Command9 } from "commander";
2305
2307
 
2306
2308
  // src/commands/start.ts
2307
2309
  import { Command } from "commander";
@@ -2501,10 +2503,10 @@ async function promptYesNo(question) {
2501
2503
  input: process.stdin,
2502
2504
  output: process.stdout
2503
2505
  });
2504
- return new Promise((resolve2) => {
2506
+ return new Promise((resolve3) => {
2505
2507
  rl.question(question, (answer) => {
2506
2508
  rl.close();
2507
- resolve2(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
2509
+ resolve3(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
2508
2510
  });
2509
2511
  });
2510
2512
  }
@@ -2628,10 +2630,13 @@ function createStartCommand() {
2628
2630
  const session = await restoreSession(supabase, config2);
2629
2631
  if (!session) {
2630
2632
  spinner.fail("Not authenticated");
2631
- logger.error('Run "clautunnel login" first.');
2633
+ logger.error(
2634
+ 'Run "clautunnel login" or "clautunnel signup" first.'
2635
+ );
2632
2636
  process.exit(1);
2633
2637
  }
2634
2638
  const { user } = session;
2639
+ spinner.update(`Authenticated as ${user.email}...`);
2635
2640
  let fdaStatus = null;
2636
2641
  if (isMacOS()) {
2637
2642
  spinner.stop();
@@ -2754,9 +2759,12 @@ function createStartCommand() {
2754
2759
  const connected = await machineClient.connect();
2755
2760
  spinner.stop();
2756
2761
  if (!connected) {
2757
- logger.error(
2758
- "Failed to connect to realtime. Check your network connection."
2759
- );
2762
+ logger.error("Failed to connect to Supabase Realtime.");
2763
+ logger.error("");
2764
+ logger.error("This may be a temporary issue. Try the following:");
2765
+ logger.error(' 1. Open a new terminal and run "clautunnel start" again');
2766
+ logger.error(" 2. Check your network connection");
2767
+ logger.error(' 3. Try "clautunnel login" to refresh your session');
2760
2768
  process.exit(1);
2761
2769
  }
2762
2770
  logger.info("");
@@ -2897,7 +2905,7 @@ function createStopCommand() {
2897
2905
  logger.info(`Sent stop signal to daemon (PID: ${pid})`);
2898
2906
  let attempts = 0;
2899
2907
  while (attempts < 10) {
2900
- await new Promise((resolve2) => setTimeout(resolve2, 500));
2908
+ await new Promise((resolve3) => setTimeout(resolve3, 500));
2901
2909
  try {
2902
2910
  process.kill(pid, 0);
2903
2911
  attempts++;
@@ -2985,10 +2993,10 @@ function prompt(question) {
2985
2993
  input: process.stdin,
2986
2994
  output: process.stdout
2987
2995
  });
2988
- return new Promise((resolve2) => {
2996
+ return new Promise((resolve3) => {
2989
2997
  rl.question(question, (answer) => {
2990
2998
  rl.close();
2991
- resolve2(answer);
2999
+ resolve3(answer);
2992
3000
  });
2993
3001
  });
2994
3002
  }
@@ -2997,7 +3005,7 @@ function promptHidden(question) {
2997
3005
  input: process.stdin,
2998
3006
  output: process.stdout
2999
3007
  });
3000
- return new Promise((resolve2) => {
3008
+ return new Promise((resolve3) => {
3001
3009
  process.stdout.write(question);
3002
3010
  if (process.stdin.isTTY) {
3003
3011
  process.stdin.setRawMode(true);
@@ -3012,7 +3020,7 @@ function promptHidden(question) {
3012
3020
  }
3013
3021
  process.stdout.write("\n");
3014
3022
  rl.close();
3015
- resolve2(password);
3023
+ resolve3(password);
3016
3024
  } else if (c === "") {
3017
3025
  process.exit(0);
3018
3026
  } else if (c === "\x7F" || c === "\b") {
@@ -3063,6 +3071,11 @@ function createLoginCommand() {
3063
3071
  });
3064
3072
  logger.info("Session saved");
3065
3073
  }
3074
+ logger.info("");
3075
+ logger.info("Next steps:");
3076
+ logger.info(' 1. Run "clautunnel start" to begin a session');
3077
+ logger.info(" 2. Set up the mobile app:");
3078
+ logger.info(" https://github.com/TongilKim/ClauTunnel#mobile-app-setup");
3066
3079
  }
3067
3080
  } catch (error) {
3068
3081
  if (error instanceof ConfigurationError) {
@@ -3078,10 +3091,100 @@ function createLoginCommand() {
3078
3091
  return command;
3079
3092
  }
3080
3093
 
3081
- // src/commands/setup.ts
3094
+ // src/commands/logout.ts
3082
3095
  import { Command as Command5 } from "commander";
3096
+ function createLogoutCommand() {
3097
+ const command = new Command5("logout");
3098
+ command.description("Log out of ClauTunnel").action(async () => {
3099
+ const config2 = new Config();
3100
+ const logger = new Logger();
3101
+ const session = config2.getSessionTokens();
3102
+ if (!session) {
3103
+ logger.info("Not currently logged in.");
3104
+ return;
3105
+ }
3106
+ config2.clearSessionTokens();
3107
+ logger.info("Logged out successfully.");
3108
+ });
3109
+ return command;
3110
+ }
3111
+
3112
+ // src/commands/signup.ts
3113
+ import { Command as Command6 } from "commander";
3114
+ function createSignupCommand() {
3115
+ const command = new Command6("signup");
3116
+ command.description("Create a new ClauTunnel account").action(async () => {
3117
+ const config2 = new Config();
3118
+ const logger = new Logger();
3119
+ try {
3120
+ config2.requireConfiguration();
3121
+ const supabase = createSupabaseClient(
3122
+ config2.getSupabaseUrl(),
3123
+ config2.getSupabaseAnonKey()
3124
+ );
3125
+ logger.info("Create a new ClauTunnel account");
3126
+ logger.info("");
3127
+ const email = await prompt("Email: ");
3128
+ if (!email) {
3129
+ logger.error("Email is required");
3130
+ process.exit(1);
3131
+ }
3132
+ const password = await promptHidden("Password: ");
3133
+ if (!password) {
3134
+ logger.error("Password is required");
3135
+ process.exit(1);
3136
+ }
3137
+ if (password.length < 6) {
3138
+ logger.error("Password must be at least 6 characters");
3139
+ process.exit(1);
3140
+ }
3141
+ const confirmPassword = await promptHidden("Confirm Password: ");
3142
+ if (password !== confirmPassword) {
3143
+ logger.error("Passwords do not match");
3144
+ process.exit(1);
3145
+ }
3146
+ const { data, error } = await supabase.auth.signUp({
3147
+ email,
3148
+ password
3149
+ });
3150
+ if (error) {
3151
+ logger.error(`Signup failed: ${error.message}`);
3152
+ process.exit(1);
3153
+ }
3154
+ if (data.user) {
3155
+ logger.info("");
3156
+ logger.info(`Account created for ${data.user.email}`);
3157
+ if (data.session) {
3158
+ config2.setSession({
3159
+ accessToken: data.session.access_token,
3160
+ refreshToken: data.session.refresh_token
3161
+ });
3162
+ logger.info("Logged in automatically");
3163
+ }
3164
+ logger.info("");
3165
+ logger.info("Next steps:");
3166
+ logger.info(' 1. Run "clautunnel start" to begin a session');
3167
+ logger.info(" 2. Set up the mobile app:");
3168
+ logger.info(" https://github.com/TongilKim/ClauTunnel#mobile-app-setup");
3169
+ }
3170
+ } catch (error) {
3171
+ if (error instanceof ConfigurationError) {
3172
+ logger.error(error.message);
3173
+ process.exit(1);
3174
+ }
3175
+ logger.error(
3176
+ `Signup failed: ${error instanceof Error ? error.message : "Unknown error"}`
3177
+ );
3178
+ process.exit(1);
3179
+ }
3180
+ });
3181
+ return command;
3182
+ }
3183
+
3184
+ // src/commands/setup.ts
3185
+ import { Command as Command7 } from "commander";
3083
3186
  function createSetupCommand() {
3084
- const command = new Command5("setup");
3187
+ const command = new Command7("setup");
3085
3188
  command.description("Configure ClauTunnel with Supabase credentials").action(async () => {
3086
3189
  const config2 = new Config();
3087
3190
  const logger = new Logger();
@@ -3089,43 +3192,41 @@ function createSetupCommand() {
3089
3192
  logger.info("ClauTunnel Setup");
3090
3193
  logger.info("================");
3091
3194
  logger.info("");
3092
- logger.info("Enter your Supabase credentials to connect ClauTunnel.");
3195
+ logger.info("[Step 1/2] Supabase Project ID");
3093
3196
  logger.info("");
3094
- logger.info("To find your credentials:");
3095
3197
  logger.info(" 1. Go to your Supabase project dashboard");
3096
- logger.info(' 2. Settings > General > Copy "Project URL"');
3097
- logger.info(' 3. Settings > API Keys > Copy "anon public" key');
3198
+ logger.info(' 2. Settings > General > Copy "Project ID"');
3098
3199
  logger.info("");
3099
- const url = await prompt("Supabase Project URL (e.g., https://xxxx.supabase.co): ");
3100
- if (!url) {
3101
- logger.error("Supabase URL is required");
3200
+ const projectId = await prompt("Project ID: ");
3201
+ if (!projectId) {
3202
+ logger.error("Supabase Project ID is required");
3102
3203
  process.exit(1);
3103
3204
  }
3104
- try {
3105
- const parsedUrl = new URL(url);
3106
- if (parsedUrl.hostname === "supabase.com") {
3107
- logger.error("");
3108
- logger.error("This looks like a dashboard URL, not the API URL.");
3109
- logger.error("");
3110
- logger.error("Please use the Project URL from Settings > API, which looks like:");
3111
- logger.error(" https://your-project-id.supabase.co");
3112
- logger.error("");
3113
- logger.error("NOT the dashboard URL:");
3114
- logger.error(" https://supabase.com/dashboard/project/...");
3115
- process.exit(1);
3116
- }
3117
- if (!parsedUrl.hostname.endsWith(".supabase.co")) {
3118
- logger.error("");
3119
- logger.error("Invalid Supabase URL format.");
3120
- logger.error("The URL should end with .supabase.co");
3121
- logger.error("Example: https://your-project-id.supabase.co");
3122
- process.exit(1);
3123
- }
3124
- } catch {
3125
- logger.error("Invalid URL format. Please enter a valid URL (e.g., https://xxxx.supabase.co)");
3205
+ if (projectId.includes("supabase.co") || projectId.startsWith("http")) {
3206
+ logger.error("");
3207
+ logger.error("Please enter only the Project ID, not the full URL.");
3208
+ logger.error("");
3209
+ logger.error("Example: abcdefghijklmnop");
3210
+ logger.error("NOT: https://abcdefghijklmnop.supabase.co");
3211
+ process.exit(1);
3212
+ }
3213
+ if (!/^[a-zA-Z0-9-]+$/.test(projectId)) {
3214
+ logger.error("");
3215
+ logger.error("Invalid Project ID format.");
3216
+ logger.error("The Project ID should only contain letters, numbers, and hyphens.");
3217
+ logger.error("");
3218
+ logger.error("You can find it at: Settings > General > Project ID");
3126
3219
  process.exit(1);
3127
3220
  }
3128
- const anonKey = await prompt("Supabase Anon Key: ");
3221
+ const url = `https://${projectId}.supabase.co`;
3222
+ logger.info("\u2713 Project ID saved");
3223
+ logger.info("");
3224
+ logger.info("[Step 2/2] Supabase Anon Key");
3225
+ logger.info("");
3226
+ logger.info(" 1. Go to your Supabase project dashboard");
3227
+ logger.info(" 2. Settings > API Keys > Legacy anon Tab > Copy anon key");
3228
+ logger.info("");
3229
+ const anonKey = await prompt("Anon Key: ");
3129
3230
  if (!anonKey) {
3130
3231
  logger.error("Supabase Anon Key is required");
3131
3232
  process.exit(1);
@@ -3135,8 +3236,8 @@ function createSetupCommand() {
3135
3236
  logger.info("\u2713 Configuration saved successfully!");
3136
3237
  logger.info("");
3137
3238
  logger.info("Next steps:");
3138
- logger.info(' 1. Run "clautunnel login" to authenticate');
3139
- logger.info(' 2. Run "clautunnel start" to begin a session');
3239
+ logger.info(' - New user? Run "clautunnel signup" to create an account');
3240
+ logger.info(' - Have account? Run "clautunnel login" to authenticate');
3140
3241
  } catch (error) {
3141
3242
  logger.error(
3142
3243
  `Setup failed: ${error instanceof Error ? error.message : "Unknown error"}`
@@ -3147,21 +3248,78 @@ function createSetupCommand() {
3147
3248
  return command;
3148
3249
  }
3149
3250
 
3251
+ // src/commands/mobile-setup.ts
3252
+ import { Command as Command8 } from "commander";
3253
+ import { existsSync as existsSync5, writeFileSync as writeFileSync3 } from "fs";
3254
+ import { join as join6, resolve } from "path";
3255
+ function createMobileSetupCommand() {
3256
+ const command = new Command8("mobile-setup");
3257
+ command.description("Generate mobile app .env file from CLI credentials").action(async () => {
3258
+ const config2 = new Config();
3259
+ const logger = new Logger();
3260
+ try {
3261
+ config2.requireConfiguration();
3262
+ const supabaseUrl = config2.getSupabaseUrl();
3263
+ const supabaseAnonKey = config2.getSupabaseAnonKey();
3264
+ const mobileDir = resolve(process.cwd(), "apps", "mobile");
3265
+ if (!existsSync5(mobileDir)) {
3266
+ logger.error("Could not find apps/mobile directory.");
3267
+ logger.error("");
3268
+ logger.error("Make sure you run this command from the ClauTunnel project root:");
3269
+ logger.error(" cd clautunnel");
3270
+ logger.error(" clautunnel mobile-setup");
3271
+ process.exit(1);
3272
+ }
3273
+ const envPath = join6(mobileDir, ".env");
3274
+ if (existsSync5(envPath)) {
3275
+ logger.warn("apps/mobile/.env already exists and will be overwritten.");
3276
+ }
3277
+ const envContent = [
3278
+ `EXPO_PUBLIC_SUPABASE_URL=${supabaseUrl}`,
3279
+ `EXPO_PUBLIC_SUPABASE_ANON_KEY=${supabaseAnonKey}`,
3280
+ ""
3281
+ ].join("\n");
3282
+ writeFileSync3(envPath, envContent);
3283
+ logger.info("");
3284
+ logger.info("Mobile app .env file created successfully!");
3285
+ logger.info(` ${envPath}`);
3286
+ logger.info("");
3287
+ logger.info("Next steps:");
3288
+ logger.info(" 1. cd apps/mobile");
3289
+ logger.info(" 2. pnpm start");
3290
+ logger.info(" 3. Scan the QR code with Expo Go");
3291
+ } catch (error) {
3292
+ if (error instanceof ConfigurationError) {
3293
+ logger.error(error.message);
3294
+ process.exit(1);
3295
+ }
3296
+ logger.error(
3297
+ `Mobile setup failed: ${error instanceof Error ? error.message : "Unknown error"}`
3298
+ );
3299
+ process.exit(1);
3300
+ }
3301
+ });
3302
+ return command;
3303
+ }
3304
+
3150
3305
  // src/index.ts
3151
3306
  var __filename = fileURLToPath(import.meta.url);
3152
3307
  var __dirname = dirname2(__filename);
3153
- config({ path: resolve(__dirname, "../.env"), quiet: true });
3308
+ config({ path: resolve2(__dirname, "../.env"), quiet: true });
3154
3309
  var packageJson = JSON.parse(
3155
- readFileSync5(resolve(__dirname, "../package.json"), "utf-8")
3310
+ readFileSync5(resolve2(__dirname, "../package.json"), "utf-8")
3156
3311
  );
3157
3312
  var version = packageJson.version || "0.0.0";
3158
- var program = new Command6();
3313
+ var program = new Command9();
3159
3314
  program.name("clautunnel").description("Remote control for Claude Code CLI").version(version);
3160
3315
  program.addCommand(createSetupCommand());
3161
3316
  program.addCommand(createStartCommand());
3162
3317
  program.addCommand(createStopCommand());
3163
3318
  program.addCommand(createStatusCommand());
3164
3319
  program.addCommand(createLoginCommand());
3320
+ program.addCommand(createLogoutCommand());
3321
+ program.addCommand(createSignupCommand());
3322
+ program.addCommand(createMobileSetupCommand());
3165
3323
  if (process.argv[1]?.includes("clautunnel") || process.argv[1]?.endsWith("/index.js") || process.argv[1]?.endsWith("/index.ts")) {
3166
3324
  program.parse();
3167
3325
  }