@tongil_kim/clautunnel 1.4.1 → 1.5.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.d.ts CHANGED
@@ -83,7 +83,7 @@ declare class RealtimeClient extends EventEmitter {
83
83
  broadcastUserQuestion(questionData: UserQuestionData): Promise<void>;
84
84
  broadcastPermissionRequest(requestData: PermissionRequestData): Promise<void>;
85
85
  broadcastToolUse(toolUseData: ToolUseData): Promise<void>;
86
- broadcastStatusResponse(isProcessing: boolean, isMessageQueued: boolean): Promise<void>;
86
+ broadcastStatusResponse(isProcessing: boolean, isMessageQueued: boolean, permissionMode?: PermissionMode): Promise<void>;
87
87
  broadcastComplete(): Promise<void>;
88
88
  broadcastSessionTitle(title: string): Promise<void>;
89
89
  broadcastQueued(): Promise<void>;
package/dist/index.js CHANGED
@@ -127,6 +127,51 @@ var require_constants = __commonJS({
127
127
  }
128
128
  });
129
129
 
130
+ // ../../packages/shared/dist/utils/permission-mode.js
131
+ var require_permission_mode = __commonJS({
132
+ "../../packages/shared/dist/utils/permission-mode.js"(exports) {
133
+ "use strict";
134
+ Object.defineProperty(exports, "__esModule", { value: true });
135
+ exports.isPermissionMode = isPermissionMode3;
136
+ var VALID_PERMISSION_MODES = [
137
+ "default",
138
+ "acceptEdits",
139
+ "plan",
140
+ "bypassPermissions",
141
+ "delegate",
142
+ "dontAsk"
143
+ ];
144
+ function isPermissionMode3(value) {
145
+ return typeof value === "string" && VALID_PERMISSION_MODES.includes(value);
146
+ }
147
+ }
148
+ });
149
+
150
+ // ../../packages/shared/dist/utils/index.js
151
+ var require_utils = __commonJS({
152
+ "../../packages/shared/dist/utils/index.js"(exports) {
153
+ "use strict";
154
+ var __createBinding = exports && exports.__createBinding || (Object.create ? (function(o, m, k, k2) {
155
+ if (k2 === void 0) k2 = k;
156
+ var desc = Object.getOwnPropertyDescriptor(m, k);
157
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
158
+ desc = { enumerable: true, get: function() {
159
+ return m[k];
160
+ } };
161
+ }
162
+ Object.defineProperty(o, k2, desc);
163
+ }) : (function(o, m, k, k2) {
164
+ if (k2 === void 0) k2 = k;
165
+ o[k2] = m[k];
166
+ }));
167
+ var __exportStar = exports && exports.__exportStar || function(m, exports2) {
168
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports2, p)) __createBinding(exports2, m, p);
169
+ };
170
+ Object.defineProperty(exports, "__esModule", { value: true });
171
+ __exportStar(require_permission_mode(), exports);
172
+ }
173
+ });
174
+
130
175
  // ../../packages/shared/dist/index.js
