@tongil_kim/clautunnel 1.5.1 → 1.6.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.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
  });
@@ -3234,6 +3387,52 @@ function removePidFileUnchecked(pidFile) {
3234
3387
  }
3235
3388
  }
3236
3389
 
3390
+ // src/utils/claude-auth.ts
3391
+ import { execSync as execSync3 } from "child_process";
3392
+ function checkClaudeCliAuth() {
3393
+ try {
3394
+ const output = execSync3("claude auth status --json", {
3395
+ encoding: "utf-8",
3396
+ timeout: 1e4,
3397
+ stdio: ["pipe", "pipe", "pipe"]
3398
+ });
3399
+ const status = JSON.parse(output.trim());
3400
+ if (status.loggedIn === true) {
3401
+ return {
3402
+ loggedIn: true,
3403
+ authMethod: status.authMethod,
3404
+ apiProvider: status.apiProvider
3405
+ };
3406
+ }
3407
+ return { loggedIn: false, failure: "not_logged_in" };
3408
+ } catch (error) {
3409
+ const execError = error;
3410
+ if (execError.stdout) {
3411
+ try {
3412
+ const stdout = typeof execError.stdout === "string" ? execError.stdout : execError.stdout.toString("utf-8");
3413
+ const status = JSON.parse(stdout.trim());
3414
+ if (status.loggedIn === true) {
3415
+ return {
3416
+ loggedIn: true,
3417
+ authMethod: status.authMethod,
3418
+ apiProvider: status.apiProvider
3419
+ };
3420
+ }
3421
+ return { loggedIn: false, failure: "not_logged_in" };
3422
+ } catch {
3423
+ }
3424
+ }
3425
+ const message = execError.message ?? String(error);
3426
+ if (message.includes("command not found") || message.includes("ENOENT") || message.includes("not recognized")) {
3427
+ return { loggedIn: false, failure: "cli_not_found" };
3428
+ }
3429
+ if (message.includes("Unknown command") || message.includes("unknown command") || message.includes("Invalid subcommand") || message.includes("invalid subcommand")) {
3430
+ return { loggedIn: false, failure: "subcommand_not_supported" };
3431
+ }
3432
+ return { loggedIn: false, failure: "unknown" };
3433
+ }
3434
+ }
3435
+
3237
3436
  // src/commands/start.ts
3238
3437
  if (typeof globalThis.WebSocket === "undefined") {
3239
3438
  globalThis.WebSocket = WebSocket;
@@ -3282,6 +3481,36 @@ function createStartCommand() {
3282
3481
  }
3283
3482
  });
3284
3483
  spinner.update(`Authenticated as ${user.email}...`);
3484
+ spinner.update("Checking Claude CLI auth...");
3485
+ const claudeAuth = checkClaudeCliAuth();
3486
+ if (!claudeAuth.loggedIn) {
3487
+ switch (claudeAuth.failure) {
3488
+ case "cli_not_found":
3489
+ spinner.fail("Claude CLI not found");
3490
+ logger.error("Claude Code CLI is not installed or not in PATH.");
3491
+ logger.error("Install it first: https://docs.anthropic.com/en/docs/claude-code");
3492
+ removePidFile();
3493
+ process.exit(1);
3494
+ break;
3495
+ case "subcommand_not_supported":
3496
+ spinner.update('Claude CLI auth check skipped (CLI version does not support "auth status")');
3497
+ logger.warn('Could not verify Claude CLI auth \u2014 your CLI version may not support "claude auth status".');
3498
+ logger.warn('If sessions fail to start, try updating Claude Code and running "claude login".');
3499
+ break;
3500
+ case "not_logged_in":
3501
+ spinner.fail("Claude CLI is not logged in");
3502
+ logger.error("Claude Code requires authentication to use the Anthropic API.");
3503
+ logger.error('Run "claude login" first, then try "clautunnel start" again.');
3504
+ removePidFile();
3505
+ process.exit(1);
3506
+ break;
3507
+ default:
3508
+ spinner.update("Claude CLI auth check inconclusive");
3509
+ logger.warn("Could not verify Claude CLI authentication (unexpected error).");
3510
+ logger.warn('If sessions fail to start, run "claude login" and try again.');
3511
+ break;
3512
+ }
3513
+ }
3285
3514
  let fdaStatus = null;
3286
3515
  if (isMacOS()) {
3287
3516
  spinner.stop();