@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 +1 -1
- package/dist/index.js +250 -54
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
1004
|
-
|
|
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
|
|
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
|
-
|
|
1128
|
+
const pendingRequest = { resolve: resolve3, reject };
|
|
1129
|
+
this.pendingAnswerRequest = pendingRequest;
|
|
1079
1130
|
options.signal.addEventListener("abort", () => {
|
|
1080
|
-
this.
|
|
1081
|
-
|
|
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.
|
|
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.
|
|
1499
|
+
if (this.pendingAnswerRequest) {
|
|
1430
1500
|
const resolvedAnswers = answers || { result: answerText };
|
|
1431
|
-
this.
|
|
1432
|
-
this.
|
|
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.
|
|
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.
|
|
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(
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
3573
|
+
const pid = readPidFile();
|
|
3574
|
+
if (pid === null) {
|
|
3390
3575
|
logger.info("No daemon is running");
|
|
3391
3576
|
return;
|
|
3392
3577
|
}
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
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
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
}
|
|
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
|
-
|
|
3415
|
-
|
|
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
|
-
|
|
3426
|
-
|
|
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 =
|
|
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")
|