131
176
  var require_dist = __commonJS({
132
177
  "../../packages/shared/dist/index.js"(exports) {
@@ -150,12 +195,13 @@ var require_dist = __commonJS({
150
195
  Object.defineProperty(exports, "__esModule", { value: true });
151
196
  __exportStar(require_types(), exports);
152
197
  __exportStar(require_constants(), exports);
198
+ __exportStar(require_utils(), exports);
153
199
  }
154
200
  });
155
201
 
156
202
  // src/index.ts
157
203
  import { config } from "dotenv";
158
- import { resolve as resolve2, dirname as dirname2 } from "path";
204
+ import { resolve as resolve2, dirname as dirname3 } from "path";
159
205
  import { readFileSync as readFileSync5 } from "fs";
160
206
  import { fileURLToPath } from "url";
161
207
 
@@ -715,7 +761,7 @@ var RealtimeClient = class extends EventEmitter {
715
761
  }
716
762
  this.emit("broadcast", message);
717
763
  }
718
- async broadcastStatusResponse(isProcessing, isMessageQueued) {
764
+ async broadcastStatusResponse(isProcessing, isMessageQueued, permissionMode) {
719
765
  if (!this.outputChannel) {
720
766
  throw new Error("Not connected");
721
767
  }
@@ -726,6 +772,7 @@ var RealtimeClient = class extends EventEmitter {
726
772
  type: "status-response",
727
773
  isProcessing,
728
774
  isMessageQueued,
775
+ permissionMode,
729
776
  timestamp: Date.now(),
730
777
  seq: ++this.seq
731
778
  };
@@ -1000,8 +1047,8 @@ var SdkSession = class extends EventEmitter2 {
1000
1047
  pendingContextTransfer = false;
1001
1048
  thinkingEnabled = false;
1002
1049
  pendingPermissionRequests = /* @__PURE__ */ new Map();
1003
- // Pending AskUserQuestion answer resolver - resolved by provideAnswer()
1004
- pendingAnswerResolve = null;
1050
+ // Pending AskUserQuestion answer promise
1051
+ pendingAnswerRequest = null;
1005
1052
  // Track pending question/permission data for re-broadcast on status-request
1006
1053
  pendingQuestionData = null;
1007
1054
  pendingPermissionData = null;
@@ -1031,7 +1078,10 @@ var SdkSession = class extends EventEmitter2 {
1031
1078
  if (response.behavior === "allow") {
1032
1079
  const result = {
1033
1080
  behavior: "allow",
1034
- updatedInput: response.updatedInput,
1081
+ // updatedInput is required by the SDK's Zod schema — if omitted,
1082
+ // JSON.stringify drops the key and subprocess validation fails.
1083
+ // Fall back to the original tool input to keep it valid.
1084
+ updatedInput: response.updatedInput ?? pending.toolInput,
1035
1085
  updatedPermissions: response.updatedPermissions
1036
1086
  };
1037
1087
  pending.resolve(result);
@@ -1075,15 +1125,18 @@ var SdkSession = class extends EventEmitter2 {
1075
1125
  this.emit("user-question", questionData);
1076
1126
  const answers = await new Promise(
1077
1127
  (resolve3, reject) => {
1078
- this.pendingAnswerResolve = resolve3;
1128
+ const pendingRequest = { resolve: resolve3, reject };
1129
+ this.pendingAnswerRequest = pendingRequest;
1079
1130
  options.signal.addEventListener("abort", () => {
1080
- this.pendingAnswerResolve = null;
1081
- this.pendingQuestionData = null;
1131
+ if (this.pendingAnswerRequest === pendingRequest) {
1132
+ this.pendingAnswerRequest = null;
1133
+ this.pendingQuestionData = null;
1134
+ }
1082
1135
  reject(new Error("Question aborted"));
1083
1136
  });
1084
1137
  }
1085
1138
  );
1086
- this.pendingAnswerResolve = null;
1139
+ this.pendingAnswerRequest = null;
1087
1140
  this.pendingQuestionData = null;
1088
1141
  return {
1089
1142
  behavior: "allow",
@@ -1134,7 +1187,8 @@ var SdkSession = class extends EventEmitter2 {
1134
1187
  this.pendingPermissionRequests.set(requestId, {
1135
1188
  resolve: resolve3,
1136
1189
  reject,
1137
- signal: options.signal
1190
+ signal: options.signal,
1191
+ toolInput: input
1138
1192
  });
1139
1193
  options.signal.addEventListener("abort", () => {
1140
1194
  this.pendingPermissionRequests.delete(requestId);
@@ -1145,7 +1199,23 @@ var SdkSession = class extends EventEmitter2 {
1145
1199
  };
1146
1200
  }
1147
1201
  setPermissionMode(mode) {
1202
+ const modeChanged = this.currentPermissionMode !== mode;
1148
1203
  this.currentPermissionMode = mode;
1204
+ if (modeChanged) {
1205
+ this.clearPendingInteractionState("Session reconfigured");
1206
+ if (this.v2Session) {
1207
+ this.v2Session.close();
1208
+ this.v2Session = null;
1209
+ this.streamLoopRunning = false;
1210
+ this.sessionId = null;
1211
+ this.isProcessing = false;
1212
+ this.pendingPrompt = null;
1213
+ this.pendingContextTransfer = true;
1214
+ } else if (this.sessionId) {
1215
+ this.sessionId = null;
1216
+ this.pendingContextTransfer = true;
1217
+ }
1218
+ }
1149
1219
  this.emit("permission-mode", mode);
1150
1220
  }
1151
1221
  getPermissionMode() {
@@ -1426,19 +1496,17 @@ ${contextLines.join("\n")}
1426
1496
  if (answerText.trim()) {
1427
1497
  this.conversationHistory.push({ role: "user", content: answerText });
1428
1498
  }
1429
- if (this.pendingAnswerResolve) {
1499
+ if (this.pendingAnswerRequest) {
1430
1500
  const resolvedAnswers = answers || { result: answerText };
1431
- this.pendingAnswerResolve(resolvedAnswers);
1432
- this.pendingAnswerResolve = null;
1501
+ const pendingRequest = this.pendingAnswerRequest;
1502
+ this.pendingAnswerRequest = null;
1503
+ pendingRequest.resolve(resolvedAnswers);
1433
1504
  return;
1434
1505
  }
1435
1506
  await this.sendPrompt(answerText);
1436
1507
  }
1437
1508
  cancel() {
1438
- this.pendingAnswerResolve = null;
1439
- this.pendingQuestionData = null;
1440
- this.pendingPermissionData = null;
1441
- this.pendingPermissionRequests.clear();
1509
+ this.clearPendingInteractionState("Session cancelled");
1442
1510
  if (this.v2Session) {
1443
1511
  this.v2Session.close();
1444
1512
  this.v2Session = null;
@@ -1481,6 +1549,7 @@ ${contextLines.join("\n")}
1481
1549
  async setModel(model) {
1482
1550
  if (model === this.currentModel) return;
1483
1551
  this.currentModel = model;
1552
+ this.clearPendingInteractionState("Session reconfigured");
1484
1553
  if (this.v2Session) {
1485
1554
  this.v2Session.close();
1486
1555
  this.v2Session = null;
@@ -1512,6 +1581,18 @@ ${contextLines.join("\n")}
1512
1581
  this.isProcessing = false;
1513
1582
  this.pendingPrompt = null;
1514
1583
  }
1584
+ clearPendingInteractionState(reason) {
1585
+ if (this.pendingAnswerRequest) {
1586
+ this.pendingAnswerRequest.reject(new Error(reason));
1587
+ this.pendingAnswerRequest = null;
1588
+ }
1589
+ this.pendingQuestionData = null;
1590
+ this.pendingPermissionData = null;
1591
+ for (const pending of this.pendingPermissionRequests.values()) {
1592
+ pending.reject(new Error(reason));
1593
+ }
1594
+ this.pendingPermissionRequests.clear();
1595
+ }
1515
1596
  async getSupportedModels() {
1516
1597
  const coreModels = [
1517
1598
  { value: "opus", displayName: "Opus 4.6", description: "Opus 4.6 \xB7 Most capable for complex work" },
@@ -1651,6 +1732,7 @@ ${contextLines.join("\n")}
1651
1732
  };
1652
1733
 
1653
1734
  // src/daemon/config-manager.ts
1735
+ var import_clautunnel_shared2 = __toESM(require_dist(), 1);
1654
1736
  import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
1655
1737
  import { join as join3, dirname } from "path";
1656
1738
  import { homedir as homedir3 } from "os";
@@ -1720,6 +1802,14 @@ var ConfigManager = class {
1720
1802
  const settings = this.getMergedSettings();
1721
1803
  return settings.alwaysThinkingEnabled ?? false;
1722
1804
  }
1805
+ /**
1806
+ * Get the current permission mode setting from merged settings
1807
+ */
1808
+ getPermissionMode() {
1809
+ const settings = this.getMergedSettings();
1810
+ const mode = settings.permissions?.mode;
1811
+ return (0, import_clautunnel_shared2.isPermissionMode)(mode) ? mode : "default";
1812
+ }
1723
1813
  getInteractiveData(command) {
1724
1814
  const settings = this.getMergedSettings();
1725
1815
  switch (command) {
@@ -1964,6 +2054,7 @@ var ConfigManager = class {
1964
2054
  };
1965
2055
 
1966
2056
  // src/daemon/daemon.ts
2057
+ var import_clautunnel_shared3 = __toESM(require_dist(), 1);
1967
2058
  var Daemon = class extends EventEmitter3 {
1968
2059
  options;
1969
2060
  sdkSession;
@@ -1980,18 +2071,19 @@ var Daemon = class extends EventEmitter3 {
1980
2071
  constructor(options) {
1981
2072
  super();
1982
2073
  this.options = options;
1983
- this.sdkSession = new SdkSession({
2074
+ this.configManager = new ConfigManager({
1984
2075
  cwd: options.cwd
1985
2076
  });
2077
+ this.sdkSession = new SdkSession({
2078
+ cwd: options.cwd,
2079
+ permissionMode: this.configManager.getPermissionMode()
2080
+ });
1986
2081
  this.sessionManager = new SessionManager({
1987
2082
  supabase: options.supabase
1988
2083
  });
1989
2084
  this.machineManager = new MachineManager({
1990
2085
  supabase: options.supabase
1991
2086
  });
1992
- this.configManager = new ConfigManager({
1993
- cwd: options.cwd
1994
- });
1995
2087
  const thinkingEnabled = this.configManager.getThinkingMode();
1996
2088
  if (thinkingEnabled) {
1997
2089
  this.sdkSession.setThinkingMode(true);
@@ -2195,7 +2287,11 @@ ${confirmationMsg}
2195
2287
  const isProcessing = this.sdkSession.isActive();
2196
2288
  const isActivelyWorking = isProcessing && !pendingQuestion && !pendingPermission;
2197
2289
  const isMessageQueued = this.sdkSession.hasPendingPrompt();
2198
- await this.realtimeClient.broadcastStatusResponse(isActivelyWorking, isMessageQueued);
2290
+ await this.realtimeClient.broadcastStatusResponse(
2291
+ isActivelyWorking,
2292
+ isMessageQueued,
2293
+ this.sdkSession.getPermissionMode()
2294
+ );
2199
2295
  } catch {
2200
2296
  }
2201
2297
  }
@@ -2220,6 +2316,9 @@ ${confirmationMsg}
2220
2316
  }
2221
2317
  if (message.type === "interactive-apply" && message.interactivePayload) {
2222
2318
  const result = this.configManager.applyChange(message.interactivePayload);
2319
+ if (result.success && message.interactivePayload.command === "permissions" && (0, import_clautunnel_shared3.isPermissionMode)(message.interactivePayload.value)) {
2320
+ this.sdkSession.setPermissionMode(message.interactivePayload.value);
2321
+ }
2223
2322
  if (message.interactivePayload.command === "config" && message.interactivePayload.key === "alwaysThinkingEnabled") {
2224
2323
  const enabled = Boolean(message.interactivePayload.value);
2225
2324
  await this.sdkSession.setThinkingMode(enabled);
@@ -2399,7 +2498,7 @@ import { Command } from "commander";
2399
2498
  import WebSocket from "ws";
2400
2499
 
2401
2500
  // src/realtime/machine-client.ts
2402
- var import_clautunnel_shared2 = __toESM(require_dist(), 1);
2501
+ var import_clautunnel_shared4 = __toESM(require_dist(), 1);
2403
2502
  import { EventEmitter as EventEmitter4 } from "events";
2404
2503
  var MachineRealtimeClient = class extends EventEmitter4 {
2405
2504
  supabase;
@@ -2413,12 +2512,12 @@ var MachineRealtimeClient = class extends EventEmitter4 {
2413
2512
  this.machineId = options.machineId;
2414
2513
  }
2415
2514
  async connect() {
2416
- const inputChannelName = import_clautunnel_shared2.REALTIME_CHANNELS.machineInput(this.machineId);
2515
+ const inputChannelName = import_clautunnel_shared4.REALTIME_CHANNELS.machineInput(this.machineId);
2417
2516
  this.inputChannel = this.supabase.channel(inputChannelName);
2418
2517
  this.inputChannel.on("broadcast", { event: "machine-command" }, (payload) => {
2419
2518
  this.emit("command", payload.payload);
2420
2519
  });
2421
- const outputChannelName = import_clautunnel_shared2.REALTIME_CHANNELS.machineOutput(this.machineId);
2520
+ const outputChannelName = import_clautunnel_shared4.REALTIME_CHANNELS.machineOutput(this.machineId);
2422
2521
  this.outputChannel = this.supabase.channel(outputChannelName);
2423
2522
  const results = await Promise.all([
2424
2523
  subscribeWithTimeout(this.inputChannel, "machine-input"),
@@ -2426,7 +2525,7 @@ var MachineRealtimeClient = class extends EventEmitter4 {
2426
2525
  ]);
2427
2526
  const connected = results.every((success) => success);
2428
2527
  if (connected) {
2429
- const presenceChannelName = import_clautunnel_shared2.REALTIME_CHANNELS.machinePresence(this.machineId);
2528
+ const presenceChannelName = import_clautunnel_shared4.REALTIME_CHANNELS.machinePresence(this.machineId);
2430
2529
  this.presenceChannel = this.supabase.channel(presenceChannelName);
2431
2530
  this.presenceChannel.subscribe(async (status) => {
2432
2531
  if (status === "SUBSCRIBED" && this.presenceChannel) {
@@ -3062,6 +3161,79 @@ var MobileServerManager = class {
3062
3161
  }
3063
3162
  };
3064
3163
 
3164
+ // src/utils/pid.ts
3165
+ import * as fs2 from "fs";
3166
+ import * as path3 from "path";
3167
+ import * as os4 from "os";
3168
+ var PID_FILE = path3.join(os4.homedir(), ".clautunnel", "daemon.pid");
3169
+ var startTimestamp = Date.now();
3170
+ function acquirePidFile(pidFile = PID_FILE) {
3171
+ const dir = path3.dirname(pidFile);
3172
+ if (!fs2.existsSync(dir)) {
3173
+ fs2.mkdirSync(dir, { recursive: true });
3174
+ }
3175
+ const content = `${process.pid}:${startTimestamp}`;
3176
+ try {
3177
+ const fd = fs2.openSync(pidFile, "wx");
3178
+ fs2.writeSync(fd, content);
3179
+ fs2.closeSync(fd);
3180
+ return null;
3181
+ } catch (err) {
3182
+ if (err.code !== "EEXIST") {
3183
+ throw err;
3184
+ }
3185
+ }
3186
+ const existing = parsePidFile(pidFile);
3187
+ if (existing === null) {
3188
+ removePidFileUnchecked(pidFile);
3189
+ return acquirePidFile(pidFile);
3190
+ }
3191
+ if (isProcessAlive(existing.pid)) {
3192
+ return existing.pid;
3193
+ }
3194
+ removePidFileUnchecked(pidFile);
3195
+ return acquirePidFile(pidFile);
3196
+ }
3197
+ function removePidFile(pidFile = PID_FILE) {
3198
+ const existing = parsePidFile(pidFile);
3199
+ if (existing && existing.pid === process.pid && existing.timestamp === startTimestamp) {
3200
+ removePidFileUnchecked(pidFile);
3201
+ }
3202
+ }
3203
+ function readPidFile(pidFile = PID_FILE) {
3204
+ const existing = parsePidFile(pidFile);
3205
+ return existing?.pid ?? null;
3206
+ }
3207
+ function isProcessAlive(pid) {
3208
+ try {
3209
+ process.kill(pid, 0);
3210
+ return true;
3211
+ } catch {
3212
+ return false;
3213
+ }
3214
+ }
3215
+ function pidFileExists(pidFile = PID_FILE) {
3216
+ return fs2.existsSync(pidFile);
3217
+ }
3218
+ function parsePidFile(pidFile) {
3219
+ try {
3220
+ const content = fs2.readFileSync(pidFile, "utf-8").trim();
3221
+ const parts = content.split(":");
3222
+ const pid = parseInt(parts[0], 10);
3223
+ const timestamp = parts.length > 1 ? parseInt(parts[1], 10) : 0;
3224
+ if (isNaN(pid)) return null;
3225
+ return { pid, timestamp: isNaN(timestamp) ? 0 : timestamp };
3226
+ } catch {
3227
+ return null;
3228
+ }
3229
+ }
3230
+ function removePidFileUnchecked(pidFile) {
3231
+ try {
3232
+ fs2.unlinkSync(pidFile);
3233
+ } catch {
3234
+ }
3235
+ }
3236
+
3065
3237
  // src/commands/start.ts
3066
3238
  if (typeof globalThis.WebSocket === "undefined") {
3067
3239
  globalThis.WebSocket = WebSocket;
@@ -3075,6 +3247,12 @@ function createStartCommand() {
3075
3247
  const daemons = /* @__PURE__ */ new Map();
3076
3248
  let machineClient = null;
3077
3249
  try {
3250
+ const existingPid = acquirePidFile();
3251
+ if (existingPid !== null) {
3252
+ logger.error(`Another clautunnel process is already running (PID: ${existingPid})`);
3253
+ logger.error('Run "clautunnel stop" first, or kill the existing process.');
3254
+ process.exit(1);
3255
+ }
3078
3256
  config2.requireConfiguration();
3079
3257
  spinner.start();
3080
3258
  const supabase = createSupabaseClient(
@@ -3089,6 +3267,7 @@ function createStartCommand() {
3089
3267
  logger.error(
3090
3268
  'Run "clautunnel login" or "clautunnel signup" first.'
3091
3269
  );
3270
+ removePidFile();
3092
3271
  process.exit(1);
3093
3272
  }
3094
3273
  const { user } = session;
@@ -3186,11 +3365,13 @@ function createStartCommand() {
3186
3365
  logger.info("");
3187
3366
  } else {
3188
3367
  logger.error(`Mobile server failed: ${result.error}`);
3368
+ removePidFile();
3189
3369
  process.exit(1);
3190
3370
  }
3191
3371
  spinner.start();
3192
3372
  }
3193
3373
  const cleanup2 = async () => {
3374
+ removePidFile();
3194
3375
  if (mobileServer) {
3195
3376
  try {
3196
3377
  await mobileServer.stop();
@@ -3202,6 +3383,14 @@ function createStartCommand() {
3202
3383
  }
3203
3384
  cleanup(sleepState);
3204
3385
  };
3386
+ spinner.update("Registering machine...");
3387
+ const machineManager = new MachineManager({ supabase });
3388
+ const machine = await machineManager.registerMachine(
3389
+ user.id,
3390
+ options.name,
3391
+ config2.getMachineId()
3392
+ );
3393
+ config2.setMachineId(machine.id);
3205
3394
  let isShuttingDown = false;
3206
3395
  const gracefulShutdown = async (signal) => {
3207
3396
  if (isShuttingDown) {
@@ -3219,6 +3408,10 @@ function createStartCommand() {
3219
3408
  }
3220
3409
  daemons.delete(sessionId);
3221
3410
  }
3411
+ try {
3412
+ await machineManager.updateMachineStatus(machine.id, "offline");
3413
+ } catch {
3414
+ }
3222
3415
  if (machineClient) {
3223
3416
  await machineClient.disconnect();
3224
3417
  machineClient = null;
@@ -3237,14 +3430,6 @@ function createStartCommand() {
3237
3430
  process.on("SIGTERM", () => {
3238
3431
  gracefulShutdown("SIGTERM").catch(console.error);
3239
3432
  });
3240
- spinner.update("Registering machine...");
3241
- const machineManager = new MachineManager({ supabase });
3242
- const machine = await machineManager.registerMachine(
3243
- user.id,
3244
- options.name,
3245
- config2.getMachineId()
3246
- );
3247
- config2.setMachineId(machine.id);
3248
3433
  spinner.update("Connecting to realtime...");
3249
3434
  machineClient = new MachineRealtimeClient({
3250
3435
  supabase,
@@ -3259,6 +3444,7 @@ function createStartCommand() {
3259
3444
  logger.error(' 1. Open a new terminal and run "clautunnel start" again');
3260
3445
  logger.error(" 2. Check your network connection");
3261
3446
  logger.error(' 3. Try "clautunnel login" to refresh your session');
3447
+ removePidFile();
3262
3448
  process.exit(1);
3263
3449
  }
3264
3450
  logger.info("");
@@ -3360,6 +3546,7 @@ function createStartCommand() {
3360
3546
  }
3361
3547
  });
3362
3548
  } catch (error) {
3549
+ removePidFile();
3363
3550
  if (error instanceof ConfigurationError) {
3364
3551
  spinner.stop();
3365
3552
  logger.error(error.message);
@@ -3377,42 +3564,47 @@ function createStartCommand() {
3377
3564
 
3378
3565
  // src/commands/stop.ts
3379
3566
  import { Command as Command2 } from "commander";
3380
- import * as fs2 from "fs";
3381
- import * as path3 from "path";
3382
- import * as os4 from "os";
3383
- var PID_FILE = path3.join(os4.homedir(), ".clautunnel", "daemon.pid");
3567
+ import * as fs3 from "fs";
3384
3568
  function createStopCommand() {
3385
3569
  const command = new Command2("stop");
3386
3570
  command.description("Stop the running daemon").action(async () => {
3387
3571
  const logger = new Logger();
3388
3572
  try {
3389
- if (!fs2.existsSync(PID_FILE)) {
3573
+ const pid = readPidFile();
3574
+ if (pid === null) {
3390
3575
  logger.info("No daemon is running");
3391
3576
  return;
3392
3577
  }
3393
- const pid = parseInt(fs2.readFileSync(PID_FILE, "utf-8").trim(), 10);
3394
- if (isNaN(pid)) {
3395
- logger.error("Invalid PID file");
3396
- fs2.unlinkSync(PID_FILE);
3578
+ if (!isProcessAlive(pid)) {
3579
+ logger.info("Daemon process not found (already stopped)");
3580
+ try {
3581
+ fs3.unlinkSync(PID_FILE);
3582
+ } catch {
3583
+ }
3397
3584
  return;
3398
3585
  }
3399
3586
  try {
3400
- process.kill(pid, 0);
3401
3587
  process.kill(pid, "SIGTERM");
3402
3588
  logger.info(`Sent stop signal to daemon (PID: ${pid})`);
3403
3589
  let attempts = 0;
3590
+ let pidFileRemoved = false;
3404
3591
  while (attempts < 10) {
3405
3592
  await new Promise((resolve3) => setTimeout(resolve3, 500));
3406
- try {
3407
- process.kill(pid, 0);
3408
- attempts++;
3409
- } catch {
3593
+ if (!pidFileExists()) {
3594
+ pidFileRemoved = true;
3595
+ break;
3596
+ }
3597
+ if (!isProcessAlive(pid)) {
3410
3598
  break;
3411
3599
  }
3600
+ attempts++;
3412
3601
  }
3413
- if (attempts >= 10) {
3414
- logger.warn("Daemon did not stop gracefully, sending SIGKILL");
3415
- process.kill(pid, "SIGKILL");
3602
+ if (attempts >= 10 && !pidFileRemoved) {
3603
+ const currentPid2 = readPidFile();
3604
+ if (currentPid2 === pid && isProcessAlive(pid)) {
3605
+ logger.warn("Daemon did not stop gracefully, sending SIGKILL");
3606
+ process.kill(pid, "SIGKILL");
3607
+ }
3416
3608
  }
3417
3609
  logger.info("Daemon stopped");
3418
3610
  } catch (err) {
@@ -3422,8 +3614,12 @@ function createStopCommand() {
3422
3614
  throw err;
3423
3615
  }
3424
3616
  }
3425
- if (fs2.existsSync(PID_FILE)) {
3426
- fs2.unlinkSync(PID_FILE);
3617
+ const currentPid = readPidFile();
3618
+ if (currentPid === pid) {
3619
+ try {
3620
+ fs3.unlinkSync(PID_FILE);
3621
+ } catch {
3622
+ }
3427
3623
  }
3428
3624
  } catch (error) {
3429
3625
  logger.error(
@@ -3801,7 +3997,7 @@ function createMobileSetupCommand() {
3801
3997
 
3802
3998
  // src/index.ts
3803
3999
  var __filename = fileURLToPath(import.meta.url);
3804
- var __dirname = dirname2(__filename);
4000
+ var __dirname = dirname3(__filename);
3805
4001
  config({ path: resolve2(__dirname, "../.env"), quiet: true });
3806
4002
  var packageJson = JSON.parse(
3807
4003
  readFileSync5(resolve2(__dirname, "../package.json"), "utf-8")