@tongil_kim/clautunnel 1.5.1 → 1.6.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.
package/dist/index.js CHANGED
@@ -1021,6 +1021,9 @@ import * as os2 from "os";
1021
1021
  import { unstable_v2_createSession, unstable_v2_resumeSession } from "@anthropic-ai/claude-agent-sdk";
1022
1022
  import { v4 as uuidv4 } from "uuid";
1023
1023
  var MAX_TOOL_CONTENT = 1e4;
1024
+ var COMPACT_FALLBACK_GRACE_MS = 40;
1025
+ var COMPACT_QUEUED_DISPATCH_DELAY_MS = 40;
1026
+ var COMPACT_STALE_RESULT_GUARD_MS = COMPACT_FALLBACK_GRACE_MS + COMPACT_QUEUED_DISPATCH_DELAY_MS;
1024
1027
  var UNSUPPORTED_COMMANDS = /* @__PURE__ */ new Set([
1025
1028
  "keybindings-help",
1026
1029
  "help",
@@ -1058,6 +1061,20 @@ var SdkSession = class extends EventEmitter2 {
1058
1061
  pendingPrompt = null;
1059
1062
  // Tracks prompt during a resume attempt — used to auto-retry with a fresh session on failure
1060
1063
  resumeAttemptPrompt = null;
1064
+ // Tracks whether the current in-flight prompt is a manual /compact command.
1065
+ awaitingCompactCompletionFallback = false;
1066
+ // Monotonic counter for turn ordering.
1067
+ turnGeneration = 0;
1068
+ // Generation of the current in-flight turn.
1069
+ activeTurnGeneration = 0;
1070
+ // Timestamp when the current in-flight turn started.
1071
+ activeTurnStartedAtMs = 0;
1072
+ // Generation whose late result should be ignored after compact fallback.
1073
+ compactLateResultSuppressedGeneration = null;
1074
+ // Timer used to defer compact completion fallback in case result arrives.
1075
+ compactCompletionFallbackTimer = null;
1076
+ // Timer used to dispatch queued prompt shortly after compact fallback.
1077
+ deferredQueuedPromptTimer = null;
1061
1078
  constructor(options) {
1062
1079
  super();
1063
1080
  this.options = options;
@@ -1203,6 +1220,10 @@ var SdkSession = class extends EventEmitter2 {
1203
1220
  this.currentPermissionMode = mode;
1204
1221
  if (modeChanged) {
1205
1222
  this.clearPendingInteractionState("Session reconfigured");
1223
+ this.clearCompactCompletionFallbackTimer();
1224
+ this.clearDeferredQueuedPromptTimer();
1225
+ this.awaitingCompactCompletionFallback = false;
1226
+ this.clearCompactLateResultSuppression();
1206
1227
  if (this.v2Session) {
1207
1228
  this.v2Session.close();
1208
1229
  this.v2Session = null;
@@ -1337,7 +1358,26 @@ var SdkSession = class extends EventEmitter2 {
1337
1358
  });
1338
1359
  this.emit("commands-updated", this.cachedCommands);
1339
1360
  }
1361
+ } else if (message.type === "system" && "subtype" in message && message.subtype === "status" && "status" in message && message.status === null && this.awaitingCompactCompletionFallback && this.isProcessing) {
1362
+ this.scheduleCompactCompletionFallback();
1363
+ } else if (message.type === "system" && "subtype" in message && message.subtype === "compact_boundary" && this.awaitingCompactCompletionFallback && this.isProcessing) {
1364
+ this.scheduleCompactCompletionFallback();
1365
+ } else if (message.type === "auth_status") {
1366
+ const authMsg = message;
1367
+ if (authMsg.error) {
1368
+ this.emit("auth-error", {
1369
+ errorCode: "authentication_failed",
1370
+ message: authMsg.error
1371
+ });
1372
+ }
1340
1373
  } else if (message.type === "assistant") {
1374
+ const assistantError = message.error;
1375
+ if (assistantError) {
1376
+ this.emit("auth-error", {
1377
+ errorCode: assistantError,
1378
+ message: assistantError
1379
+ });
1380
+ }
1341
1381
  if (message.message?.content) {
1342
1382
  for (const block of message.message.content) {
1343
1383
  if ("type" in block && block.type === "text" && "text" in block) {
@@ -1374,6 +1414,10 @@ var SdkSession = class extends EventEmitter2 {
1374
1414
  }
1375
1415
  }
1376
1416
  } else if (message.type === "result") {
1417
+ this.clearCompactCompletionFallbackTimer();
1418
+ if (this.shouldIgnoreLateCompactResult()) {
1419
+ return;
1420
+ }
1377
1421
  if (!this.currentAssistantResponse.trim() && "result" in message && message.result) {
1378
1422
  const resultText = String(message.result);
1379
1423
  this.emit("output", resultText);
@@ -1383,14 +1427,8 @@ var SdkSession = class extends EventEmitter2 {
1383
1427
  this.conversationHistory.push({ role: "assistant", content: this.currentAssistantResponse.trim() });
1384
1428
  }
1385
1429
  this.currentAssistantResponse = "";
1386
- this.isProcessing = false;
1387
- const queued = this.pendingPrompt;
1388
- if (queued) {
1389
- this.pendingPrompt = null;
1390
- this.sendPrompt(queued.prompt, queued.attachments);
1391
- } else {
1392
- this.emit("complete");
1393
- }
1430
+ this.awaitingCompactCompletionFallback = false;
1431
+ this.completeCurrentTurn();
1394
1432
  } else if (message.type === "tool_progress") {
1395
1433
  if ("tool_name" in message) {
1396
1434
  this.emit("output", `
@@ -1410,6 +1448,14 @@ var SdkSession = class extends EventEmitter2 {
1410
1448
  this.emit("request-queued");
1411
1449
  return;
1412
1450
  }
1451
+ this.clearCompactCompletionFallbackTimer();
1452
+ this.clearDeferredQueuedPromptTimer();
1453
+ this.awaitingCompactCompletionFallback = /^\/compact(?:\s|$)/.test(prompt2.trim());
1454
+ this.activeTurnGeneration = ++this.turnGeneration;
1455
+ this.activeTurnStartedAtMs = Date.now();
1456
+ if (this.compactLateResultSuppressedGeneration !== null && this.activeTurnGeneration > this.compactLateResultSuppressedGeneration + 1) {
1457
+ this.clearCompactLateResultSuppression();
1458
+ }
1413
1459
  this.isProcessing = true;
1414
1460
  this.currentAssistantResponse = "";
1415
1461
  if (prompt2.trim()) {
@@ -1480,6 +1526,9 @@ ${contextLines.join("\n")}
1480
1526
  `);
1481
1527
  }
1482
1528
  this.isProcessing = false;
1529
+ this.clearCompactCompletionFallbackTimer();
1530
+ this.clearDeferredQueuedPromptTimer();
1531
+ this.awaitingCompactCompletionFallback = false;
1483
1532
  }
1484
1533
  }
1485
1534
  /**
@@ -1507,6 +1556,8 @@ ${contextLines.join("\n")}
1507
1556
  }
1508
1557
  cancel() {
1509
1558
  this.clearPendingInteractionState("Session cancelled");
1559
+ this.clearCompactCompletionFallbackTimer();
1560
+ this.clearDeferredQueuedPromptTimer();
1510
1561
  if (this.v2Session) {
1511
1562
  this.v2Session.close();
1512
1563
  this.v2Session = null;
@@ -1514,6 +1565,8 @@ ${contextLines.join("\n")}
1514
1565
  }
1515
1566
  this.isProcessing = false;
1516
1567
  this.pendingPrompt = null;
1568
+ this.awaitingCompactCompletionFallback = false;
1569
+ this.clearCompactLateResultSuppression();
1517
1570
  }
1518
1571
  getSessionId() {
1519
1572
  return this.sessionId;
@@ -1531,6 +1584,10 @@ ${contextLines.join("\n")}
1531
1584
  this.sessionId = sessionId;
1532
1585
  this.isProcessing = false;
1533
1586
  this.pendingPrompt = null;
1587
+ this.clearCompactCompletionFallbackTimer();
1588
+ this.clearDeferredQueuedPromptTimer();
1589
+ this.awaitingCompactCompletionFallback = false;
1590
+ this.clearCompactLateResultSuppression();
1534
1591
  this.conversationHistory = [];
1535
1592
  this.emit("session-resumed", sessionId);
1536
1593
  }
@@ -1550,6 +1607,8 @@ ${contextLines.join("\n")}
1550
1607
  if (model === this.currentModel) return;
1551
1608
  this.currentModel = model;
1552
1609
  this.clearPendingInteractionState("Session reconfigured");
1610
+ this.clearCompactCompletionFallbackTimer();
1611
+ this.clearDeferredQueuedPromptTimer();
1553
1612
  if (this.v2Session) {
1554
1613
  this.v2Session.close();
1555
1614
  this.v2Session = null;
@@ -1557,9 +1616,13 @@ ${contextLines.join("\n")}
1557
1616
  this.sessionId = null;
1558
1617
  this.isProcessing = false;
1559
1618
  this.pendingPrompt = null;
1619
+ this.awaitingCompactCompletionFallback = false;
1620
+ this.clearCompactLateResultSuppression();
1560
1621
  this.pendingContextTransfer = true;
1561
1622
  } else if (this.sessionId) {
1562
1623
  this.sessionId = null;
1624
+ this.awaitingCompactCompletionFallback = false;
1625
+ this.clearCompactLateResultSuppression();
1563
1626
  this.pendingContextTransfer = true;
1564
1627
  }
1565
1628
  this.emit("model", model);
@@ -1572,6 +1635,8 @@ ${contextLines.join("\n")}
1572
1635
  }
1573
1636
  clearHistory() {
1574
1637
  this.conversationHistory = [];
1638
+ this.clearCompactCompletionFallbackTimer();
1639
+ this.clearDeferredQueuedPromptTimer();
1575
1640
  if (this.v2Session) {
1576
1641
  this.v2Session.close();
1577
1642
  this.v2Session = null;
@@ -1580,6 +1645,72 @@ ${contextLines.join("\n")}
1580
1645
  this.sessionId = null;
1581
1646
  this.isProcessing = false;
1582
1647
  this.pendingPrompt = null;
1648
+ this.awaitingCompactCompletionFallback = false;
1649
+ this.clearCompactLateResultSuppression();
1650
+ }
1651
+ completeCompactCommandFallback() {
1652
+ this.clearCompactCompletionFallbackTimer();
1653
+ this.awaitingCompactCompletionFallback = false;
1654
+ this.compactLateResultSuppressedGeneration = this.activeTurnGeneration;
1655
+ this.currentAssistantResponse = "";
1656
+ this.completeCurrentTurn(COMPACT_QUEUED_DISPATCH_DELAY_MS);
1657
+ }
1658
+ scheduleCompactCompletionFallback() {
1659
+ if (this.compactCompletionFallbackTimer) return;
1660
+ this.compactCompletionFallbackTimer = setTimeout(() => {
1661
+ this.compactCompletionFallbackTimer = null;
1662
+ if (this.awaitingCompactCompletionFallback && this.isProcessing) {
1663
+ this.completeCompactCommandFallback();
1664
+ }
1665
+ }, COMPACT_FALLBACK_GRACE_MS);
1666
+ }
1667
+ clearCompactCompletionFallbackTimer() {
1668
+ if (this.compactCompletionFallbackTimer) {
1669
+ clearTimeout(this.compactCompletionFallbackTimer);
1670
+ this.compactCompletionFallbackTimer = null;
1671
+ }
1672
+ }
1673
+ clearDeferredQueuedPromptTimer() {
1674
+ if (this.deferredQueuedPromptTimer) {
1675
+ clearTimeout(this.deferredQueuedPromptTimer);
1676
+ this.deferredQueuedPromptTimer = null;
1677
+ }
1678
+ }
1679
+ clearCompactLateResultSuppression() {
1680
+ this.compactLateResultSuppressedGeneration = null;
1681
+ }
1682
+ shouldIgnoreLateCompactResult() {
1683
+ const suppressedGeneration = this.compactLateResultSuppressedGeneration;
1684
+ if (suppressedGeneration === null) return false;
1685
+ this.clearCompactLateResultSuppression();
1686
+ if (this.activeTurnGeneration === suppressedGeneration) return true;
1687
+ if (this.activeTurnGeneration === suppressedGeneration + 1) {
1688
+ const elapsedSinceTurnStart = Date.now() - this.activeTurnStartedAtMs;
1689
+ return elapsedSinceTurnStart <= COMPACT_STALE_RESULT_GUARD_MS;
1690
+ }
1691
+ return false;
1692
+ }
1693
+ completeCurrentTurn(deferQueuedDispatchMs = 0) {
1694
+ if (!this.isProcessing) return;
1695
+ this.isProcessing = false;
1696
+ if (!this.pendingPrompt) {
1697
+ this.emit("complete");
1698
+ return;
1699
+ }
1700
+ if (deferQueuedDispatchMs > 0) {
1701
+ this.clearDeferredQueuedPromptTimer();
1702
+ this.deferredQueuedPromptTimer = setTimeout(() => {
1703
+ this.deferredQueuedPromptTimer = null;
1704
+ const deferred = this.pendingPrompt;
1705
+ if (!deferred || this.isProcessing) return;
1706
+ this.pendingPrompt = null;
1707
+ this.sendPrompt(deferred.prompt, deferred.attachments);
1708
+ }, deferQueuedDispatchMs);
1709
+ return;
1710
+ }
1711
+ const queued = this.pendingPrompt;
1712
+ this.pendingPrompt = null;
1713
+ this.sendPrompt(queued.prompt, queued.attachments);
1583
1714
  }
1584
1715
  clearPendingInteractionState(reason) {
1585
1716
  if (this.pendingAnswerRequest) {
@@ -2121,6 +2252,28 @@ var Daemon = class extends EventEmitter3 {
2121
2252
  this.sdkSession.on("error", (error) => {
2122
2253
  this.emit("error", error);
2123
2254
  });
2255
+ this.sdkSession.on("auth-error", async (errorInfo) => {
2256
+ const errorMessages = {
2257
+ "authentication_failed": 'Claude CLI authentication expired. Please run "claude login" on the host machine.',
2258
+ "billing_error": "Billing error. Please check your Anthropic account billing status.",
2259
+ "rate_limit": "Rate limit reached. Please wait a moment and try again.",
2260
+ "server_error": "Anthropic API server error. Please try again later."
2261
+ };
2262
+ const displayMessage = errorMessages[errorInfo.errorCode] || `API error: ${errorInfo.message}`;
2263
+ if (this.options.hybrid !== false) {
2264
+ process.stdout.write(`
2265
+ [Error] ${displayMessage}
2266
+ `);
2267
+ }
2268
+ this.emit("error", new Error(displayMessage));
2269
+ if (this.realtimeClient) {
2270
+ try {
2271
+ await this.realtimeClient.broadcastError(displayMessage, errorInfo.errorCode);
2272
+ await this.realtimeClient.broadcastComplete();
2273
+ } catch {
2274
+ }
2275
+ }
2276
+ });
2124
2277
  this.sdkSession.on("commands-updated", async () => {
2125
2278
  await this.broadcastCommands();
2126
2279
  });
@@ -2847,9 +3000,24 @@ var MobileServerManager = class {
2847
3000
  let needsInstall = false;
2848
3001
  try {
2849
3002
  execSync2("which ngrok", { stdio: "pipe" });
3003
+ try {
3004
+ const configOutput = execSync2("ngrok config check", {
3005
+ stdio: "pipe",
3006
+ timeout: 5e3
3007
+ }).toString();
3008
+ if (!configOutput.toLowerCase().includes("valid")) {
3009
+ issues.push(
3010
+ "ngrok authtoken is not configured.\n 1. Sign up at https://ngrok.com\n 2. Copy your authtoken from https://dashboard.ngrok.com/get-started/your-authtoken\n 3. Run: ngrok config add-authtoken <your-token>"
3011
+ );
3012
+ }
3013
+ } catch {
3014
+ issues.push(
3015
+ "ngrok authtoken is not configured.\n 1. Sign up at https://ngrok.com\n 2. Copy your authtoken from https://dashboard.ngrok.com/get-started/your-authtoken\n 3. Run: ngrok config add-authtoken <your-token>"
3016
+ );
3017
+ }
2850
3018
  } catch {
2851
3019
  issues.push(
2852
- "ngrok is not installed.\n Install: brew install ngrok\n Sign up: https://ngrok.com\n Auth: ngrok config add-authtoken <your-token>"
3020
+ "ngrok is not installed.\n Install: brew install ngrok\n Then configure your account:\n 1. Sign up at https://ngrok.com\n 2. Copy your authtoken from https://dashboard.ngrok.com/get-started/your-authtoken\n 3. Run: ngrok config add-authtoken <your-token>"
2853
3021
  );
2854
3022
  }
2855
3023
  const nodeModulesPath = join5(this.mobileProjectPath, "node_modules");
@@ -2956,26 +3124,51 @@ var MobileServerManager = class {
2956
3124
  async startNgrok() {
2957
3125
  this.ensureLogDir();
2958
3126
  this.ngrokLogStream = createWriteStream(join5(this.logDir, "ngrok.log"));
3127
+ let stderrData = "";
2959
3128
  this.ngrokProcess = spawn2("ngrok", ["http", String(this.expoPort)], {
2960
3129
  stdio: ["ignore", "pipe", "pipe"],
2961
3130
  detached: false
2962
3131
  });
2963
3132
  this.ngrokProcess.stdout?.pipe(this.ngrokLogStream);
2964
- this.ngrokProcess.stderr?.pipe(this.ngrokLogStream);
3133
+ this.ngrokProcess.stderr?.on("data", (chunk) => {
3134
+ stderrData += chunk.toString();
3135
+ this.ngrokLogStream?.write(chunk);
3136
+ });
2965
3137
  this.ngrokProcess.on("error", () => {
2966
3138
  });
2967
3139
  for (let i = 0; i < 10; i++) {
2968
3140
  await this.sleep(1e3);
3141
+ if (this.ngrokProcess.exitCode !== null) {
3142
+ break;
3143
+ }
2969
3144
  const url = await this.getNgrokTunnelUrl();
2970
3145
  if (url) {
2971
3146
  this.tunnelUrl = url;
2972
3147
  return url;
2973
3148
  }
2974
3149
  }
3150
+ this.ngrokError = this.diagnoseNgrokFailure(stderrData);
2975
3151
  this.killProcess(this.ngrokProcess);
2976
3152
  this.ngrokProcess = null;
2977
3153
  return null;
2978
3154
  }
3155
+ /** Last ngrok error diagnosis (available after startNgrok fails) */
3156
+ ngrokError = null;
3157
+ diagnoseNgrokFailure(stderr) {
3158
+ const lower = stderr.toLowerCase();
3159
+ if (lower.includes("authtoken") || lower.includes("err_ngrok_105") || lower.includes("authentication")) {
3160
+ return "ngrok authentication failed.\n Your authtoken may be invalid or expired.\n 1. Get a new token at https://dashboard.ngrok.com/get-started/your-authtoken\n 2. Run: ngrok config add-authtoken <your-token>";
3161
+ }
3162
+ if (lower.includes("tunnel session limit") || lower.includes("err_ngrok_108")) {
3163
+ return "ngrok free plan session limit reached.\n Free accounts allow 1 tunnel at a time.\n Close other ngrok tunnels or upgrade your plan at https://ngrok.com/pricing";
3164
+ }
3165
+ if (lower.includes("tcp dial") || lower.includes("connection refused")) {
3166
+ return `ngrok could not connect to localhost:${this.expoPort}.
3167
+ This is usually a timing issue \u2014 the tunnel started before Expo was ready.`;
3168
+ }
3169
+ return `ngrok tunnel failed to start.
3170
+ Check logs for details: ${join5(this.logDir, "ngrok.log")}`;
3171
+ }
2979
3172
  async startExpo(tunnelUrl) {
2980
3173
  this.ensureLogDir();
2981
3174
  this.expoLogStream = createWriteStream(join5(this.logDir, "expo.log"));
@@ -3066,7 +3259,7 @@ var MobileServerManager = class {
3066
3259
  this.onProgress("Starting ngrok tunnel...");
3067
3260
  const tunnelUrl = await this.startNgrok();
3068
3261
  if (!tunnelUrl) {
3069
- return { started: false, error: "Failed to start ngrok tunnel" };
3262
+ return { started: false, error: this.ngrokError ?? "Failed to start ngrok tunnel" };
3070
3263
  }
3071
3264
  this.onProgress("Starting Expo server...");
3072
3265
  const expoStarted = await this.startExpo(tunnelUrl);
@@ -3234,6 +3427,52 @@ function removePidFileUnchecked(pidFile) {
3234
3427
  }
3235
3428
  }
3236
3429
 
3430
+ // src/utils/claude-auth.ts
3431
+ import { execSync as execSync3 } from "child_process";
3432
+ function checkClaudeCliAuth() {
3433
+ try {
3434
+ const output = execSync3("claude auth status --json", {
3435
+ encoding: "utf-8",
3436
+ timeout: 1e4,
3437
+ stdio: ["pipe", "pipe", "pipe"]
3438
+ });
3439
+ const status = JSON.parse(output.trim());
3440
+ if (status.loggedIn === true) {
3441
+ return {
3442
+ loggedIn: true,
3443
+ authMethod: status.authMethod,
3444
+ apiProvider: status.apiProvider
3445
+ };
3446
+ }
3447
+ return { loggedIn: false, failure: "not_logged_in" };
3448
+ } catch (error) {
3449
+ const execError = error;
3450
+ if (execError.stdout) {
3451
+ try {
3452
+ const stdout = typeof execError.stdout === "string" ? execError.stdout : execError.stdout.toString("utf-8");
3453
+ const status = JSON.parse(stdout.trim());
3454
+ if (status.loggedIn === true) {
3455
+ return {
3456
+ loggedIn: true,
3457
+ authMethod: status.authMethod,
3458
+ apiProvider: status.apiProvider
3459
+ };
3460
+ }
3461
+ return { loggedIn: false, failure: "not_logged_in" };
3462
+ } catch {
3463
+ }
3464
+ }
3465
+ const message = execError.message ?? String(error);
3466
+ if (message.includes("command not found") || message.includes("ENOENT") || message.includes("not recognized")) {
3467
+ return { loggedIn: false, failure: "cli_not_found" };
3468
+ }
3469
+ if (message.includes("Unknown command") || message.includes("unknown command") || message.includes("Invalid subcommand") || message.includes("invalid subcommand")) {
3470
+ return { loggedIn: false, failure: "subcommand_not_supported" };
3471
+ }
3472
+ return { loggedIn: false, failure: "unknown" };
3473
+ }
3474
+ }
3475
+
3237
3476
  // src/commands/start.ts
3238
3477
  if (typeof globalThis.WebSocket === "undefined") {
3239
3478
  globalThis.WebSocket = WebSocket;
@@ -3282,6 +3521,36 @@ function createStartCommand() {
3282
3521
  }
3283
3522
  });
3284
3523
  spinner.update(`Authenticated as ${user.email}...`);
3524
+ spinner.update("Checking Claude CLI auth...");
3525
+ const claudeAuth = checkClaudeCliAuth();
3526
+ if (!claudeAuth.loggedIn) {
3527
+ switch (claudeAuth.failure) {
3528
+ case "cli_not_found":
3529
+ spinner.fail("Claude CLI not found");
3530
+ logger.error("Claude Code CLI is not installed or not in PATH.");
3531
+ logger.error("Install it first: https://docs.anthropic.com/en/docs/claude-code");
3532
+ removePidFile();
3533
+ process.exit(1);
3534
+ break;
3535
+ case "subcommand_not_supported":
3536
+ spinner.update('Claude CLI auth check skipped (CLI version does not support "auth status")');
3537
+ logger.warn('Could not verify Claude CLI auth \u2014 your CLI version may not support "claude auth status".');
3538
+ logger.warn('If sessions fail to start, try updating Claude Code and running "claude login".');
3539
+ break;
3540
+ case "not_logged_in":
3541
+ spinner.fail("Claude CLI is not logged in");
3542
+ logger.error("Claude Code requires authentication to use the Anthropic API.");
3543
+ logger.error('Run "claude login" first, then try "clautunnel start" again.');
3544
+ removePidFile();
3545
+ process.exit(1);
3546
+ break;
3547
+ default:
3548
+ spinner.update("Claude CLI auth check inconclusive");
3549
+ logger.warn("Could not verify Claude CLI authentication (unexpected error).");
3550
+ logger.warn('If sessions fail to start, run "claude login" and try again.');
3551
+ break;
3552
+ }
3553
+ }
3285
3554
  let fdaStatus = null;
3286
3555
  if (isMacOS()) {
3287
3556
  spinner.stop();