@tongil_kim/clautunnel 0.1.9
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 +74 -0
- package/bin/clautunnel +3 -0
- package/dist/index.d.ts +151 -0
- package/dist/index.js +3168 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
9
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
|
|
28
|
+
// ../../packages/shared/dist/types/message.js
|
|
29
|
+
var require_message = __commonJS({
|
|
30
|
+
"../../packages/shared/dist/types/message.js"(exports) {
|
|
31
|
+
"use strict";
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// ../../packages/shared/dist/types/session.js
|
|
37
|
+
var require_session = __commonJS({
|
|
38
|
+
"../../packages/shared/dist/types/session.js"(exports) {
|
|
39
|
+
"use strict";
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// ../../packages/shared/dist/types/machine.js
|
|
45
|
+
var require_machine = __commonJS({
|
|
46
|
+
"../../packages/shared/dist/types/machine.js"(exports) {
|
|
47
|
+
"use strict";
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// ../../packages/shared/dist/types/presence.js
|
|
53
|
+
var require_presence = __commonJS({
|
|
54
|
+
"../../packages/shared/dist/types/presence.js"(exports) {
|
|
55
|
+
"use strict";
|
|
56
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ../../packages/shared/dist/types/index.js
|
|
61
|
+
var require_types = __commonJS({
|
|
62
|
+
"../../packages/shared/dist/types/index.js"(exports) {
|
|
63
|
+
"use strict";
|
|
64
|
+
var __createBinding = exports && exports.__createBinding || (Object.create ? (function(o, m, k, k2) {
|
|
65
|
+
if (k2 === void 0) k2 = k;
|
|
66
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
67
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
68
|
+
desc = { enumerable: true, get: function() {
|
|
69
|
+
return m[k];
|
|
70
|
+
} };
|
|
71
|
+
}
|
|
72
|
+
Object.defineProperty(o, k2, desc);
|
|
73
|
+
}) : (function(o, m, k, k2) {
|
|
74
|
+
if (k2 === void 0) k2 = k;
|
|
75
|
+
o[k2] = m[k];
|
|
76
|
+
}));
|
|
77
|
+
var __exportStar = exports && exports.__exportStar || function(m, exports2) {
|
|
78
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports2, p)) __createBinding(exports2, m, p);
|
|
79
|
+
};
|
|
80
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
81
|
+
__exportStar(require_message(), exports);
|
|
82
|
+
__exportStar(require_session(), exports);
|
|
83
|
+
__exportStar(require_machine(), exports);
|
|
84
|
+
__exportStar(require_presence(), exports);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ../../packages/shared/dist/constants/events.js
|
|
89
|
+
var require_events = __commonJS({
|
|
90
|
+
"../../packages/shared/dist/constants/events.js"(exports) {
|
|
91
|
+
"use strict";
|
|
92
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
93
|
+
exports.REALTIME_CHANNELS = void 0;
|
|
94
|
+
exports.REALTIME_CHANNELS = {
|
|
95
|
+
sessionOutput: (sessionId) => `session:${sessionId}:output`,
|
|
96
|
+
sessionInput: (sessionId) => `session:${sessionId}:input`,
|
|
97
|
+
sessionPresence: (sessionId) => `session:${sessionId}:presence`,
|
|
98
|
+
machinePresence: (machineId) => `machine:${machineId}:presence`,
|
|
99
|
+
machineInput: (machineId) => `machine:${machineId}:input`,
|
|
100
|
+
machineOutput: (machineId) => `machine:${machineId}:output`
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ../../packages/shared/dist/constants/index.js
|
|
106
|
+
var require_constants = __commonJS({
|
|
107
|
+
"../../packages/shared/dist/constants/index.js"(exports) {
|
|
108
|
+
"use strict";
|
|
109
|
+
var __createBinding = exports && exports.__createBinding || (Object.create ? (function(o, m, k, k2) {
|
|
110
|
+
if (k2 === void 0) k2 = k;
|
|
111
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
112
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
113
|
+
desc = { enumerable: true, get: function() {
|
|
114
|
+
return m[k];
|
|
115
|
+
} };
|
|
116
|
+
}
|
|
117
|
+
Object.defineProperty(o, k2, desc);
|
|
118
|
+
}) : (function(o, m, k, k2) {
|
|
119
|
+
if (k2 === void 0) k2 = k;
|
|
120
|
+
o[k2] = m[k];
|
|
121
|
+
}));
|
|
122
|
+
var __exportStar = exports && exports.__exportStar || function(m, exports2) {
|
|
123
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports2, p)) __createBinding(exports2, m, p);
|
|
124
|
+
};
|
|
125
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
126
|
+
__exportStar(require_events(), exports);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ../../packages/shared/dist/index.js
|
|
131
|
+
var require_dist = __commonJS({
|
|
132
|
+
"../../packages/shared/dist/index.js"(exports) {
|
|
133
|
+
"use strict";
|
|
134
|
+
var __createBinding = exports && exports.__createBinding || (Object.create ? (function(o, m, k, k2) {
|
|
135
|
+
if (k2 === void 0) k2 = k;
|
|
136
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
137
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
138
|
+
desc = { enumerable: true, get: function() {
|
|
139
|
+
return m[k];
|
|
140
|
+
} };
|
|
141
|
+
}
|
|
142
|
+
Object.defineProperty(o, k2, desc);
|
|
143
|
+
}) : (function(o, m, k, k2) {
|
|
144
|
+
if (k2 === void 0) k2 = k;
|
|
145
|
+
o[k2] = m[k];
|
|
146
|
+
}));
|
|
147
|
+
var __exportStar = exports && exports.__exportStar || function(m, exports2) {
|
|
148
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports2, p)) __createBinding(exports2, m, p);
|
|
149
|
+
};
|
|
150
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
151
|
+
__exportStar(require_types(), exports);
|
|
152
|
+
__exportStar(require_constants(), exports);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// src/index.ts
|
|
157
|
+
import { config } from "dotenv";
|
|
158
|
+
import { resolve, dirname as dirname2 } from "path";
|
|
159
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
160
|
+
import { fileURLToPath } from "url";
|
|
161
|
+
|
|
162
|
+
// src/utils/config.ts
|
|
163
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
164
|
+
import { join } from "path";
|
|
165
|
+
import { homedir } from "os";
|
|
166
|
+
var ConfigurationError = class extends Error {
|
|
167
|
+
constructor(message) {
|
|
168
|
+
super(message);
|
|
169
|
+
this.name = "ConfigurationError";
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
var Config = class {
|
|
173
|
+
configDir;
|
|
174
|
+
configFile;
|
|
175
|
+
data;
|
|
176
|
+
constructor(configDir) {
|
|
177
|
+
this.configDir = configDir ?? join(homedir(), ".clautunnel");
|
|
178
|
+
this.configFile = join(this.configDir, "config.json");
|
|
179
|
+
this.data = this.loadConfig();
|
|
180
|
+
}
|
|
181
|
+
loadConfig() {
|
|
182
|
+
if (existsSync(this.configFile)) {
|
|
183
|
+
try {
|
|
184
|
+
return JSON.parse(readFileSync(this.configFile, "utf-8"));
|
|
185
|
+
} catch {
|
|
186
|
+
return {};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return {};
|
|
190
|
+
}
|
|
191
|
+
saveConfig() {
|
|
192
|
+
if (!existsSync(this.configDir)) {
|
|
193
|
+
mkdirSync(this.configDir, { recursive: true });
|
|
194
|
+
}
|
|
195
|
+
writeFileSync(this.configFile, JSON.stringify(this.data, null, 2));
|
|
196
|
+
}
|
|
197
|
+
getSupabaseUrl() {
|
|
198
|
+
const url = process.env["SUPABASE_URL"] || this.data.supabaseUrl;
|
|
199
|
+
if (!url) {
|
|
200
|
+
throw new Error("SUPABASE_URL environment variable is not set");
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
new URL(url);
|
|
204
|
+
} catch {
|
|
205
|
+
throw new Error("SUPABASE_URL must be a valid URL");
|
|
206
|
+
}
|
|
207
|
+
return url;
|
|
208
|
+
}
|
|
209
|
+
getSupabaseAnonKey() {
|
|
210
|
+
const key = process.env["SUPABASE_ANON_KEY"] || this.data.supabaseAnonKey;
|
|
211
|
+
if (!key) {
|
|
212
|
+
throw new Error("SUPABASE_ANON_KEY environment variable is not set");
|
|
213
|
+
}
|
|
214
|
+
return key;
|
|
215
|
+
}
|
|
216
|
+
setSupabaseCredentials(credentials) {
|
|
217
|
+
this.data.supabaseUrl = credentials.url;
|
|
218
|
+
this.data.supabaseAnonKey = credentials.anonKey;
|
|
219
|
+
this.saveConfig();
|
|
220
|
+
}
|
|
221
|
+
isConfigured() {
|
|
222
|
+
const hasEnvVars = !!(process.env["SUPABASE_URL"] && process.env["SUPABASE_ANON_KEY"]);
|
|
223
|
+
const hasConfigFile = !!(this.data.supabaseUrl && this.data.supabaseAnonKey);
|
|
224
|
+
return hasEnvVars || hasConfigFile;
|
|
225
|
+
}
|
|
226
|
+
requireConfiguration() {
|
|
227
|
+
if (!this.isConfigured()) {
|
|
228
|
+
throw new ConfigurationError(
|
|
229
|
+
'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'
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
getMachineId() {
|
|
234
|
+
return this.data.machineId;
|
|
235
|
+
}
|
|
236
|
+
setMachineId(machineId) {
|
|
237
|
+
this.data.machineId = machineId;
|
|
238
|
+
this.saveConfig();
|
|
239
|
+
}
|
|
240
|
+
getSessionTokens() {
|
|
241
|
+
return this.data.sessionTokens;
|
|
242
|
+
}
|
|
243
|
+
setSessionTokens(tokens) {
|
|
244
|
+
this.data.sessionTokens = tokens;
|
|
245
|
+
this.saveConfig();
|
|
246
|
+
}
|
|
247
|
+
// Alias for setSessionTokens
|
|
248
|
+
setSession(tokens) {
|
|
249
|
+
this.setSessionTokens(tokens);
|
|
250
|
+
}
|
|
251
|
+
clearSessionTokens() {
|
|
252
|
+
delete this.data.sessionTokens;
|
|
253
|
+
this.saveConfig();
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
var defaultConfig = null;
|
|
257
|
+
function getConfig() {
|
|
258
|
+
if (!defaultConfig) {
|
|
259
|
+
defaultConfig = new Config();
|
|
260
|
+
}
|
|
261
|
+
return defaultConfig;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/utils/logger.ts
|
|
265
|
+
var Logger = class {
|
|
266
|
+
silent;
|
|
267
|
+
debugEnabled;
|
|
268
|
+
constructor() {
|
|
269
|
+
this.silent = process.env["SILENT"] === "true";
|
|
270
|
+
this.debugEnabled = process.env["DEBUG"] === "true";
|
|
271
|
+
}
|
|
272
|
+
formatTimestamp() {
|
|
273
|
+
const now = /* @__PURE__ */ new Date();
|
|
274
|
+
return now.toTimeString().split(" ")[0] ?? now.toISOString();
|
|
275
|
+
}
|
|
276
|
+
formatMessage(level, message, data) {
|
|
277
|
+
const timestamp = this.formatTimestamp();
|
|
278
|
+
const levelStr = level.toUpperCase().padEnd(5);
|
|
279
|
+
let formatted = `[${timestamp}] [${levelStr}] ${message}`;
|
|
280
|
+
if (data) {
|
|
281
|
+
formatted += ` ${JSON.stringify(data)}`;
|
|
282
|
+
}
|
|
283
|
+
return formatted;
|
|
284
|
+
}
|
|
285
|
+
debug(message, data) {
|
|
286
|
+
if (this.silent || !this.debugEnabled) return;
|
|
287
|
+
console.log(this.formatMessage("debug", message, data));
|
|
288
|
+
}
|
|
289
|
+
info(message, data) {
|
|
290
|
+
if (this.silent) return;
|
|
291
|
+
console.log(this.formatMessage("info", message, data));
|
|
292
|
+
}
|
|
293
|
+
warn(message, data) {
|
|
294
|
+
if (this.silent) return;
|
|
295
|
+
console.warn(this.formatMessage("warn", message, data));
|
|
296
|
+
}
|
|
297
|
+
error(message, data) {
|
|
298
|
+
if (this.silent) return;
|
|
299
|
+
console.error(this.formatMessage("error", message, data));
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
var defaultLogger = null;
|
|
303
|
+
function getLogger() {
|
|
304
|
+
if (!defaultLogger) {
|
|
305
|
+
defaultLogger = new Logger();
|
|
306
|
+
}
|
|
307
|
+
return defaultLogger;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/realtime/client.ts
|
|
311
|
+
var import_clautunnel_shared = __toESM(require_dist(), 1);
|
|
312
|
+
import { EventEmitter } from "events";
|
|
313
|
+
|
|
314
|
+
// src/realtime/utils.ts
|
|
315
|
+
var DEFAULT_TIMEOUT = 1e4;
|
|
316
|
+
function subscribeWithTimeout(channel, channelName, timeout = DEFAULT_TIMEOUT) {
|
|
317
|
+
return new Promise((resolve2) => {
|
|
318
|
+
const timer = setTimeout(() => {
|
|
319
|
+
console.warn(
|
|
320
|
+
`[WARN] Realtime subscription timeout for ${channelName}. Mobile sync disabled.`
|
|
321
|
+
);
|
|
322
|
+
resolve2(false);
|
|
323
|
+
}, timeout);
|
|
324
|
+
channel.subscribe((status, err) => {
|
|
325
|
+
if (status === "SUBSCRIBED") {
|
|
326
|
+
clearTimeout(timer);
|
|
327
|
+
resolve2(true);
|
|
328
|
+
} else if (status === "CHANNEL_ERROR" || status === "CLOSED" || status === "TIMED_OUT") {
|
|
329
|
+
clearTimeout(timer);
|
|
330
|
+
console.warn(
|
|
331
|
+
`[WARN] Channel ${channelName} ${status.toLowerCase()}. Mobile sync disabled.`
|
|
332
|
+
);
|
|
333
|
+
if (err) {
|
|
334
|
+
console.warn(`[WARN] Error details: ${err.message || err}`);
|
|
335
|
+
}
|
|
336
|
+
resolve2(false);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/realtime/client.ts
|
|
343
|
+
var RealtimeClient = class extends EventEmitter {
|
|
344
|
+
supabase;
|
|
345
|
+
sessionId;
|
|
346
|
+
outputChannel = null;
|
|
347
|
+
inputChannel = null;
|
|
348
|
+
presenceChannel = null;
|
|
349
|
+
seq = 0;
|
|
350
|
+
realtimeEnabled = false;
|
|
351
|
+
constructor(options) {
|
|
352
|
+
super();
|
|
353
|
+
this.supabase = options.supabase;
|
|
354
|
+
this.sessionId = options.sessionId;
|
|
355
|
+
}
|
|
356
|
+
async connect() {
|
|
357
|
+
const outputChannelName = import_clautunnel_shared.REALTIME_CHANNELS.sessionOutput(this.sessionId);
|
|
358
|
+
this.outputChannel = this.supabase.channel(outputChannelName);
|
|
359
|
+
const inputChannelName = import_clautunnel_shared.REALTIME_CHANNELS.sessionInput(this.sessionId);
|
|
360
|
+
this.inputChannel = this.supabase.channel(inputChannelName);
|
|
361
|
+
this.inputChannel.on("broadcast", { event: "input" }, (payload) => {
|
|
362
|
+
this.emit("input", payload.payload);
|
|
363
|
+
});
|
|
364
|
+
const results = await Promise.all([
|
|
365
|
+
subscribeWithTimeout(this.outputChannel, "output"),
|
|
366
|
+
subscribeWithTimeout(this.inputChannel, "input")
|
|
367
|
+
]);
|
|
368
|
+
this.realtimeEnabled = results.every((success) => success);
|
|
369
|
+
if (this.realtimeEnabled) {
|
|
370
|
+
const presenceChannelName = import_clautunnel_shared.REALTIME_CHANNELS.sessionPresence(this.sessionId);
|
|
371
|
+
this.presenceChannel = this.supabase.channel(presenceChannelName);
|
|
372
|
+
this.presenceChannel.subscribe(async (status, err) => {
|
|
373
|
+
if (status === "SUBSCRIBED" && this.presenceChannel) {
|
|
374
|
+
try {
|
|
375
|
+
const payload = {
|
|
376
|
+
type: "cli",
|
|
377
|
+
online_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
378
|
+
};
|
|
379
|
+
await this.presenceChannel.track(payload);
|
|
380
|
+
} catch (trackError) {
|
|
381
|
+
console.warn("[WARN] Failed to track presence:", trackError);
|
|
382
|
+
}
|
|
383
|
+
} else if (status === "CHANNEL_ERROR") {
|
|
384
|
+
console.warn("[WARN] Presence channel error:", err?.message || "Unknown error");
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
this.emit("connected");
|
|
389
|
+
}
|
|
390
|
+
async disconnect() {
|
|
391
|
+
if (this.presenceChannel) {
|
|
392
|
+
await this.presenceChannel.untrack();
|
|
393
|
+
await this.supabase.removeChannel(this.presenceChannel);
|
|
394
|
+
this.presenceChannel = null;
|
|
395
|
+
}
|
|
396
|
+
if (this.outputChannel) {
|
|
397
|
+
await this.supabase.removeChannel(this.outputChannel);
|
|
398
|
+
this.outputChannel = null;
|
|
399
|
+
}
|
|
400
|
+
if (this.inputChannel) {
|
|
401
|
+
await this.supabase.removeChannel(this.inputChannel);
|
|
402
|
+
this.inputChannel = null;
|
|
403
|
+
}
|
|
404
|
+
this.emit("disconnected");
|
|
405
|
+
}
|
|
406
|
+
async broadcast(content) {
|
|
407
|
+
if (!this.outputChannel) {
|
|
408
|
+
throw new Error("Not connected");
|
|
409
|
+
}
|
|
410
|
+
const message = {
|
|
411
|
+
type: "output",
|
|
412
|
+
content,
|
|
413
|
+
timestamp: Date.now(),
|
|
414
|
+
seq: ++this.seq
|
|
415
|
+
};
|
|
416
|
+
try {
|
|
417
|
+
const { error } = await this.supabase.from("messages").insert({
|
|
418
|
+
session_id: this.sessionId,
|
|
419
|
+
type: message.type,
|
|
420
|
+
content: message.content,
|
|
421
|
+
seq: message.seq
|
|
422
|
+
});
|
|
423
|
+
if (error) {
|
|
424
|
+
console.warn("[WARN] Failed to persist message:", error.message);
|
|
425
|
+
}
|
|
426
|
+
} catch (error) {
|
|
427
|
+
console.warn("[WARN] Failed to persist message:", error);
|
|
428
|
+
}
|
|
429
|
+
if (!this.realtimeEnabled) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
try {
|
|
433
|
+
await this.outputChannel.send({
|
|
434
|
+
type: "broadcast",
|
|
435
|
+
event: "output",
|
|
436
|
+
payload: message
|
|
437
|
+
});
|
|
438
|
+
} catch (error) {
|
|
439
|
+
throw error;
|
|
440
|
+
}
|
|
441
|
+
this.emit("broadcast", message);
|
|
442
|
+
}
|
|
443
|
+
async broadcastMode(mode) {
|
|
444
|
+
if (!this.outputChannel) {
|
|
445
|
+
throw new Error("Not connected");
|
|
446
|
+
}
|
|
447
|
+
if (!this.realtimeEnabled) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const message = {
|
|
451
|
+
type: "mode",
|
|
452
|
+
permissionMode: mode,
|
|
453
|
+
timestamp: Date.now(),
|
|
454
|
+
seq: ++this.seq
|
|
455
|
+
};
|
|
456
|
+
await this.outputChannel.send({
|
|
457
|
+
type: "broadcast",
|
|
458
|
+
event: "output",
|
|
459
|
+
payload: message
|
|
460
|
+
});
|
|
461
|
+
this.emit("broadcast", message);
|
|
462
|
+
}
|
|
463
|
+
async broadcastCommands(commands) {
|
|
464
|
+
if (!this.outputChannel) {
|
|
465
|
+
throw new Error("Not connected");
|
|
466
|
+
}
|
|
467
|
+
if (!this.realtimeEnabled) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const message = {
|
|
471
|
+
type: "commands",
|
|
472
|
+
commands,
|
|
473
|
+
timestamp: Date.now(),
|
|
474
|
+
seq: ++this.seq
|
|
475
|
+
};
|
|
476
|
+
await this.outputChannel.send({
|
|
477
|
+
type: "broadcast",
|
|
478
|
+
event: "output",
|
|
479
|
+
payload: message
|
|
480
|
+
});
|
|
481
|
+
this.emit("broadcast", message);
|
|
482
|
+
}
|
|
483
|
+
async broadcastModel(model) {
|
|
484
|
+
if (!this.outputChannel) {
|
|
485
|
+
throw new Error("Not connected");
|
|
486
|
+
}
|
|
487
|
+
if (!this.realtimeEnabled) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const message = {
|
|
491
|
+
type: "model",
|
|
492
|
+
model,
|
|
493
|
+
timestamp: Date.now(),
|
|
494
|
+
seq: ++this.seq
|
|
495
|
+
};
|
|
496
|
+
await this.outputChannel.send({
|
|
497
|
+
type: "broadcast",
|
|
498
|
+
event: "output",
|
|
499
|
+
payload: message
|
|
500
|
+
});
|
|
501
|
+
this.emit("broadcast", message);
|
|
502
|
+
}
|
|
503
|
+
async broadcastModels(models) {
|
|
504
|
+
if (!this.outputChannel) {
|
|
505
|
+
throw new Error("Not connected");
|
|
506
|
+
}
|
|
507
|
+
if (!this.realtimeEnabled) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
const message = {
|
|
511
|
+
type: "models",
|
|
512
|
+
availableModels: models,
|
|
513
|
+
timestamp: Date.now(),
|
|
514
|
+
seq: ++this.seq
|
|
515
|
+
};
|
|
516
|
+
await this.outputChannel.send({
|
|
517
|
+
type: "broadcast",
|
|
518
|
+
event: "output",
|
|
519
|
+
payload: message
|
|
520
|
+
});
|
|
521
|
+
this.emit("broadcast", message);
|
|
522
|
+
}
|
|
523
|
+
async broadcastSystem(content) {
|
|
524
|
+
if (!this.outputChannel) {
|
|
525
|
+
throw new Error("Not connected");
|
|
526
|
+
}
|
|
527
|
+
const message = {
|
|
528
|
+
type: "system",
|
|
529
|
+
content,
|
|
530
|
+
timestamp: Date.now(),
|
|
531
|
+
seq: ++this.seq
|
|
532
|
+
};
|
|
533
|
+
try {
|
|
534
|
+
const { error } = await this.supabase.from("messages").insert({
|
|
535
|
+
session_id: this.sessionId,
|
|
536
|
+
type: message.type,
|
|
537
|
+
content: message.content,
|
|
538
|
+
seq: message.seq
|
|
539
|
+
});
|
|
540
|
+
if (error) {
|
|
541
|
+
console.warn("[WARN] Failed to persist system message:", error.message);
|
|
542
|
+
}
|
|
543
|
+
} catch (error) {
|
|
544
|
+
console.warn("[WARN] Failed to persist system message:", error);
|
|
545
|
+
}
|
|
546
|
+
if (!this.realtimeEnabled) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
await this.outputChannel.send({
|
|
551
|
+
type: "broadcast",
|
|
552
|
+
event: "output",
|
|
553
|
+
payload: message
|
|
554
|
+
});
|
|
555
|
+
} catch (error) {
|
|
556
|
+
throw error;
|
|
557
|
+
}
|
|
558
|
+
this.emit("broadcast", message);
|
|
559
|
+
}
|
|
560
|
+
async broadcastInteractiveResponse(data) {
|
|
561
|
+
if (!this.outputChannel) {
|
|
562
|
+
throw new Error("Not connected");
|
|
563
|
+
}
|
|
564
|
+
if (!this.realtimeEnabled) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
const message = {
|
|
568
|
+
type: "interactive-response",
|
|
569
|
+
interactiveData: data,
|
|
570
|
+
timestamp: Date.now(),
|
|
571
|
+
seq: ++this.seq
|
|
572
|
+
};
|
|
573
|
+
await this.outputChannel.send({
|
|
574
|
+
type: "broadcast",
|
|
575
|
+
event: "output",
|
|
576
|
+
payload: message
|
|
577
|
+
});
|
|
578
|
+
this.emit("broadcast", message);
|
|
579
|
+
}
|
|
580
|
+
async broadcastInteractiveConfirm(command, result) {
|
|
581
|
+
if (!this.outputChannel) {
|
|
582
|
+
throw new Error("Not connected");
|
|
583
|
+
}
|
|
584
|
+
if (!this.realtimeEnabled) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const message = {
|
|
588
|
+
type: "interactive-confirm",
|
|
589
|
+
interactiveCommand: command,
|
|
590
|
+
interactiveResult: result,
|
|
591
|
+
timestamp: Date.now(),
|
|
592
|
+
seq: ++this.seq
|
|
593
|
+
};
|
|
594
|
+
await this.outputChannel.send({
|
|
595
|
+
type: "broadcast",
|
|
596
|
+
event: "output",
|
|
597
|
+
payload: message
|
|
598
|
+
});
|
|
599
|
+
this.emit("broadcast", message);
|
|
600
|
+
}
|
|
601
|
+
async broadcastResumeHistory(historySessionId) {
|
|
602
|
+
if (!this.outputChannel) {
|
|
603
|
+
throw new Error("Not connected");
|
|
604
|
+
}
|
|
605
|
+
if (!this.realtimeEnabled) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const message = {
|
|
609
|
+
type: "resume-history",
|
|
610
|
+
historySessionId,
|
|
611
|
+
timestamp: Date.now(),
|
|
612
|
+
seq: ++this.seq
|
|
613
|
+
};
|
|
614
|
+
await this.outputChannel.send({
|
|
615
|
+
type: "broadcast",
|
|
616
|
+
event: "output",
|
|
617
|
+
payload: message
|
|
618
|
+
});
|
|
619
|
+
this.emit("broadcast", message);
|
|
620
|
+
}
|
|
621
|
+
async broadcastUserQuestion(questionData) {
|
|
622
|
+
if (!this.outputChannel) {
|
|
623
|
+
throw new Error("Not connected");
|
|
624
|
+
}
|
|
625
|
+
if (!this.realtimeEnabled) {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
const message = {
|
|
629
|
+
type: "user-question",
|
|
630
|
+
userQuestion: questionData,
|
|
631
|
+
timestamp: Date.now(),
|
|
632
|
+
seq: ++this.seq
|
|
633
|
+
};
|
|
634
|
+
await this.outputChannel.send({
|
|
635
|
+
type: "broadcast",
|
|
636
|
+
event: "output",
|
|
637
|
+
payload: message
|
|
638
|
+
});
|
|
639
|
+
this.emit("broadcast", message);
|
|
640
|
+
}
|
|
641
|
+
async broadcastPermissionRequest(requestData) {
|
|
642
|
+
if (!this.outputChannel) {
|
|
643
|
+
throw new Error("Not connected");
|
|
644
|
+
}
|
|
645
|
+
if (!this.realtimeEnabled) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const message = {
|
|
649
|
+
type: "permission-request",
|
|
650
|
+
permissionRequest: requestData,
|
|
651
|
+
timestamp: Date.now(),
|
|
652
|
+
seq: ++this.seq
|
|
653
|
+
};
|
|
654
|
+
await this.outputChannel.send({
|
|
655
|
+
type: "broadcast",
|
|
656
|
+
event: "output",
|
|
657
|
+
payload: message
|
|
658
|
+
});
|
|
659
|
+
this.emit("broadcast", message);
|
|
660
|
+
}
|
|
661
|
+
async broadcastToolUse(toolUseData) {
|
|
662
|
+
if (!this.outputChannel) {
|
|
663
|
+
throw new Error("Not connected");
|
|
664
|
+
}
|
|
665
|
+
const message = {
|
|
666
|
+
type: "tool-use",
|
|
667
|
+
toolUseData,
|
|
668
|
+
content: JSON.stringify(toolUseData),
|
|
669
|
+
timestamp: Date.now(),
|
|
670
|
+
seq: ++this.seq
|
|
671
|
+
};
|
|
672
|
+
try {
|
|
673
|
+
const { error } = await this.supabase.from("messages").insert({
|
|
674
|
+
session_id: this.sessionId,
|
|
675
|
+
type: message.type,
|
|
676
|
+
content: message.content,
|
|
677
|
+
seq: message.seq
|
|
678
|
+
});
|
|
679
|
+
if (error) {
|
|
680
|
+
console.warn("[WARN] Failed to persist tool-use message:", error.message);
|
|
681
|
+
}
|
|
682
|
+
} catch (error) {
|
|
683
|
+
console.warn("[WARN] Failed to persist tool-use message:", error);
|
|
684
|
+
}
|
|
685
|
+
if (!this.realtimeEnabled) {
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
try {
|
|
689
|
+
await this.outputChannel.send({
|
|
690
|
+
type: "broadcast",
|
|
691
|
+
event: "output",
|
|
692
|
+
payload: message
|
|
693
|
+
});
|
|
694
|
+
} catch (error) {
|
|
695
|
+
throw error;
|
|
696
|
+
}
|
|
697
|
+
this.emit("broadcast", message);
|
|
698
|
+
}
|
|
699
|
+
async broadcastComplete() {
|
|
700
|
+
if (!this.outputChannel) {
|
|
701
|
+
throw new Error("Not connected");
|
|
702
|
+
}
|
|
703
|
+
if (!this.realtimeEnabled) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const message = {
|
|
707
|
+
type: "complete",
|
|
708
|
+
timestamp: Date.now(),
|
|
709
|
+
seq: ++this.seq
|
|
710
|
+
};
|
|
711
|
+
await this.outputChannel.send({
|
|
712
|
+
type: "broadcast",
|
|
713
|
+
event: "output",
|
|
714
|
+
payload: message
|
|
715
|
+
});
|
|
716
|
+
this.emit("broadcast", message);
|
|
717
|
+
}
|
|
718
|
+
async broadcastSessionTitle(title) {
|
|
719
|
+
if (!this.outputChannel) {
|
|
720
|
+
throw new Error("Not connected");
|
|
721
|
+
}
|
|
722
|
+
if (!this.realtimeEnabled) {
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const message = {
|
|
726
|
+
type: "session-title",
|
|
727
|
+
sessionTitle: title,
|
|
728
|
+
timestamp: Date.now(),
|
|
729
|
+
seq: ++this.seq
|
|
730
|
+
};
|
|
731
|
+
await this.outputChannel.send({
|
|
732
|
+
type: "broadcast",
|
|
733
|
+
event: "output",
|
|
734
|
+
payload: message
|
|
735
|
+
});
|
|
736
|
+
this.emit("broadcast", message);
|
|
737
|
+
}
|
|
738
|
+
async broadcastQueued() {
|
|
739
|
+
if (!this.outputChannel) {
|
|
740
|
+
throw new Error("Not connected");
|
|
741
|
+
}
|
|
742
|
+
if (!this.realtimeEnabled) {
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
const message = {
|
|
746
|
+
type: "request-queued",
|
|
747
|
+
timestamp: Date.now(),
|
|
748
|
+
seq: ++this.seq
|
|
749
|
+
};
|
|
750
|
+
await this.outputChannel.send({
|
|
751
|
+
type: "broadcast",
|
|
752
|
+
event: "output",
|
|
753
|
+
payload: message
|
|
754
|
+
});
|
|
755
|
+
this.emit("broadcast", message);
|
|
756
|
+
}
|
|
757
|
+
async broadcastError(errorMessage, errorCode) {
|
|
758
|
+
if (!this.outputChannel) {
|
|
759
|
+
throw new Error("Not connected");
|
|
760
|
+
}
|
|
761
|
+
if (!this.realtimeEnabled) {
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
const message = {
|
|
765
|
+
type: "error",
|
|
766
|
+
content: errorMessage,
|
|
767
|
+
errorCode: errorCode || "unknown",
|
|
768
|
+
timestamp: Date.now(),
|
|
769
|
+
seq: ++this.seq
|
|
770
|
+
};
|
|
771
|
+
await this.outputChannel.send({
|
|
772
|
+
type: "broadcast",
|
|
773
|
+
event: "output",
|
|
774
|
+
payload: message
|
|
775
|
+
});
|
|
776
|
+
this.emit("broadcast", message);
|
|
777
|
+
}
|
|
778
|
+
getSeq() {
|
|
779
|
+
return this.seq;
|
|
780
|
+
}
|
|
781
|
+
isConnected() {
|
|
782
|
+
return this.outputChannel !== null && this.inputChannel !== null;
|
|
783
|
+
}
|
|
784
|
+
isRealtimeEnabled() {
|
|
785
|
+
return this.realtimeEnabled;
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
// src/daemon/session.ts
|
|
790
|
+
var SessionManager = class {
|
|
791
|
+
supabase;
|
|
792
|
+
constructor(options) {
|
|
793
|
+
this.supabase = options.supabase;
|
|
794
|
+
}
|
|
795
|
+
async createSession(machineId, workingDirectory) {
|
|
796
|
+
const { data, error } = await this.supabase.from("sessions").insert({
|
|
797
|
+
machine_id: machineId,
|
|
798
|
+
status: "active",
|
|
799
|
+
working_directory: workingDirectory,
|
|
800
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
801
|
+
}).select().single();
|
|
802
|
+
if (error) {
|
|
803
|
+
throw new Error(`Failed to create session: ${error.message}`);
|
|
804
|
+
}
|
|
805
|
+
return data;
|
|
806
|
+
}
|
|
807
|
+
async endSession(sessionId) {
|
|
808
|
+
const { error } = await this.supabase.from("sessions").update({
|
|
809
|
+
status: "ended",
|
|
810
|
+
ended_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
811
|
+
}).eq("id", sessionId);
|
|
812
|
+
if (error) {
|
|
813
|
+
throw new Error(`Failed to end session: ${error.message}`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
async getSession(sessionId) {
|
|
817
|
+
const { data, error } = await this.supabase.from("sessions").select().eq("id", sessionId).single();
|
|
818
|
+
if (error) {
|
|
819
|
+
if (error.code === "PGRST116") {
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
throw new Error(`Failed to get session: ${error.message}`);
|
|
823
|
+
}
|
|
824
|
+
return data;
|
|
825
|
+
}
|
|
826
|
+
async updateSessionStatus(sessionId, status) {
|
|
827
|
+
const updates = { status };
|
|
828
|
+
if (status === "ended") {
|
|
829
|
+
updates.ended_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
830
|
+
}
|
|
831
|
+
const { error } = await this.supabase.from("sessions").update(updates).eq("id", sessionId);
|
|
832
|
+
if (error) {
|
|
833
|
+
throw new Error(`Failed to update session status: ${error.message}`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
async updateSessionModel(sessionId, model) {
|
|
837
|
+
const { error } = await this.supabase.from("sessions").update({ model }).eq("id", sessionId);
|
|
838
|
+
if (error) {
|
|
839
|
+
throw new Error(`Failed to update session model: ${error.message}`);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
async updateSdkSessionId(sessionId, sdkSessionId) {
|
|
843
|
+
const { error } = await this.supabase.from("sessions").update({ sdk_session_id: sdkSessionId }).eq("id", sessionId);
|
|
844
|
+
if (error) {
|
|
845
|
+
throw new Error(`Failed to update SDK session ID: ${error.message}`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
async updateSessionTitle(sessionId, title) {
|
|
849
|
+
const { error } = await this.supabase.from("sessions").update({ title }).eq("id", sessionId);
|
|
850
|
+
if (error) {
|
|
851
|
+
throw new Error(`Failed to update session title: ${error.message}`);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
// src/daemon/machine.ts
|
|
857
|
+
import * as os from "os";
|
|
858
|
+
var MachineManager = class {
|
|
859
|
+
supabase;
|
|
860
|
+
constructor(options) {
|
|
861
|
+
this.supabase = options.supabase;
|
|
862
|
+
}
|
|
863
|
+
async registerMachine(userId, name, machineId) {
|
|
864
|
+
const hostname2 = os.hostname();
|
|
865
|
+
const machineName = name || hostname2;
|
|
866
|
+
if (machineId) {
|
|
867
|
+
const existing = await this.getMachine(machineId);
|
|
868
|
+
if (existing) {
|
|
869
|
+
await this.updateMachineStatus(machineId, "online");
|
|
870
|
+
return existing;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
const { data: existingByHostname } = await this.supabase.from("machines").select().eq("user_id", userId).eq("hostname", hostname2).single();
|
|
874
|
+
if (existingByHostname) {
|
|
875
|
+
await this.updateMachineStatus(existingByHostname.id, "online");
|
|
876
|
+
return existingByHostname;
|
|
877
|
+
}
|
|
878
|
+
const { data, error } = await this.supabase.from("machines").insert({
|
|
879
|
+
user_id: userId,
|
|
880
|
+
name: machineName,
|
|
881
|
+
hostname: hostname2,
|
|
882
|
+
status: "online",
|
|
883
|
+
last_seen_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
884
|
+
}).select().single();
|
|
885
|
+
if (error) {
|
|
886
|
+
throw new Error(`Failed to register machine: ${error.message}`);
|
|
887
|
+
}
|
|
888
|
+
return data;
|
|
889
|
+
}
|
|
890
|
+
async getMachine(machineId) {
|
|
891
|
+
const { data, error } = await this.supabase.from("machines").select().eq("id", machineId).single();
|
|
892
|
+
if (error) {
|
|
893
|
+
if (error.code === "PGRST116") {
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
throw new Error(`Failed to get machine: ${error.message}`);
|
|
897
|
+
}
|
|
898
|
+
return data;
|
|
899
|
+
}
|
|
900
|
+
async updateMachineStatus(machineId, status) {
|
|
901
|
+
const { error } = await this.supabase.from("machines").update({
|
|
902
|
+
status,
|
|
903
|
+
last_seen_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
904
|
+
}).eq("id", machineId);
|
|
905
|
+
if (error) {
|
|
906
|
+
throw new Error(`Failed to update machine status: ${error.message}`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
async heartbeat(machineId) {
|
|
910
|
+
const { error } = await this.supabase.from("machines").update({
|
|
911
|
+
last_seen_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
912
|
+
}).eq("id", machineId);
|
|
913
|
+
if (error) {
|
|
914
|
+
throw new Error(`Failed to update heartbeat: ${error.message}`);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
async listMachines(userId) {
|
|
918
|
+
const { data, error } = await this.supabase.from("machines").select().eq("user_id", userId).order("last_seen_at", { ascending: false });
|
|
919
|
+
if (error) {
|
|
920
|
+
throw new Error(`Failed to list machines: ${error.message}`);
|
|
921
|
+
}
|
|
922
|
+
return data;
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
// src/daemon/daemon.ts
|
|
927
|
+
import { EventEmitter as EventEmitter3 } from "events";
|
|
928
|
+
|
|
929
|
+
// src/daemon/sdk-session.ts
|
|
930
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
931
|
+
import * as fs from "fs";
|
|
932
|
+
import * as path from "path";
|
|
933
|
+
import * as os2 from "os";
|
|
934
|
+
import { unstable_v2_createSession, unstable_v2_resumeSession } from "@anthropic-ai/claude-agent-sdk";
|
|
935
|
+
import { v4 as uuidv4 } from "uuid";
|
|
936
|
+
var MAX_TOOL_CONTENT = 1e4;
|
|
937
|
+
var UNSUPPORTED_COMMANDS = /* @__PURE__ */ new Set([
|
|
938
|
+
"keybindings-help",
|
|
939
|
+
"help",
|
|
940
|
+
"context",
|
|
941
|
+
"cost",
|
|
942
|
+
"release-notes",
|
|
943
|
+
"vim",
|
|
944
|
+
"mcp",
|
|
945
|
+
"agents",
|
|
946
|
+
"hooks",
|
|
947
|
+
"status"
|
|
948
|
+
]);
|
|
949
|
+
var SdkSession = class extends EventEmitter2 {
|
|
950
|
+
options;
|
|
951
|
+
sessionId = null;
|
|
952
|
+
isProcessing = false;
|
|
953
|
+
currentPermissionMode;
|
|
954
|
+
v2Session = null;
|
|
955
|
+
streamLoopRunning = false;
|
|
956
|
+
cachedCommands = null;
|
|
957
|
+
cachedModels = null;
|
|
958
|
+
currentModel = "opus";
|
|
959
|
+
conversationHistory = [];
|
|
960
|
+
pendingContextTransfer = false;
|
|
961
|
+
thinkingEnabled = false;
|
|
962
|
+
pendingPermissionRequests = /* @__PURE__ */ new Map();
|
|
963
|
+
// Pending AskUserQuestion answer resolver - resolved by provideAnswer()
|
|
964
|
+
pendingAnswerResolve = null;
|
|
965
|
+
// Track pending question/permission data for re-broadcast on status-request
|
|
966
|
+
pendingQuestionData = null;
|
|
967
|
+
pendingPermissionData = null;
|
|
968
|
+
// Track current assistant response text for conversation history
|
|
969
|
+
currentAssistantResponse = "";
|
|
970
|
+
// Queued prompt to send after current processing completes (last-one-wins)
|
|
971
|
+
pendingPrompt = null;
|
|
972
|
+
constructor(options) {
|
|
973
|
+
super();
|
|
974
|
+
this.options = options;
|
|
975
|
+
this.currentPermissionMode = options.permissionMode || "default";
|
|
976
|
+
this.currentModel = options.model || "opus";
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Handle permission response from mobile
|
|
980
|
+
*/
|
|
981
|
+
handlePermissionResponse(response) {
|
|
982
|
+
const pending = this.pendingPermissionRequests.get(response.requestId);
|
|
983
|
+
if (!pending) {
|
|
984
|
+
console.warn(`[WARN] No pending permission request found for ID: ${response.requestId}`);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
this.pendingPermissionRequests.delete(response.requestId);
|
|
988
|
+
this.pendingPermissionData = null;
|
|
989
|
+
if (response.behavior === "allow") {
|
|
990
|
+
const result = {
|
|
991
|
+
behavior: "allow",
|
|
992
|
+
updatedInput: response.updatedInput,
|
|
993
|
+
updatedPermissions: response.updatedPermissions
|
|
994
|
+
};
|
|
995
|
+
pending.resolve(result);
|
|
996
|
+
} else {
|
|
997
|
+
const result = {
|
|
998
|
+
behavior: "deny",
|
|
999
|
+
message: response.message || "Permission denied by user"
|
|
1000
|
+
};
|
|
1001
|
+
pending.resolve(result);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Create canUseTool callback for SDK
|
|
1006
|
+
*
|
|
1007
|
+
* AskUserQuestion goes through this path: the subprocess sends a
|
|
1008
|
+
* control_request with subtype "can_use_tool" and blocks until a
|
|
1009
|
+
* control_response is returned. For AskUserQuestion we emit a
|
|
1010
|
+
* user-question event, wait for provideAnswer() to resolve the pending
|
|
1011
|
+
* promise, and return {behavior:'allow', updatedInput:{questions, answers}}
|
|
1012
|
+
* so the subprocess can produce the tool_result and continue.
|
|
1013
|
+
*/
|
|
1014
|
+
createCanUseTool() {
|
|
1015
|
+
return async (toolName, input, options) => {
|
|
1016
|
+
if (toolName === "AskUserQuestion") {
|
|
1017
|
+
const questionInput = input;
|
|
1018
|
+
if (questionInput.questions && Array.isArray(questionInput.questions)) {
|
|
1019
|
+
const questions = questionInput.questions.map((q) => ({
|
|
1020
|
+
question: q.question,
|
|
1021
|
+
header: q.header,
|
|
1022
|
+
options: (q.options || []).map((o) => ({
|
|
1023
|
+
label: o.label,
|
|
1024
|
+
description: o.description
|
|
1025
|
+
})),
|
|
1026
|
+
multiSelect: q.multiSelect
|
|
1027
|
+
}));
|
|
1028
|
+
const questionData = {
|
|
1029
|
+
toolUseId: options.toolUseID,
|
|
1030
|
+
questions
|
|
1031
|
+
};
|
|
1032
|
+
this.pendingQuestionData = questionData;
|
|
1033
|
+
this.emit("user-question", questionData);
|
|
1034
|
+
const answers = await new Promise(
|
|
1035
|
+
(resolve2, reject) => {
|
|
1036
|
+
this.pendingAnswerResolve = resolve2;
|
|
1037
|
+
options.signal.addEventListener("abort", () => {
|
|
1038
|
+
this.pendingAnswerResolve = null;
|
|
1039
|
+
this.pendingQuestionData = null;
|
|
1040
|
+
reject(new Error("Question aborted"));
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
);
|
|
1044
|
+
this.pendingAnswerResolve = null;
|
|
1045
|
+
this.pendingQuestionData = null;
|
|
1046
|
+
return {
|
|
1047
|
+
behavior: "allow",
|
|
1048
|
+
updatedInput: {
|
|
1049
|
+
questions: questionInput.questions,
|
|
1050
|
+
answers
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
const requestId = uuidv4();
|
|
1056
|
+
const suggestions = options.suggestions?.map((s) => {
|
|
1057
|
+
if (s.type === "addRules" || s.type === "replaceRules" || s.type === "removeRules") {
|
|
1058
|
+
return {
|
|
1059
|
+
type: s.type,
|
|
1060
|
+
rules: s.rules,
|
|
1061
|
+
behavior: s.behavior,
|
|
1062
|
+
destination: s.destination
|
|
1063
|
+
};
|
|
1064
|
+
} else if (s.type === "setMode") {
|
|
1065
|
+
return {
|
|
1066
|
+
type: "setMode",
|
|
1067
|
+
mode: s.mode,
|
|
1068
|
+
destination: s.destination
|
|
1069
|
+
};
|
|
1070
|
+
} else if (s.type === "addDirectories" || s.type === "removeDirectories") {
|
|
1071
|
+
return {
|
|
1072
|
+
type: s.type,
|
|
1073
|
+
directories: s.directories,
|
|
1074
|
+
destination: s.destination
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
return s;
|
|
1078
|
+
});
|
|
1079
|
+
const requestData = {
|
|
1080
|
+
requestId,
|
|
1081
|
+
toolName,
|
|
1082
|
+
toolInput: input,
|
|
1083
|
+
toolUseId: options.toolUseID,
|
|
1084
|
+
suggestions,
|
|
1085
|
+
blockedPath: options.blockedPath,
|
|
1086
|
+
decisionReason: options.decisionReason,
|
|
1087
|
+
agentId: options.agentID
|
|
1088
|
+
};
|
|
1089
|
+
this.pendingPermissionData = requestData;
|
|
1090
|
+
this.emit("permission-request", requestData);
|
|
1091
|
+
return new Promise((resolve2, reject) => {
|
|
1092
|
+
this.pendingPermissionRequests.set(requestId, {
|
|
1093
|
+
resolve: resolve2,
|
|
1094
|
+
reject,
|
|
1095
|
+
signal: options.signal
|
|
1096
|
+
});
|
|
1097
|
+
options.signal.addEventListener("abort", () => {
|
|
1098
|
+
this.pendingPermissionRequests.delete(requestId);
|
|
1099
|
+
this.pendingPermissionData = null;
|
|
1100
|
+
reject(new Error("Permission request aborted"));
|
|
1101
|
+
});
|
|
1102
|
+
});
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
setPermissionMode(mode) {
|
|
1106
|
+
this.currentPermissionMode = mode;
|
|
1107
|
+
this.emit("permission-mode", mode);
|
|
1108
|
+
}
|
|
1109
|
+
getPermissionMode() {
|
|
1110
|
+
return this.currentPermissionMode;
|
|
1111
|
+
}
|
|
1112
|
+
async setThinkingMode(enabled) {
|
|
1113
|
+
this.thinkingEnabled = enabled;
|
|
1114
|
+
this.emit("thinking-mode", enabled);
|
|
1115
|
+
}
|
|
1116
|
+
getThinkingMode() {
|
|
1117
|
+
return this.thinkingEnabled;
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Build V2 session options from current state
|
|
1121
|
+
*/
|
|
1122
|
+
buildSessionOptions() {
|
|
1123
|
+
const opts = {
|
|
1124
|
+
// SDK accepts shorthand model names: 'opus' | 'sonnet' | 'haiku'
|
|
1125
|
+
// Full model IDs (e.g. 'claude-opus-4-6') are also valid but shorthand is preferred
|
|
1126
|
+
model: this.currentModel,
|
|
1127
|
+
allowedTools: this.options.allowedTools || ["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
|
|
1128
|
+
canUseTool: this.createCanUseTool(),
|
|
1129
|
+
permissionMode: this.currentPermissionMode
|
|
1130
|
+
};
|
|
1131
|
+
return opts;
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Ensure a V2 session exists, creating one if needed
|
|
1135
|
+
*/
|
|
1136
|
+
ensureSession() {
|
|
1137
|
+
if (!this.v2Session) {
|
|
1138
|
+
const opts = this.buildSessionOptions();
|
|
1139
|
+
if (this.sessionId) {
|
|
1140
|
+
this.v2Session = unstable_v2_resumeSession(this.sessionId, opts);
|
|
1141
|
+
} else {
|
|
1142
|
+
this.v2Session = unstable_v2_createSession(opts);
|
|
1143
|
+
}
|
|
1144
|
+
this.startStreamLoop();
|
|
1145
|
+
}
|
|
1146
|
+
return this.v2Session;
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Background loop that processes messages from the V2 session stream.
|
|
1150
|
+
* Runs continuously for the lifetime of the session.
|
|
1151
|
+
*/
|
|
1152
|
+
async startStreamLoop() {
|
|
1153
|
+
if (this.streamLoopRunning || !this.v2Session) return;
|
|
1154
|
+
this.streamLoopRunning = true;
|
|
1155
|
+
try {
|
|
1156
|
+
while (this.v2Session && !this.v2Session["closed"]) {
|
|
1157
|
+
for await (const message of this.v2Session.stream()) {
|
|
1158
|
+
this.processMessage(message);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
} catch (error) {
|
|
1162
|
+
if (error.name !== "AbortError") {
|
|
1163
|
+
this.emit("error", error);
|
|
1164
|
+
}
|
|
1165
|
+
} finally {
|
|
1166
|
+
this.streamLoopRunning = false;
|
|
1167
|
+
if (this.isProcessing) {
|
|
1168
|
+
this.isProcessing = false;
|
|
1169
|
+
this.pendingPrompt = null;
|
|
1170
|
+
this.emit("complete");
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Process a single SDK message from the stream
|
|
1176
|
+
*/
|
|
1177
|
+
processMessage(message) {
|
|
1178
|
+
if (message.type === "system" && "subtype" in message && message.subtype === "init") {
|
|
1179
|
+
this.sessionId = message.session_id;
|
|
1180
|
+
this.emit("session-started", this.sessionId);
|
|
1181
|
+
if ("permissionMode" in message) {
|
|
1182
|
+
this.emit("permission-mode", message.permissionMode);
|
|
1183
|
+
}
|
|
1184
|
+
if ("slash_commands" in message && Array.isArray(message.slash_commands)) {
|
|
1185
|
+
this.cachedCommands = message.slash_commands.map((cmd) => {
|
|
1186
|
+
if (typeof cmd === "string") {
|
|
1187
|
+
return { name: cmd, description: "", argumentHint: "" };
|
|
1188
|
+
} else if (typeof cmd === "object" && cmd !== null) {
|
|
1189
|
+
const cmdObj = cmd;
|
|
1190
|
+
return {
|
|
1191
|
+
name: cmdObj.name || "",
|
|
1192
|
+
description: cmdObj.description || "",
|
|
1193
|
+
argumentHint: cmdObj.argumentHint || ""
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
return { name: String(cmd), description: "", argumentHint: "" };
|
|
1197
|
+
});
|
|
1198
|
+
this.emit("commands-updated", this.cachedCommands);
|
|
1199
|
+
}
|
|
1200
|
+
} else if (message.type === "assistant") {
|
|
1201
|
+
if (message.message?.content) {
|
|
1202
|
+
for (const block of message.message.content) {
|
|
1203
|
+
if ("type" in block && block.type === "text" && "text" in block) {
|
|
1204
|
+
this.emit("output", block.text);
|
|
1205
|
+
this.currentAssistantResponse += block.text;
|
|
1206
|
+
} else if ("type" in block && block.type === "tool_use" && "name" in block) {
|
|
1207
|
+
const toolName = block.name;
|
|
1208
|
+
if (toolName !== "AskUserQuestion") {
|
|
1209
|
+
const input = block.input || {};
|
|
1210
|
+
let toolUseData;
|
|
1211
|
+
if (toolName === "Edit" && input.file_path && input.old_string !== void 0) {
|
|
1212
|
+
toolUseData = {
|
|
1213
|
+
action: "Edit",
|
|
1214
|
+
filePath: input.file_path,
|
|
1215
|
+
oldString: String(input.old_string).slice(0, MAX_TOOL_CONTENT),
|
|
1216
|
+
newString: String(input.new_string || "").slice(0, MAX_TOOL_CONTENT)
|
|
1217
|
+
};
|
|
1218
|
+
} else if (toolName === "Write" && input.file_path) {
|
|
1219
|
+
toolUseData = {
|
|
1220
|
+
action: "Write",
|
|
1221
|
+
filePath: input.file_path,
|
|
1222
|
+
content: String(input.content || "").slice(0, MAX_TOOL_CONTENT)
|
|
1223
|
+
};
|
|
1224
|
+
} else {
|
|
1225
|
+
toolUseData = {
|
|
1226
|
+
action: toolName,
|
|
1227
|
+
toolName,
|
|
1228
|
+
input
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
this.emit("tool-use", toolUseData);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
} else if (message.type === "result") {
|
|
1237
|
+
if (!this.currentAssistantResponse.trim() && "result" in message && message.result) {
|
|
1238
|
+
const resultText = String(message.result);
|
|
1239
|
+
this.emit("output", resultText);
|
|
1240
|
+
this.currentAssistantResponse = resultText;
|
|
1241
|
+
}
|
|
1242
|
+
if (this.currentAssistantResponse.trim()) {
|
|
1243
|
+
this.conversationHistory.push({ role: "assistant", content: this.currentAssistantResponse.trim() });
|
|
1244
|
+
}
|
|
1245
|
+
this.currentAssistantResponse = "";
|
|
1246
|
+
this.isProcessing = false;
|
|
1247
|
+
const queued = this.pendingPrompt;
|
|
1248
|
+
if (queued) {
|
|
1249
|
+
this.pendingPrompt = null;
|
|
1250
|
+
this.sendPrompt(queued.prompt, queued.attachments);
|
|
1251
|
+
} else {
|
|
1252
|
+
this.emit("complete");
|
|
1253
|
+
}
|
|
1254
|
+
} else if (message.type === "tool_progress") {
|
|
1255
|
+
if ("tool_name" in message) {
|
|
1256
|
+
this.emit("output", `
|
|
1257
|
+
[Using tool: ${message.tool_name}]
|
|
1258
|
+
`);
|
|
1259
|
+
}
|
|
1260
|
+
} else if (message.type === "tool_use_summary") {
|
|
1261
|
+
if ("tool_name" in message) {
|
|
1262
|
+
this.emit("output", `[Tool ${message.tool_name} completed]
|
|
1263
|
+
`);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
async sendPrompt(prompt2, attachments) {
|
|
1268
|
+
if (this.isProcessing) {
|
|
1269
|
+
this.pendingPrompt = { prompt: prompt2, attachments };
|
|
1270
|
+
this.emit("request-queued");
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
this.isProcessing = true;
|
|
1274
|
+
this.currentAssistantResponse = "";
|
|
1275
|
+
if (prompt2.trim()) {
|
|
1276
|
+
this.conversationHistory.push({ role: "user", content: prompt2 });
|
|
1277
|
+
}
|
|
1278
|
+
try {
|
|
1279
|
+
let finalPrompt = prompt2;
|
|
1280
|
+
if (this.pendingContextTransfer && this.conversationHistory.length > 1) {
|
|
1281
|
+
const previousHistory = this.conversationHistory.slice(0, -1);
|
|
1282
|
+
const contextLines = previousHistory.map(
|
|
1283
|
+
(msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`
|
|
1284
|
+
);
|
|
1285
|
+
const contextPrefix = `[Previous conversation context - you switched to a new model]
|
|
1286
|
+
${contextLines.join("\n")}
|
|
1287
|
+
|
|
1288
|
+
[Continue with new message]
|
|
1289
|
+
`;
|
|
1290
|
+
finalPrompt = contextPrefix + prompt2;
|
|
1291
|
+
this.pendingContextTransfer = false;
|
|
1292
|
+
}
|
|
1293
|
+
const session = this.ensureSession();
|
|
1294
|
+
const contentBlocks = [];
|
|
1295
|
+
if (attachments && attachments.length > 0) {
|
|
1296
|
+
for (const attachment of attachments) {
|
|
1297
|
+
contentBlocks.push({
|
|
1298
|
+
type: "image",
|
|
1299
|
+
source: {
|
|
1300
|
+
type: "base64",
|
|
1301
|
+
media_type: attachment.mediaType,
|
|
1302
|
+
data: attachment.data
|
|
1303
|
+
}
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
if (finalPrompt.trim()) {
|
|
1308
|
+
contentBlocks.push({
|
|
1309
|
+
type: "text",
|
|
1310
|
+
text: finalPrompt
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
const userMessage = {
|
|
1314
|
+
type: "user",
|
|
1315
|
+
message: {
|
|
1316
|
+
role: "user",
|
|
1317
|
+
content: contentBlocks.length > 0 ? contentBlocks : [{ type: "text", text: finalPrompt }]
|
|
1318
|
+
},
|
|
1319
|
+
parent_tool_use_id: null,
|
|
1320
|
+
session_id: this.sessionId || ""
|
|
1321
|
+
};
|
|
1322
|
+
await session.send(userMessage);
|
|
1323
|
+
} catch (error) {
|
|
1324
|
+
if (error.name === "AbortError") {
|
|
1325
|
+
this.emit("output", "\n[Cancelled]\n");
|
|
1326
|
+
} else {
|
|
1327
|
+
this.emit("error", error);
|
|
1328
|
+
this.emit("output", `
|
|
1329
|
+
[Error: ${error.message}]
|
|
1330
|
+
`);
|
|
1331
|
+
}
|
|
1332
|
+
this.isProcessing = false;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Provide an answer to a pending AskUserQuestion.
|
|
1337
|
+
*
|
|
1338
|
+
* AskUserQuestion flows through the canUseTool callback which blocks
|
|
1339
|
+
* waiting for a Promise to resolve. This method resolves that Promise
|
|
1340
|
+
* with the user's answers, which causes canUseTool to return
|
|
1341
|
+
* {behavior:'allow', updatedInput:{questions, answers}} → the SDK
|
|
1342
|
+
* sends the control_response back to the subprocess → it unblocks and
|
|
1343
|
+
* builds the tool_result for the Claude API.
|
|
1344
|
+
*/
|
|
1345
|
+
async provideAnswer(answerText, answers) {
|
|
1346
|
+
if (answerText.trim()) {
|
|
1347
|
+
this.conversationHistory.push({ role: "user", content: answerText });
|
|
1348
|
+
}
|
|
1349
|
+
if (this.pendingAnswerResolve) {
|
|
1350
|
+
const resolvedAnswers = answers || { result: answerText };
|
|
1351
|
+
this.pendingAnswerResolve(resolvedAnswers);
|
|
1352
|
+
this.pendingAnswerResolve = null;
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
await this.sendPrompt(answerText);
|
|
1356
|
+
}
|
|
1357
|
+
cancel() {
|
|
1358
|
+
this.pendingAnswerResolve = null;
|
|
1359
|
+
this.pendingQuestionData = null;
|
|
1360
|
+
this.pendingPermissionData = null;
|
|
1361
|
+
this.pendingPermissionRequests.clear();
|
|
1362
|
+
if (this.v2Session) {
|
|
1363
|
+
this.v2Session.close();
|
|
1364
|
+
this.v2Session = null;
|
|
1365
|
+
this.streamLoopRunning = false;
|
|
1366
|
+
}
|
|
1367
|
+
this.isProcessing = false;
|
|
1368
|
+
this.pendingPrompt = null;
|
|
1369
|
+
}
|
|
1370
|
+
getSessionId() {
|
|
1371
|
+
return this.sessionId;
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Resume a different session by setting its ID.
|
|
1375
|
+
* The next sendPrompt call will use this session ID.
|
|
1376
|
+
*/
|
|
1377
|
+
resumeSession(sessionId) {
|
|
1378
|
+
if (this.v2Session) {
|
|
1379
|
+
this.v2Session.close();
|
|
1380
|
+
this.v2Session = null;
|
|
1381
|
+
this.streamLoopRunning = false;
|
|
1382
|
+
}
|
|
1383
|
+
this.sessionId = sessionId;
|
|
1384
|
+
this.isProcessing = false;
|
|
1385
|
+
this.pendingPrompt = null;
|
|
1386
|
+
this.conversationHistory = [];
|
|
1387
|
+
this.emit("session-resumed", sessionId);
|
|
1388
|
+
}
|
|
1389
|
+
isActive() {
|
|
1390
|
+
return this.isProcessing;
|
|
1391
|
+
}
|
|
1392
|
+
getPendingQuestionData() {
|
|
1393
|
+
return this.pendingQuestionData;
|
|
1394
|
+
}
|
|
1395
|
+
getPendingPermissionData() {
|
|
1396
|
+
return this.pendingPermissionData;
|
|
1397
|
+
}
|
|
1398
|
+
async setModel(model) {
|
|
1399
|
+
if (model === this.currentModel) return;
|
|
1400
|
+
this.currentModel = model;
|
|
1401
|
+
if (this.v2Session) {
|
|
1402
|
+
this.v2Session.close();
|
|
1403
|
+
this.v2Session = null;
|
|
1404
|
+
this.streamLoopRunning = false;
|
|
1405
|
+
this.sessionId = null;
|
|
1406
|
+
this.isProcessing = false;
|
|
1407
|
+
this.pendingPrompt = null;
|
|
1408
|
+
this.pendingContextTransfer = true;
|
|
1409
|
+
} else if (this.sessionId) {
|
|
1410
|
+
this.sessionId = null;
|
|
1411
|
+
this.pendingContextTransfer = true;
|
|
1412
|
+
}
|
|
1413
|
+
this.emit("model", model);
|
|
1414
|
+
}
|
|
1415
|
+
getModel() {
|
|
1416
|
+
return this.currentModel;
|
|
1417
|
+
}
|
|
1418
|
+
getConversationHistory() {
|
|
1419
|
+
return [...this.conversationHistory];
|
|
1420
|
+
}
|
|
1421
|
+
clearHistory() {
|
|
1422
|
+
this.conversationHistory = [];
|
|
1423
|
+
if (this.v2Session) {
|
|
1424
|
+
this.v2Session.close();
|
|
1425
|
+
this.v2Session = null;
|
|
1426
|
+
this.streamLoopRunning = false;
|
|
1427
|
+
}
|
|
1428
|
+
this.sessionId = null;
|
|
1429
|
+
this.isProcessing = false;
|
|
1430
|
+
this.pendingPrompt = null;
|
|
1431
|
+
}
|
|
1432
|
+
async getSupportedModels() {
|
|
1433
|
+
const coreModels = [
|
|
1434
|
+
{ value: "opus", displayName: "Opus 4.6", description: "Opus 4.6 \xB7 Most capable for complex work" },
|
|
1435
|
+
{ value: "haiku", displayName: "Haiku 4.5", description: "Haiku 4.5 \xB7 Fastest for quick answers" },
|
|
1436
|
+
{ value: "sonnet", displayName: "Sonnet 4.5", description: "Sonnet 4.5 \xB7 Best for everyday tasks" }
|
|
1437
|
+
];
|
|
1438
|
+
if (this.cachedModels) {
|
|
1439
|
+
return this.cachedModels;
|
|
1440
|
+
}
|
|
1441
|
+
return coreModels;
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Scan file system for custom commands/skills
|
|
1445
|
+
* Commands are in ~/.claude/commands/ and .claude/commands/
|
|
1446
|
+
* Subdirectories create namespaced commands (e.g., gsd/add-phase.md -> gsd:add-phase)
|
|
1447
|
+
*/
|
|
1448
|
+
scanCustomCommands() {
|
|
1449
|
+
const commands = [];
|
|
1450
|
+
const homeDir = os2.homedir();
|
|
1451
|
+
const dirs = [
|
|
1452
|
+
path.join(homeDir, ".claude", "commands"),
|
|
1453
|
+
// Personal commands
|
|
1454
|
+
path.join(this.options.cwd, ".claude", "commands")
|
|
1455
|
+
// Project commands
|
|
1456
|
+
];
|
|
1457
|
+
for (const dir of dirs) {
|
|
1458
|
+
if (!fs.existsSync(dir)) continue;
|
|
1459
|
+
try {
|
|
1460
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1461
|
+
for (const entry of entries) {
|
|
1462
|
+
if (entry.isDirectory()) {
|
|
1463
|
+
const namespace = entry.name;
|
|
1464
|
+
const subDir = path.join(dir, namespace);
|
|
1465
|
+
const subEntries = fs.readdirSync(subDir);
|
|
1466
|
+
for (const file of subEntries) {
|
|
1467
|
+
if (file.endsWith(".md")) {
|
|
1468
|
+
const name = `${namespace}:${file.replace(".md", "")}`;
|
|
1469
|
+
const description = this.extractDescription(path.join(subDir, file));
|
|
1470
|
+
commands.push({ name, description, argumentHint: "" });
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1474
|
+
const name = entry.name.replace(".md", "");
|
|
1475
|
+
const description = this.extractDescription(path.join(dir, entry.name));
|
|
1476
|
+
commands.push({ name, description, argumentHint: "" });
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
} catch {
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
return commands;
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Extract description from markdown file (first line or frontmatter)
|
|
1486
|
+
*/
|
|
1487
|
+
extractDescription(filePath) {
|
|
1488
|
+
try {
|
|
1489
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
1490
|
+
const lines = content.split("\n");
|
|
1491
|
+
if (lines[0] === "---") {
|
|
1492
|
+
for (let i = 1; i < lines.length; i++) {
|
|
1493
|
+
const line = lines[i];
|
|
1494
|
+
if (!line) continue;
|
|
1495
|
+
if (line === "---") break;
|
|
1496
|
+
if (line.startsWith("description:")) {
|
|
1497
|
+
return line.replace("description:", "").trim().replace(/^["']|["']$/g, "");
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
for (const line of lines) {
|
|
1502
|
+
const trimmed = line.trim();
|
|
1503
|
+
if (trimmed.startsWith("# ")) {
|
|
1504
|
+
return trimmed.replace("# ", "");
|
|
1505
|
+
}
|
|
1506
|
+
if (trimmed && !trimmed.startsWith("---")) {
|
|
1507
|
+
return trimmed.slice(0, 100);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
} catch {
|
|
1511
|
+
}
|
|
1512
|
+
return "";
|
|
1513
|
+
}
|
|
1514
|
+
async getSupportedCommands() {
|
|
1515
|
+
const fallbackCommands = [
|
|
1516
|
+
// Session management
|
|
1517
|
+
{ name: "help", description: "Show all commands and custom slash commands", argumentHint: "" },
|
|
1518
|
+
{ name: "clear", description: "Clear the conversation history", argumentHint: "" },
|
|
1519
|
+
{ name: "compact", description: "Compress conversation by summarizing older messages", argumentHint: "" },
|
|
1520
|
+
{ name: "resume", description: "Resume a previous conversation", argumentHint: "<session-id>" },
|
|
1521
|
+
{ name: "rewind", description: "Go back to a previous message in the session", argumentHint: "" },
|
|
1522
|
+
{ name: "context", description: "Check context and excluded skills", argumentHint: "" },
|
|
1523
|
+
// Configuration
|
|
1524
|
+
{ name: "config", description: "Configure Claude Code settings interactively", argumentHint: "" },
|
|
1525
|
+
{ name: "permissions", description: "View or update tool permissions", argumentHint: "" },
|
|
1526
|
+
{ name: "allowed-tools", description: "Configure tool permissions interactively", argumentHint: "" },
|
|
1527
|
+
{ name: "model", description: "Change the AI model", argumentHint: "" },
|
|
1528
|
+
{ name: "vim", description: "Enable vim-style editing mode", argumentHint: "" },
|
|
1529
|
+
// Integrations
|
|
1530
|
+
{ name: "hooks", description: "Configure hooks", argumentHint: "" },
|
|
1531
|
+
{ name: "mcp", description: "Manage MCP servers", argumentHint: "" },
|
|
1532
|
+
{ name: "agents", description: "Manage subagents (create, edit, list)", argumentHint: "" },
|
|
1533
|
+
{ name: "terminal-setup", description: "Install terminal shortcuts for iTerm2/VS Code", argumentHint: "" },
|
|
1534
|
+
{ name: "install-github-app", description: "Set up GitHub Actions integration", argumentHint: "" },
|
|
1535
|
+
{ name: "ide", description: "Open in IDE or configure IDE integration", argumentHint: "" },
|
|
1536
|
+
// Project
|
|
1537
|
+
{ name: "init", description: "Initialize Claude Code and generate CLAUDE.md", argumentHint: "" },
|
|
1538
|
+
{ name: "memory", description: "Edit CLAUDE.md memory file", argumentHint: "" },
|
|
1539
|
+
{ name: "add-dir", description: "Add a directory to the context", argumentHint: "<path>" },
|
|
1540
|
+
// Git & Code Review
|
|
1541
|
+
{ name: "commit", description: "Commit changes to git with a generated message", argumentHint: "" },
|
|
1542
|
+
{ name: "review", description: "Review code changes", argumentHint: "" },
|
|
1543
|
+
{ name: "review-pr", description: "Review a GitHub pull request", argumentHint: "<pr-url>" },
|
|
1544
|
+
{ name: "pr-comments", description: "Get comments from a GitHub pull request", argumentHint: "" },
|
|
1545
|
+
{ name: "release-notes", description: "Generate release notes", argumentHint: "" },
|
|
1546
|
+
{ name: "security-review", description: "Perform a security review", argumentHint: "" },
|
|
1547
|
+
// Account & System
|
|
1548
|
+
{ name: "login", description: "Log in to your Anthropic account", argumentHint: "" },
|
|
1549
|
+
{ name: "logout", description: "Log out of your Anthropic account", argumentHint: "" },
|
|
1550
|
+
{ name: "doctor", description: "Check Claude Code health and configuration", argumentHint: "" },
|
|
1551
|
+
{ name: "bug", description: "Report a bug to Anthropic", argumentHint: "" },
|
|
1552
|
+
{ name: "cost", description: "Show token usage and cost", argumentHint: "" },
|
|
1553
|
+
{ name: "status", description: "Show current session status", argumentHint: "" }
|
|
1554
|
+
];
|
|
1555
|
+
const customCommands = this.scanCustomCommands();
|
|
1556
|
+
let allCommands = [...customCommands];
|
|
1557
|
+
if (this.cachedCommands && this.cachedCommands.length > 0) {
|
|
1558
|
+
const existingNames2 = new Set(allCommands.map((c) => c.name));
|
|
1559
|
+
const newCached = this.cachedCommands.filter((c) => !existingNames2.has(c.name));
|
|
1560
|
+
allCommands = [...allCommands, ...newCached];
|
|
1561
|
+
}
|
|
1562
|
+
const existingNames = new Set(allCommands.map((c) => c.name));
|
|
1563
|
+
const uniqueFallbacks = fallbackCommands.filter((c) => !existingNames.has(c.name));
|
|
1564
|
+
allCommands = [...allCommands, ...uniqueFallbacks];
|
|
1565
|
+
allCommands = allCommands.filter((c) => !UNSUPPORTED_COMMANDS.has(c.name));
|
|
1566
|
+
return allCommands;
|
|
1567
|
+
}
|
|
1568
|
+
};
|
|
1569
|
+
|
|
1570
|
+
// src/daemon/config-manager.ts
|
|
1571
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
1572
|
+
import { join as join3, dirname } from "path";
|
|
1573
|
+
import { homedir as homedir3 } from "os";
|
|
1574
|
+
var ConfigManager = class {
|
|
1575
|
+
claudeDir;
|
|
1576
|
+
cwd;
|
|
1577
|
+
constructor(options = {}) {
|
|
1578
|
+
const home = options.homeDir || homedir3();
|
|
1579
|
+
this.claudeDir = join3(home, ".claude");
|
|
1580
|
+
this.cwd = options.cwd || process.cwd();
|
|
1581
|
+
}
|
|
1582
|
+
getGlobalSettingsPath() {
|
|
1583
|
+
return join3(this.claudeDir, "settings.json");
|
|
1584
|
+
}
|
|
1585
|
+
getLocalSettingsPath() {
|
|
1586
|
+
return join3(this.claudeDir, "settings.local.json");
|
|
1587
|
+
}
|
|
1588
|
+
getProjectSettingsPath() {
|
|
1589
|
+
return join3(this.cwd, ".claude", "settings.json");
|
|
1590
|
+
}
|
|
1591
|
+
readSettingsFile(path4) {
|
|
1592
|
+
try {
|
|
1593
|
+
if (existsSync3(path4)) {
|
|
1594
|
+
const content = readFileSync3(path4, "utf-8");
|
|
1595
|
+
return JSON.parse(content);
|
|
1596
|
+
}
|
|
1597
|
+
} catch {
|
|
1598
|
+
}
|
|
1599
|
+
return null;
|
|
1600
|
+
}
|
|
1601
|
+
writeSettingsFile(path4, settings) {
|
|
1602
|
+
try {
|
|
1603
|
+
const dir = dirname(path4);
|
|
1604
|
+
if (!existsSync3(dir)) {
|
|
1605
|
+
mkdirSync2(dir, { recursive: true });
|
|
1606
|
+
}
|
|
1607
|
+
writeFileSync2(path4, JSON.stringify(settings, null, 2));
|
|
1608
|
+
return true;
|
|
1609
|
+
} catch {
|
|
1610
|
+
return false;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
getMergedSettings() {
|
|
1614
|
+
const global = this.readSettingsFile(this.getGlobalSettingsPath()) || {};
|
|
1615
|
+
const local = this.readSettingsFile(this.getLocalSettingsPath()) || {};
|
|
1616
|
+
const project = this.readSettingsFile(this.getProjectSettingsPath()) || {};
|
|
1617
|
+
return {
|
|
1618
|
+
...global,
|
|
1619
|
+
...local,
|
|
1620
|
+
...project,
|
|
1621
|
+
preferences: {
|
|
1622
|
+
...global.preferences,
|
|
1623
|
+
...local.preferences,
|
|
1624
|
+
...project.preferences
|
|
1625
|
+
},
|
|
1626
|
+
permissions: {
|
|
1627
|
+
...global.permissions,
|
|
1628
|
+
...local.permissions,
|
|
1629
|
+
...project.permissions
|
|
1630
|
+
}
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Get the current thinking mode setting from merged settings
|
|
1635
|
+
*/
|
|
1636
|
+
getThinkingMode() {
|
|
1637
|
+
const settings = this.getMergedSettings();
|
|
1638
|
+
return settings.alwaysThinkingEnabled ?? false;
|
|
1639
|
+
}
|
|
1640
|
+
getInteractiveData(command) {
|
|
1641
|
+
const settings = this.getMergedSettings();
|
|
1642
|
+
switch (command) {
|
|
1643
|
+
case "config":
|
|
1644
|
+
return this.getConfigData(settings);
|
|
1645
|
+
case "permissions":
|
|
1646
|
+
return this.getPermissionsData(settings);
|
|
1647
|
+
case "vim":
|
|
1648
|
+
return this.getVimData(settings);
|
|
1649
|
+
case "allowed-tools":
|
|
1650
|
+
return this.getAllowedToolsData(settings);
|
|
1651
|
+
default:
|
|
1652
|
+
return {
|
|
1653
|
+
command,
|
|
1654
|
+
uiType: "select",
|
|
1655
|
+
title: `${command} Settings`,
|
|
1656
|
+
description: "This command is not yet supported in mobile.",
|
|
1657
|
+
options: []
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
getConfigData(settings) {
|
|
1662
|
+
const options = [
|
|
1663
|
+
{
|
|
1664
|
+
id: "alwaysThinkingEnabled",
|
|
1665
|
+
label: "Thinking Mode",
|
|
1666
|
+
description: "Enable extended thinking for complex tasks",
|
|
1667
|
+
value: settings.alwaysThinkingEnabled || false,
|
|
1668
|
+
selected: settings.alwaysThinkingEnabled || false
|
|
1669
|
+
},
|
|
1670
|
+
{
|
|
1671
|
+
id: "autoCompact",
|
|
1672
|
+
label: "Auto-Compact",
|
|
1673
|
+
description: "Automatically compact conversation when context is full",
|
|
1674
|
+
value: settings.preferences?.autoCompact ?? true,
|
|
1675
|
+
selected: settings.preferences?.autoCompact ?? true
|
|
1676
|
+
}
|
|
1677
|
+
];
|
|
1678
|
+
return {
|
|
1679
|
+
command: "config",
|
|
1680
|
+
uiType: "nested",
|
|
1681
|
+
title: "Configuration",
|
|
1682
|
+
description: "Claude Code preferences and settings",
|
|
1683
|
+
options
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
getPermissionsData(settings) {
|
|
1687
|
+
const currentMode = settings.permissions?.mode || "default";
|
|
1688
|
+
const options = [
|
|
1689
|
+
{
|
|
1690
|
+
id: "default",
|
|
1691
|
+
label: "Default",
|
|
1692
|
+
description: "Ask before making changes to files",
|
|
1693
|
+
value: "default",
|
|
1694
|
+
selected: currentMode === "default"
|
|
1695
|
+
},
|
|
1696
|
+
{
|
|
1697
|
+
id: "acceptEdits",
|
|
1698
|
+
label: "Auto-approve Edits",
|
|
1699
|
+
description: "Automatically approve file edits",
|
|
1700
|
+
value: "acceptEdits",
|
|
1701
|
+
selected: currentMode === "acceptEdits"
|
|
1702
|
+
},
|
|
1703
|
+
{
|
|
1704
|
+
id: "plan",
|
|
1705
|
+
label: "Plan Mode",
|
|
1706
|
+
description: "Only plan changes without executing",
|
|
1707
|
+
value: "plan",
|
|
1708
|
+
selected: currentMode === "plan"
|
|
1709
|
+
},
|
|
1710
|
+
{
|
|
1711
|
+
id: "bypassPermissions",
|
|
1712
|
+
label: "Yolo Mode",
|
|
1713
|
+
description: "Bypass all permission prompts",
|
|
1714
|
+
value: "bypassPermissions",
|
|
1715
|
+
selected: currentMode === "bypassPermissions"
|
|
1716
|
+
}
|
|
1717
|
+
];
|
|
1718
|
+
return {
|
|
1719
|
+
command: "permissions",
|
|
1720
|
+
uiType: "select",
|
|
1721
|
+
title: "Permission Mode",
|
|
1722
|
+
description: "Select how Claude handles file edits",
|
|
1723
|
+
options,
|
|
1724
|
+
currentValue: currentMode
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
getVimData(settings) {
|
|
1728
|
+
const enabled = settings.vim ?? false;
|
|
1729
|
+
return {
|
|
1730
|
+
command: "vim",
|
|
1731
|
+
uiType: "toggle",
|
|
1732
|
+
title: "Vim Mode",
|
|
1733
|
+
description: "Enable vim keybindings in the editor",
|
|
1734
|
+
options: [
|
|
1735
|
+
{
|
|
1736
|
+
id: "vim-enabled",
|
|
1737
|
+
label: "Vim Mode",
|
|
1738
|
+
description: enabled ? "Currently enabled" : "Currently disabled",
|
|
1739
|
+
value: enabled,
|
|
1740
|
+
selected: enabled
|
|
1741
|
+
}
|
|
1742
|
+
],
|
|
1743
|
+
currentValue: enabled
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
getAllowedToolsData(settings) {
|
|
1747
|
+
const allowedTools = settings.permissions?.allowedTools || [];
|
|
1748
|
+
const allTools = [
|
|
1749
|
+
{ id: "Bash", label: "Bash", description: "Execute shell commands" },
|
|
1750
|
+
{ id: "Read", label: "Read", description: "Read file contents" },
|
|
1751
|
+
{ id: "Write", label: "Write", description: "Write to files" },
|
|
1752
|
+
{ id: "Edit", label: "Edit", description: "Edit file contents" },
|
|
1753
|
+
{ id: "Glob", label: "Glob", description: "Search for files" },
|
|
1754
|
+
{ id: "Grep", label: "Grep", description: "Search file contents" },
|
|
1755
|
+
{ id: "WebFetch", label: "WebFetch", description: "Fetch web content" },
|
|
1756
|
+
{ id: "WebSearch", label: "WebSearch", description: "Search the web" }
|
|
1757
|
+
];
|
|
1758
|
+
const options = allTools.map((tool) => ({
|
|
1759
|
+
...tool,
|
|
1760
|
+
value: tool.id,
|
|
1761
|
+
selected: allowedTools.includes(tool.id)
|
|
1762
|
+
}));
|
|
1763
|
+
return {
|
|
1764
|
+
command: "allowed-tools",
|
|
1765
|
+
uiType: "multi-select",
|
|
1766
|
+
title: "Allowed Tools",
|
|
1767
|
+
description: "Select which tools Claude can use",
|
|
1768
|
+
options,
|
|
1769
|
+
currentValue: allowedTools
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
applyChange(payload) {
|
|
1773
|
+
const globalPath = this.getGlobalSettingsPath();
|
|
1774
|
+
const settings = this.readSettingsFile(globalPath) || {};
|
|
1775
|
+
try {
|
|
1776
|
+
switch (payload.command) {
|
|
1777
|
+
case "config":
|
|
1778
|
+
return this.applyConfigChange(settings, globalPath, payload);
|
|
1779
|
+
case "permissions":
|
|
1780
|
+
return this.applyPermissionsChange(settings, globalPath, payload);
|
|
1781
|
+
case "vim":
|
|
1782
|
+
return this.applyVimChange(settings, globalPath, payload);
|
|
1783
|
+
case "allowed-tools":
|
|
1784
|
+
return this.applyAllowedToolsChange(settings, globalPath, payload);
|
|
1785
|
+
default:
|
|
1786
|
+
return {
|
|
1787
|
+
success: false,
|
|
1788
|
+
message: `Unknown command: ${payload.command}`
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
} catch (error) {
|
|
1792
|
+
return {
|
|
1793
|
+
success: false,
|
|
1794
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
applyConfigChange(settings, path4, payload) {
|
|
1799
|
+
if (!settings.preferences) {
|
|
1800
|
+
settings.preferences = {};
|
|
1801
|
+
}
|
|
1802
|
+
switch (payload.key) {
|
|
1803
|
+
case "theme":
|
|
1804
|
+
settings.preferences.theme = payload.value;
|
|
1805
|
+
break;
|
|
1806
|
+
case "alwaysThinkingEnabled":
|
|
1807
|
+
settings.alwaysThinkingEnabled = payload.value;
|
|
1808
|
+
break;
|
|
1809
|
+
case "autoCompact":
|
|
1810
|
+
settings.preferences.autoCompact = payload.value;
|
|
1811
|
+
break;
|
|
1812
|
+
case "verbose":
|
|
1813
|
+
settings.preferences.verboseMode = payload.value;
|
|
1814
|
+
break;
|
|
1815
|
+
default:
|
|
1816
|
+
return { success: false, message: `Unknown config key: ${payload.key}` };
|
|
1817
|
+
}
|
|
1818
|
+
if (this.writeSettingsFile(path4, settings)) {
|
|
1819
|
+
return { success: true, message: `Config ${payload.key} updated` };
|
|
1820
|
+
}
|
|
1821
|
+
return { success: false, message: "Failed to write settings file" };
|
|
1822
|
+
}
|
|
1823
|
+
applyPermissionsChange(settings, path4, payload) {
|
|
1824
|
+
if (!settings.permissions) {
|
|
1825
|
+
settings.permissions = {};
|
|
1826
|
+
}
|
|
1827
|
+
settings.permissions.mode = payload.value;
|
|
1828
|
+
if (this.writeSettingsFile(path4, settings)) {
|
|
1829
|
+
return { success: true, message: `Permission mode set to ${payload.value}` };
|
|
1830
|
+
}
|
|
1831
|
+
return { success: false, message: "Failed to write settings file" };
|
|
1832
|
+
}
|
|
1833
|
+
applyVimChange(settings, path4, payload) {
|
|
1834
|
+
if (payload.action === "toggle") {
|
|
1835
|
+
settings.vim = !settings.vim;
|
|
1836
|
+
} else {
|
|
1837
|
+
settings.vim = payload.value;
|
|
1838
|
+
}
|
|
1839
|
+
if (this.writeSettingsFile(path4, settings)) {
|
|
1840
|
+
return { success: true, message: `Vim mode ${settings.vim ? "enabled" : "disabled"}` };
|
|
1841
|
+
}
|
|
1842
|
+
return { success: false, message: "Failed to write settings file" };
|
|
1843
|
+
}
|
|
1844
|
+
applyAllowedToolsChange(settings, path4, payload) {
|
|
1845
|
+
if (!settings.permissions) {
|
|
1846
|
+
settings.permissions = {};
|
|
1847
|
+
}
|
|
1848
|
+
if (!settings.permissions.allowedTools) {
|
|
1849
|
+
settings.permissions.allowedTools = [];
|
|
1850
|
+
}
|
|
1851
|
+
const toolId = payload.value;
|
|
1852
|
+
switch (payload.action) {
|
|
1853
|
+
case "add":
|
|
1854
|
+
if (!settings.permissions.allowedTools.includes(toolId)) {
|
|
1855
|
+
settings.permissions.allowedTools.push(toolId);
|
|
1856
|
+
}
|
|
1857
|
+
break;
|
|
1858
|
+
case "remove":
|
|
1859
|
+
settings.permissions.allowedTools = settings.permissions.allowedTools.filter(
|
|
1860
|
+
(t) => t !== toolId
|
|
1861
|
+
);
|
|
1862
|
+
break;
|
|
1863
|
+
case "set":
|
|
1864
|
+
settings.permissions.allowedTools = payload.value;
|
|
1865
|
+
break;
|
|
1866
|
+
case "toggle":
|
|
1867
|
+
if (settings.permissions.allowedTools.includes(toolId)) {
|
|
1868
|
+
settings.permissions.allowedTools = settings.permissions.allowedTools.filter(
|
|
1869
|
+
(t) => t !== toolId
|
|
1870
|
+
);
|
|
1871
|
+
} else {
|
|
1872
|
+
settings.permissions.allowedTools.push(toolId);
|
|
1873
|
+
}
|
|
1874
|
+
break;
|
|
1875
|
+
}
|
|
1876
|
+
if (this.writeSettingsFile(path4, settings)) {
|
|
1877
|
+
return { success: true, message: "Allowed tools updated" };
|
|
1878
|
+
}
|
|
1879
|
+
return { success: false, message: "Failed to write settings file" };
|
|
1880
|
+
}
|
|
1881
|
+
};
|
|
1882
|
+
|
|
1883
|
+
// src/daemon/daemon.ts
|
|
1884
|
+
var Daemon = class extends EventEmitter3 {
|
|
1885
|
+
options;
|
|
1886
|
+
sdkSession;
|
|
1887
|
+
sessionManager;
|
|
1888
|
+
machineManager;
|
|
1889
|
+
configManager;
|
|
1890
|
+
realtimeClient = null;
|
|
1891
|
+
machine = null;
|
|
1892
|
+
session = null;
|
|
1893
|
+
running = false;
|
|
1894
|
+
commandsBroadcast = false;
|
|
1895
|
+
sdkCommandsBroadcast = false;
|
|
1896
|
+
titleSet = false;
|
|
1897
|
+
constructor(options) {
|
|
1898
|
+
super();
|
|
1899
|
+
this.options = options;
|
|
1900
|
+
this.sdkSession = new SdkSession({
|
|
1901
|
+
cwd: options.cwd
|
|
1902
|
+
});
|
|
1903
|
+
this.sessionManager = new SessionManager({
|
|
1904
|
+
supabase: options.supabase
|
|
1905
|
+
});
|
|
1906
|
+
this.machineManager = new MachineManager({
|
|
1907
|
+
supabase: options.supabase
|
|
1908
|
+
});
|
|
1909
|
+
this.configManager = new ConfigManager({
|
|
1910
|
+
cwd: options.cwd
|
|
1911
|
+
});
|
|
1912
|
+
const thinkingEnabled = this.configManager.getThinkingMode();
|
|
1913
|
+
if (thinkingEnabled) {
|
|
1914
|
+
this.sdkSession.setThinkingMode(true);
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
async start() {
|
|
1918
|
+
if (this.running) {
|
|
1919
|
+
throw new Error("Daemon is already running");
|
|
1920
|
+
}
|
|
1921
|
+
this.machine = await this.machineManager.registerMachine(
|
|
1922
|
+
this.options.userId,
|
|
1923
|
+
this.options.machineName,
|
|
1924
|
+
this.options.machineId
|
|
1925
|
+
);
|
|
1926
|
+
this.session = await this.sessionManager.createSession(
|
|
1927
|
+
this.machine.id,
|
|
1928
|
+
this.options.cwd
|
|
1929
|
+
);
|
|
1930
|
+
this.realtimeClient = new RealtimeClient({
|
|
1931
|
+
supabase: this.options.supabase,
|
|
1932
|
+
sessionId: this.session.id
|
|
1933
|
+
});
|
|
1934
|
+
this.sdkSession.on("output", async (data) => {
|
|
1935
|
+
if (this.options.hybrid !== false) {
|
|
1936
|
+
process.stdout.write(data);
|
|
1937
|
+
}
|
|
1938
|
+
this.emit("mobile-output", data);
|
|
1939
|
+
if (this.realtimeClient) {
|
|
1940
|
+
try {
|
|
1941
|
+
await this.realtimeClient.broadcast(data);
|
|
1942
|
+
} catch {
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
this.sdkSession.on("error", (error) => {
|
|
1947
|
+
this.emit("error", error);
|
|
1948
|
+
});
|
|
1949
|
+
this.sdkSession.on("commands-updated", async () => {
|
|
1950
|
+
await this.broadcastCommands();
|
|
1951
|
+
});
|
|
1952
|
+
this.sdkSession.on("session-started", async (sdkSessionId) => {
|
|
1953
|
+
if (!this.session) return;
|
|
1954
|
+
try {
|
|
1955
|
+
await this.sessionManager.updateSdkSessionId(this.session.id, sdkSessionId);
|
|
1956
|
+
} catch {
|
|
1957
|
+
}
|
|
1958
|
+
});
|
|
1959
|
+
this.sdkSession.on("complete", async () => {
|
|
1960
|
+
if (this.options.hybrid !== false) {
|
|
1961
|
+
process.stdout.write("\n> ");
|
|
1962
|
+
}
|
|
1963
|
+
if (this.realtimeClient) {
|
|
1964
|
+
try {
|
|
1965
|
+
await this.realtimeClient.broadcastComplete();
|
|
1966
|
+
} catch {
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
if (!this.sdkCommandsBroadcast && this.realtimeClient) {
|
|
1970
|
+
this.sdkCommandsBroadcast = true;
|
|
1971
|
+
await this.broadcastCommands();
|
|
1972
|
+
}
|
|
1973
|
+
});
|
|
1974
|
+
this.sdkSession.on("permission-mode", async (mode) => {
|
|
1975
|
+
if (this.realtimeClient) {
|
|
1976
|
+
try {
|
|
1977
|
+
await this.realtimeClient.broadcastMode(mode);
|
|
1978
|
+
} catch {
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
});
|
|
1982
|
+
this.sdkSession.on("user-question", async (questionData) => {
|
|
1983
|
+
if (this.realtimeClient) {
|
|
1984
|
+
try {
|
|
1985
|
+
await this.realtimeClient.broadcastUserQuestion(questionData);
|
|
1986
|
+
} catch {
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
});
|
|
1990
|
+
this.sdkSession.on("request-queued", async () => {
|
|
1991
|
+
if (this.realtimeClient) {
|
|
1992
|
+
try {
|
|
1993
|
+
await this.realtimeClient.broadcastQueued();
|
|
1994
|
+
} catch {
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
});
|
|
1998
|
+
this.sdkSession.on("permission-request", async (requestData) => {
|
|
1999
|
+
if (this.realtimeClient) {
|
|
2000
|
+
try {
|
|
2001
|
+
await this.realtimeClient.broadcastPermissionRequest(requestData);
|
|
2002
|
+
} catch {
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
});
|
|
2006
|
+
this.sdkSession.on("tool-use", async (toolUseData) => {
|
|
2007
|
+
if (this.realtimeClient) {
|
|
2008
|
+
try {
|
|
2009
|
+
await this.realtimeClient.broadcastToolUse(toolUseData);
|
|
2010
|
+
} catch (err) {
|
|
2011
|
+
console.warn("[Daemon] Failed to broadcast tool-use:", err);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
});
|
|
2015
|
+
this.sdkSession.on("model", async (model) => {
|
|
2016
|
+
if (this.session) {
|
|
2017
|
+
try {
|
|
2018
|
+
await this.sessionManager.updateSessionModel(this.session.id, model);
|
|
2019
|
+
} catch {
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
if (this.realtimeClient) {
|
|
2023
|
+
try {
|
|
2024
|
+
await this.realtimeClient.broadcastModel(model);
|
|
2025
|
+
} catch {
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
});
|
|
2029
|
+
this.realtimeClient.on("input", async (message) => {
|
|
2030
|
+
if (!this.commandsBroadcast) {
|
|
2031
|
+
this.commandsBroadcast = true;
|
|
2032
|
+
await this.broadcastCommands();
|
|
2033
|
+
}
|
|
2034
|
+
if (message.type === "mode-change" && message.permissionMode) {
|
|
2035
|
+
this.sdkSession.setPermissionMode(message.permissionMode);
|
|
2036
|
+
if (this.realtimeClient) {
|
|
2037
|
+
try {
|
|
2038
|
+
await this.realtimeClient.broadcastMode(message.permissionMode);
|
|
2039
|
+
} catch {
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
if (message.type === "commands-request") {
|
|
2045
|
+
await this.broadcastCommands();
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
if (message.type === "model-change" && message.model) {
|
|
2049
|
+
const previousModel = this.sdkSession.getModel();
|
|
2050
|
+
await this.sdkSession.setModel(message.model);
|
|
2051
|
+
if (previousModel !== message.model) {
|
|
2052
|
+
const modelNames = {
|
|
2053
|
+
"default": "Opus 4.6",
|
|
2054
|
+
"sonnet": "Sonnet 4.5",
|
|
2055
|
+
"opus": "Opus 4.6",
|
|
2056
|
+
"haiku": "Haiku 3.5"
|
|
2057
|
+
};
|
|
2058
|
+
const displayName = modelNames[message.model] || message.model;
|
|
2059
|
+
const confirmationMsg = `[Model switched to ${displayName}]`;
|
|
2060
|
+
if (this.options.hybrid !== false) {
|
|
2061
|
+
process.stdout.write(`
|
|
2062
|
+
${confirmationMsg}
|
|
2063
|
+
`);
|
|
2064
|
+
}
|
|
2065
|
+
if (this.realtimeClient) {
|
|
2066
|
+
try {
|
|
2067
|
+
await this.realtimeClient.broadcastSystem(confirmationMsg);
|
|
2068
|
+
} catch {
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
} else {
|
|
2072
|
+
try {
|
|
2073
|
+
await this.realtimeClient?.broadcastModel(this.sdkSession.getModel());
|
|
2074
|
+
} catch {
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
2079
|
+
if (message.type === "models-request") {
|
|
2080
|
+
await this.broadcastModels();
|
|
2081
|
+
try {
|
|
2082
|
+
await this.realtimeClient?.broadcastModel(this.sdkSession.getModel());
|
|
2083
|
+
} catch {
|
|
2084
|
+
}
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
if (message.type === "status-request") {
|
|
2088
|
+
if (this.realtimeClient) {
|
|
2089
|
+
try {
|
|
2090
|
+
const pendingQuestion = this.sdkSession.getPendingQuestionData();
|
|
2091
|
+
if (pendingQuestion) {
|
|
2092
|
+
await this.realtimeClient.broadcastUserQuestion(pendingQuestion);
|
|
2093
|
+
}
|
|
2094
|
+
const pendingPermission = this.sdkSession.getPendingPermissionData();
|
|
2095
|
+
if (pendingPermission) {
|
|
2096
|
+
await this.realtimeClient.broadcastPermissionRequest(pendingPermission);
|
|
2097
|
+
}
|
|
2098
|
+
} catch {
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
if (message.type === "mobile-disconnect") {
|
|
2104
|
+
if (this.options.hybrid !== false) {
|
|
2105
|
+
process.stdout.write("\n[ClauTunnel] Mobile client disconnected.\n");
|
|
2106
|
+
}
|
|
2107
|
+
this.emit("mobile-disconnected");
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
2110
|
+
if (message.type === "interactive-request" && message.interactiveCommand) {
|
|
2111
|
+
const data = this.configManager.getInteractiveData(message.interactiveCommand);
|
|
2112
|
+
if (this.realtimeClient) {
|
|
2113
|
+
try {
|
|
2114
|
+
await this.realtimeClient.broadcastInteractiveResponse(data);
|
|
2115
|
+
} catch {
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
return;
|
|
2119
|
+
}
|
|
2120
|
+
if (message.type === "interactive-apply" && message.interactivePayload) {
|
|
2121
|
+
const result = this.configManager.applyChange(message.interactivePayload);
|
|
2122
|
+
if (message.interactivePayload.command === "config" && message.interactivePayload.key === "alwaysThinkingEnabled") {
|
|
2123
|
+
const enabled = Boolean(message.interactivePayload.value);
|
|
2124
|
+
await this.sdkSession.setThinkingMode(enabled);
|
|
2125
|
+
}
|
|
2126
|
+
if (this.realtimeClient) {
|
|
2127
|
+
try {
|
|
2128
|
+
await this.realtimeClient.broadcastInteractiveConfirm(
|
|
2129
|
+
message.interactivePayload.command,
|
|
2130
|
+
result
|
|
2131
|
+
);
|
|
2132
|
+
const statusText = result.success ? `\u2713 ${result.message}` : `\u2717 ${result.message}`;
|
|
2133
|
+
await this.realtimeClient.broadcastSystem(statusText);
|
|
2134
|
+
} catch {
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
if (message.type === "cancel-request") {
|
|
2140
|
+
this.sdkSession.cancel();
|
|
2141
|
+
if (this.options.hybrid !== false) {
|
|
2142
|
+
process.stdout.write("\n[Cancelled]\n> ");
|
|
2143
|
+
}
|
|
2144
|
+
if (this.realtimeClient) {
|
|
2145
|
+
try {
|
|
2146
|
+
await this.realtimeClient.broadcastSystem("[Cancelled]");
|
|
2147
|
+
await this.realtimeClient.broadcastComplete();
|
|
2148
|
+
} catch (error) {
|
|
2149
|
+
console.warn("[Daemon] Failed to broadcast cancel:", error);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
if (message.type === "clear-request") {
|
|
2155
|
+
this.sdkSession.clearHistory();
|
|
2156
|
+
if (this.options.hybrid !== false) {
|
|
2157
|
+
process.stdout.write("\n[Conversation cleared]\n> ");
|
|
2158
|
+
}
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
if (message.type === "user-answer" && message.userAnswer) {
|
|
2162
|
+
const answerText = Object.values(message.userAnswer.answers).join("\n");
|
|
2163
|
+
this.emit("mobile-input", `(answer) ${answerText}`);
|
|
2164
|
+
await this.sdkSession.provideAnswer(answerText, message.userAnswer.answers);
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
if (message.type === "permission-response" && message.permissionResponse) {
|
|
2168
|
+
this.sdkSession.handlePermissionResponse(message.permissionResponse);
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
if (message.type === "resume-request" && message.resumeSessionId) {
|
|
2172
|
+
const targetSdkSessionId = message.resumeSessionId;
|
|
2173
|
+
this.sdkSession.resumeSession(targetSdkSessionId);
|
|
2174
|
+
const confirmationMsg = `[Resuming session: ${targetSdkSessionId.slice(0, 8)}...]`;
|
|
2175
|
+
if (this.options.hybrid !== false) {
|
|
2176
|
+
process.stdout.write(`
|
|
2177
|
+
${confirmationMsg}
|
|
2178
|
+
`);
|
|
2179
|
+
}
|
|
2180
|
+
if (this.realtimeClient) {
|
|
2181
|
+
try {
|
|
2182
|
+
const { data: sessionData } = await this.options.supabase.from("sessions").select("id").eq("sdk_session_id", targetSdkSessionId).single();
|
|
2183
|
+
if (sessionData) {
|
|
2184
|
+
await this.realtimeClient.broadcastResumeHistory(sessionData.id);
|
|
2185
|
+
}
|
|
2186
|
+
await this.realtimeClient.broadcastSystem(confirmationMsg);
|
|
2187
|
+
} catch {
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
await this.sdkSession.sendPrompt("Continue from where we left off.");
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
const prompt2 = message.content?.replace(/[\r\n]+$/, "") || "";
|
|
2194
|
+
const attachments = message.attachments;
|
|
2195
|
+
if (prompt2.trim() || attachments && attachments.length > 0) {
|
|
2196
|
+
const trimmedPrompt = prompt2.trim();
|
|
2197
|
+
this.emit("mobile-input", trimmedPrompt, attachments);
|
|
2198
|
+
if (trimmedPrompt === "/clear") {
|
|
2199
|
+
this.sdkSession.clearHistory();
|
|
2200
|
+
if (this.realtimeClient) {
|
|
2201
|
+
try {
|
|
2202
|
+
await this.realtimeClient.broadcastSystem("Conversation cleared");
|
|
2203
|
+
} catch {
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
if (this.options.hybrid !== false) {
|
|
2207
|
+
process.stdout.write("\n[Conversation cleared]\n> ");
|
|
2208
|
+
}
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
if (!this.titleSet && this.session) {
|
|
2212
|
+
this.titleSet = true;
|
|
2213
|
+
const title = trimmedPrompt.length > 50 ? trimmedPrompt.slice(0, 50) + "..." : trimmedPrompt;
|
|
2214
|
+
this.sessionManager.updateSessionTitle(this.session.id, title).catch(() => {
|
|
2215
|
+
});
|
|
2216
|
+
if (this.realtimeClient) {
|
|
2217
|
+
this.realtimeClient.broadcastSessionTitle(title).catch(() => {
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
await this.sdkSession.sendPrompt(prompt2, attachments);
|
|
2222
|
+
}
|
|
2223
|
+
});
|
|
2224
|
+
await this.realtimeClient.connect();
|
|
2225
|
+
try {
|
|
2226
|
+
await this.realtimeClient.broadcastMode(this.sdkSession.getPermissionMode());
|
|
2227
|
+
} catch {
|
|
2228
|
+
}
|
|
2229
|
+
await this.broadcastCommands();
|
|
2230
|
+
await this.broadcastModels();
|
|
2231
|
+
try {
|
|
2232
|
+
await this.realtimeClient.broadcastModel(this.sdkSession.getModel());
|
|
2233
|
+
} catch {
|
|
2234
|
+
}
|
|
2235
|
+
this.running = true;
|
|
2236
|
+
this.emit("started", {
|
|
2237
|
+
machine: this.machine,
|
|
2238
|
+
session: this.session,
|
|
2239
|
+
mobileSyncEnabled: this.realtimeClient?.isRealtimeEnabled() ?? false
|
|
2240
|
+
});
|
|
2241
|
+
if (this.options.hybrid !== false) {
|
|
2242
|
+
process.stdout.write("\n[ClauTunnel] Ready for input.\n> ");
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
async stop() {
|
|
2246
|
+
if (!this.running) {
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
this.running = false;
|
|
2250
|
+
this.sdkSession.cancel();
|
|
2251
|
+
if (this.session) {
|
|
2252
|
+
await this.sessionManager.endSession(this.session.id);
|
|
2253
|
+
}
|
|
2254
|
+
if (this.realtimeClient) {
|
|
2255
|
+
await this.realtimeClient.disconnect();
|
|
2256
|
+
}
|
|
2257
|
+
this.emit("stopped");
|
|
2258
|
+
}
|
|
2259
|
+
async sendPrompt(prompt2, attachments) {
|
|
2260
|
+
await this.sdkSession.sendPrompt(prompt2, attachments);
|
|
2261
|
+
}
|
|
2262
|
+
isRunning() {
|
|
2263
|
+
return this.running;
|
|
2264
|
+
}
|
|
2265
|
+
getSession() {
|
|
2266
|
+
return this.session;
|
|
2267
|
+
}
|
|
2268
|
+
getMachine() {
|
|
2269
|
+
return this.machine;
|
|
2270
|
+
}
|
|
2271
|
+
async broadcastCommands() {
|
|
2272
|
+
if (!this.realtimeClient) {
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
try {
|
|
2276
|
+
const commands = await this.sdkSession.getSupportedCommands();
|
|
2277
|
+
await this.realtimeClient.broadcastCommands(commands);
|
|
2278
|
+
} catch {
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
async broadcastModels() {
|
|
2282
|
+
if (!this.realtimeClient) {
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
try {
|
|
2286
|
+
const models = await this.sdkSession.getSupportedModels();
|
|
2287
|
+
await this.realtimeClient.broadcastModels(models);
|
|
2288
|
+
} catch {
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
};
|
|
2292
|
+
|
|
2293
|
+
// src/index.ts
|
|
2294
|
+
import { Command as Command6 } from "commander";
|
|
2295
|
+
|
|
2296
|
+
// src/commands/start.ts
|
|
2297
|
+
import { Command } from "commander";
|
|
2298
|
+
import WebSocket from "ws";
|
|
2299
|
+
|
|
2300
|
+
// src/realtime/machine-client.ts
|
|
2301
|
+
var import_clautunnel_shared2 = __toESM(require_dist(), 1);
|
|
2302
|
+
import { EventEmitter as EventEmitter4 } from "events";
|
|
2303
|
+
var MachineRealtimeClient = class extends EventEmitter4 {
|
|
2304
|
+
supabase;
|
|
2305
|
+
machineId;
|
|
2306
|
+
inputChannel = null;
|
|
2307
|
+
outputChannel = null;
|
|
2308
|
+
presenceChannel = null;
|
|
2309
|
+
constructor(options) {
|
|
2310
|
+
super();
|
|
2311
|
+
this.supabase = options.supabase;
|
|
2312
|
+
this.machineId = options.machineId;
|
|
2313
|
+
}
|
|
2314
|
+
async connect() {
|
|
2315
|
+
const inputChannelName = import_clautunnel_shared2.REALTIME_CHANNELS.machineInput(this.machineId);
|
|
2316
|
+
this.inputChannel = this.supabase.channel(inputChannelName);
|
|
2317
|
+
this.inputChannel.on("broadcast", { event: "machine-command" }, (payload) => {
|
|
2318
|
+
this.emit("command", payload.payload);
|
|
2319
|
+
});
|
|
2320
|
+
const outputChannelName = import_clautunnel_shared2.REALTIME_CHANNELS.machineOutput(this.machineId);
|
|
2321
|
+
this.outputChannel = this.supabase.channel(outputChannelName);
|
|
2322
|
+
const results = await Promise.all([
|
|
2323
|
+
subscribeWithTimeout(this.inputChannel, "machine-input"),
|
|
2324
|
+
subscribeWithTimeout(this.outputChannel, "machine-output")
|
|
2325
|
+
]);
|
|
2326
|
+
const connected = results.every((success) => success);
|
|
2327
|
+
if (connected) {
|
|
2328
|
+
const presenceChannelName = import_clautunnel_shared2.REALTIME_CHANNELS.machinePresence(this.machineId);
|
|
2329
|
+
this.presenceChannel = this.supabase.channel(presenceChannelName);
|
|
2330
|
+
this.presenceChannel.subscribe(async (status) => {
|
|
2331
|
+
if (status === "SUBSCRIBED" && this.presenceChannel) {
|
|
2332
|
+
try {
|
|
2333
|
+
const payload = {
|
|
2334
|
+
type: "cli",
|
|
2335
|
+
online_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2336
|
+
};
|
|
2337
|
+
await this.presenceChannel.track(payload);
|
|
2338
|
+
} catch (trackError) {
|
|
2339
|
+
console.warn("[WARN] Failed to track machine presence:", trackError);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
return connected;
|
|
2345
|
+
}
|
|
2346
|
+
async broadcastSessionStarted(sessionId, workingDirectory) {
|
|
2347
|
+
if (!this.outputChannel) return;
|
|
2348
|
+
const command = {
|
|
2349
|
+
type: "session-started",
|
|
2350
|
+
sessionId,
|
|
2351
|
+
workingDirectory,
|
|
2352
|
+
timestamp: Date.now()
|
|
2353
|
+
};
|
|
2354
|
+
await this.outputChannel.send({
|
|
2355
|
+
type: "broadcast",
|
|
2356
|
+
event: "machine-command",
|
|
2357
|
+
payload: command
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
async broadcastSessionEnded(sessionId) {
|
|
2361
|
+
if (!this.outputChannel) return;
|
|
2362
|
+
const command = {
|
|
2363
|
+
type: "session-ended",
|
|
2364
|
+
sessionId,
|
|
2365
|
+
timestamp: Date.now()
|
|
2366
|
+
};
|
|
2367
|
+
await this.outputChannel.send({
|
|
2368
|
+
type: "broadcast",
|
|
2369
|
+
event: "machine-command",
|
|
2370
|
+
payload: command
|
|
2371
|
+
});
|
|
2372
|
+
}
|
|
2373
|
+
async broadcastError(error) {
|
|
2374
|
+
if (!this.outputChannel) return;
|
|
2375
|
+
const command = {
|
|
2376
|
+
type: "start-session-error",
|
|
2377
|
+
error,
|
|
2378
|
+
timestamp: Date.now()
|
|
2379
|
+
};
|
|
2380
|
+
await this.outputChannel.send({
|
|
2381
|
+
type: "broadcast",
|
|
2382
|
+
event: "machine-command",
|
|
2383
|
+
payload: command
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
async disconnect() {
|
|
2387
|
+
if (this.presenceChannel) {
|
|
2388
|
+
await this.presenceChannel.untrack();
|
|
2389
|
+
await this.supabase.removeChannel(this.presenceChannel);
|
|
2390
|
+
this.presenceChannel = null;
|
|
2391
|
+
}
|
|
2392
|
+
if (this.inputChannel) {
|
|
2393
|
+
await this.supabase.removeChannel(this.inputChannel);
|
|
2394
|
+
this.inputChannel = null;
|
|
2395
|
+
}
|
|
2396
|
+
if (this.outputChannel) {
|
|
2397
|
+
await this.supabase.removeChannel(this.outputChannel);
|
|
2398
|
+
this.outputChannel = null;
|
|
2399
|
+
}
|
|
2400
|
+
this.emit("disconnected");
|
|
2401
|
+
}
|
|
2402
|
+
};
|
|
2403
|
+
|
|
2404
|
+
// src/utils/spinner.ts
|
|
2405
|
+
var Spinner = class {
|
|
2406
|
+
frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
2407
|
+
currentFrame = 0;
|
|
2408
|
+
interval = null;
|
|
2409
|
+
message;
|
|
2410
|
+
stream;
|
|
2411
|
+
constructor(message, stream = process.stdout) {
|
|
2412
|
+
this.message = message;
|
|
2413
|
+
this.stream = stream;
|
|
2414
|
+
}
|
|
2415
|
+
start() {
|
|
2416
|
+
if (this.interval) {
|
|
2417
|
+
return;
|
|
2418
|
+
}
|
|
2419
|
+
this.stream.write("\x1B[?25l");
|
|
2420
|
+
this.interval = setInterval(() => {
|
|
2421
|
+
const frame = this.frames[this.currentFrame];
|
|
2422
|
+
this.stream.write(`\r${frame} ${this.message}`);
|
|
2423
|
+
this.currentFrame = (this.currentFrame + 1) % this.frames.length;
|
|
2424
|
+
}, 80);
|
|
2425
|
+
}
|
|
2426
|
+
update(message) {
|
|
2427
|
+
this.message = message;
|
|
2428
|
+
}
|
|
2429
|
+
stop() {
|
|
2430
|
+
if (this.interval) {
|
|
2431
|
+
clearInterval(this.interval);
|
|
2432
|
+
this.interval = null;
|
|
2433
|
+
}
|
|
2434
|
+
this.stream.write("\r\x1B[K");
|
|
2435
|
+
this.stream.write("\x1B[?25h");
|
|
2436
|
+
}
|
|
2437
|
+
succeed(message) {
|
|
2438
|
+
this.stop();
|
|
2439
|
+
this.stream.write(`\r\u2713 ${message}
|
|
2440
|
+
`);
|
|
2441
|
+
}
|
|
2442
|
+
fail(message) {
|
|
2443
|
+
this.stop();
|
|
2444
|
+
this.stream.write(`\r\u2717 ${message}
|
|
2445
|
+
`);
|
|
2446
|
+
}
|
|
2447
|
+
};
|
|
2448
|
+
|
|
2449
|
+
// src/utils/supabase.ts
|
|
2450
|
+
import { createClient } from "@supabase/supabase-js";
|
|
2451
|
+
function createSupabaseClient(url, anonKey, options) {
|
|
2452
|
+
const clientOptions = options?.realtime ? {
|
|
2453
|
+
realtime: {
|
|
2454
|
+
params: { eventsPerSecond: 10 },
|
|
2455
|
+
timeout: 3e4
|
|
2456
|
+
}
|
|
2457
|
+
} : void 0;
|
|
2458
|
+
return createClient(url, anonKey, clientOptions);
|
|
2459
|
+
}
|
|
2460
|
+
async function restoreSession(supabase, config2) {
|
|
2461
|
+
const sessionTokens = config2.getSessionTokens();
|
|
2462
|
+
if (!sessionTokens) {
|
|
2463
|
+
return null;
|
|
2464
|
+
}
|
|
2465
|
+
const { error: sessionError } = await supabase.auth.setSession({
|
|
2466
|
+
access_token: sessionTokens.accessToken,
|
|
2467
|
+
refresh_token: sessionTokens.refreshToken
|
|
2468
|
+
});
|
|
2469
|
+
if (sessionError) {
|
|
2470
|
+
config2.clearSessionTokens();
|
|
2471
|
+
return null;
|
|
2472
|
+
}
|
|
2473
|
+
const {
|
|
2474
|
+
data: { user },
|
|
2475
|
+
error: authError
|
|
2476
|
+
} = await supabase.auth.getUser();
|
|
2477
|
+
if (authError || !user) {
|
|
2478
|
+
return null;
|
|
2479
|
+
}
|
|
2480
|
+
return { user };
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
// src/utils/sleep-prevention.ts
|
|
2484
|
+
import { spawn, execSync } from "child_process";
|
|
2485
|
+
import * as readline from "readline";
|
|
2486
|
+
import { readdirSync as readdirSync2 } from "fs";
|
|
2487
|
+
import * as path2 from "path";
|
|
2488
|
+
import * as os3 from "os";
|
|
2489
|
+
async function promptYesNo(question) {
|
|
2490
|
+
const rl = readline.createInterface({
|
|
2491
|
+
input: process.stdin,
|
|
2492
|
+
output: process.stdout
|
|
2493
|
+
});
|
|
2494
|
+
return new Promise((resolve2) => {
|
|
2495
|
+
rl.question(question, (answer) => {
|
|
2496
|
+
rl.close();
|
|
2497
|
+
resolve2(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
2498
|
+
});
|
|
2499
|
+
});
|
|
2500
|
+
}
|
|
2501
|
+
function enableSleepPrevention() {
|
|
2502
|
+
try {
|
|
2503
|
+
execSync("sudo pmset -a disablesleep 1", { stdio: "inherit" });
|
|
2504
|
+
return true;
|
|
2505
|
+
} catch {
|
|
2506
|
+
return false;
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
function disableSleepPrevention() {
|
|
2510
|
+
try {
|
|
2511
|
+
execSync("sudo pmset -a disablesleep 0", { stdio: "inherit" });
|
|
2512
|
+
} catch {
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
function startCaffeinate() {
|
|
2516
|
+
const process2 = spawn("caffeinate", ["-i", "-s"], {
|
|
2517
|
+
stdio: "ignore",
|
|
2518
|
+
detached: false
|
|
2519
|
+
});
|
|
2520
|
+
return process2;
|
|
2521
|
+
}
|
|
2522
|
+
function stopCaffeinate(process2) {
|
|
2523
|
+
if (process2) {
|
|
2524
|
+
process2.kill();
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
function cleanup(state) {
|
|
2528
|
+
stopCaffeinate(state.caffeinateProcess);
|
|
2529
|
+
if (state.pmsetEnabled) {
|
|
2530
|
+
disableSleepPrevention();
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
function isMacOS() {
|
|
2534
|
+
return process.platform === "darwin";
|
|
2535
|
+
}
|
|
2536
|
+
function checkFullDiskAccess() {
|
|
2537
|
+
if (!isMacOS()) return true;
|
|
2538
|
+
try {
|
|
2539
|
+
const safariDir = path2.join(os3.homedir(), "Library", "Safari");
|
|
2540
|
+
readdirSync2(safariDir);
|
|
2541
|
+
return true;
|
|
2542
|
+
} catch {
|
|
2543
|
+
return false;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
var TERM_PROGRAM_MAP = {
|
|
2547
|
+
vscode: "Visual Studio Code",
|
|
2548
|
+
Apple_Terminal: "Terminal",
|
|
2549
|
+
"iTerm.app": "iTerm2",
|
|
2550
|
+
WarpTerminal: "Warp",
|
|
2551
|
+
Hyper: "Hyper"
|
|
2552
|
+
};
|
|
2553
|
+
function getTerminalAppName() {
|
|
2554
|
+
const termProgram = process.env.TERM_PROGRAM;
|
|
2555
|
+
if (termProgram && TERM_PROGRAM_MAP[termProgram]) {
|
|
2556
|
+
return TERM_PROGRAM_MAP[termProgram];
|
|
2557
|
+
}
|
|
2558
|
+
return "your terminal app";
|
|
2559
|
+
}
|
|
2560
|
+
function openFullDiskAccessSettings() {
|
|
2561
|
+
if (!isMacOS()) return false;
|
|
2562
|
+
try {
|
|
2563
|
+
execSync(
|
|
2564
|
+
'open "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"'
|
|
2565
|
+
);
|
|
2566
|
+
return true;
|
|
2567
|
+
} catch {
|
|
2568
|
+
return false;
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
function getFullDiskAccessStatus(enabled, terminalApp) {
|
|
2572
|
+
if (enabled) {
|
|
2573
|
+
return {
|
|
2574
|
+
enabled: true,
|
|
2575
|
+
label: "Enabled"
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
const appName = terminalApp || "your terminal app";
|
|
2579
|
+
return {
|
|
2580
|
+
enabled: false,
|
|
2581
|
+
label: "Not enabled",
|
|
2582
|
+
warning: [
|
|
2583
|
+
"Without Full Disk Access, macOS may show permission dialogs",
|
|
2584
|
+
"when Claude tries to access certain files or directories.",
|
|
2585
|
+
"These dialogs are only visible on this machine's screen \u2014",
|
|
2586
|
+
"you won't be able to see or approve them from the mobile app,",
|
|
2587
|
+
"which will cause Claude's operations to silently hang.",
|
|
2588
|
+
"",
|
|
2589
|
+
"To enable:",
|
|
2590
|
+
` 1. Open System Settings \u2192 Privacy & Security \u2192 Full Disk Access`,
|
|
2591
|
+
` 2. Toggle ON "${appName}" in the list`,
|
|
2592
|
+
' 3. Restart your terminal and run "clautunnel start" again'
|
|
2593
|
+
].join("\n")
|
|
2594
|
+
};
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
// src/commands/start.ts
|
|
2598
|
+
if (typeof globalThis.WebSocket === "undefined") {
|
|
2599
|
+
globalThis.WebSocket = WebSocket;
|
|
2600
|
+
}
|
|
2601
|
+
function createStartCommand() {
|
|
2602
|
+
const command = new Command("start");
|
|
2603
|
+
command.description("Start ClauTunnel and listen for session requests from mobile app").option("-n, --name <name>", "Machine name").option("--prevent-sleep", "Auto-enable sleep prevention (skip prompt)").action(async (options) => {
|
|
2604
|
+
const config2 = new Config();
|
|
2605
|
+
const logger = new Logger();
|
|
2606
|
+
const spinner = new Spinner("Starting ClauTunnel...");
|
|
2607
|
+
const daemons = /* @__PURE__ */ new Map();
|
|
2608
|
+
let machineClient = null;
|
|
2609
|
+
try {
|
|
2610
|
+
config2.requireConfiguration();
|
|
2611
|
+
spinner.start();
|
|
2612
|
+
const supabase = createSupabaseClient(
|
|
2613
|
+
config2.getSupabaseUrl(),
|
|
2614
|
+
config2.getSupabaseAnonKey(),
|
|
2615
|
+
{ realtime: true }
|
|
2616
|
+
);
|
|
2617
|
+
spinner.update("Authenticating...");
|
|
2618
|
+
const session = await restoreSession(supabase, config2);
|
|
2619
|
+
if (!session) {
|
|
2620
|
+
spinner.fail("Not authenticated");
|
|
2621
|
+
logger.error('Run "clautunnel login" first.');
|
|
2622
|
+
process.exit(1);
|
|
2623
|
+
}
|
|
2624
|
+
const { user } = session;
|
|
2625
|
+
let fdaStatus = null;
|
|
2626
|
+
if (isMacOS()) {
|
|
2627
|
+
spinner.stop();
|
|
2628
|
+
const terminalApp = getTerminalAppName();
|
|
2629
|
+
let fdaEnabled = checkFullDiskAccess();
|
|
2630
|
+
fdaStatus = getFullDiskAccessStatus(fdaEnabled, terminalApp);
|
|
2631
|
+
if (fdaStatus.enabled) {
|
|
2632
|
+
logger.info(`\u2713 Full Disk Access: ${fdaStatus.label}`);
|
|
2633
|
+
} else {
|
|
2634
|
+
logger.warn(`\u26A0 Full Disk Access: ${fdaStatus.label}`);
|
|
2635
|
+
logger.warn("");
|
|
2636
|
+
for (const line of fdaStatus.warning.split("\n")) {
|
|
2637
|
+
logger.warn(` ${line}`);
|
|
2638
|
+
}
|
|
2639
|
+
logger.warn("");
|
|
2640
|
+
const openSettings = await promptYesNo(
|
|
2641
|
+
"Open Full Disk Access settings? [Y/n]: "
|
|
2642
|
+
);
|
|
2643
|
+
if (openSettings) {
|
|
2644
|
+
openFullDiskAccessSettings();
|
|
2645
|
+
logger.info("");
|
|
2646
|
+
await promptYesNo(
|
|
2647
|
+
"Press Enter after enabling Full Disk Access (or Enter to skip): "
|
|
2648
|
+
);
|
|
2649
|
+
fdaEnabled = checkFullDiskAccess();
|
|
2650
|
+
fdaStatus = getFullDiskAccessStatus(fdaEnabled, terminalApp);
|
|
2651
|
+
if (fdaStatus.enabled) {
|
|
2652
|
+
logger.info("\u2713 Full Disk Access: Enabled");
|
|
2653
|
+
} else {
|
|
2654
|
+
logger.warn("Full Disk Access still not enabled. Continuing without it.");
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
logger.info("");
|
|
2658
|
+
}
|
|
2659
|
+
spinner.start();
|
|
2660
|
+
}
|
|
2661
|
+
const sleepState = {
|
|
2662
|
+
caffeinateProcess: null,
|
|
2663
|
+
pmsetEnabled: false
|
|
2664
|
+
};
|
|
2665
|
+
if (isMacOS()) {
|
|
2666
|
+
spinner.stop();
|
|
2667
|
+
const enableSleep = options.preventSleep || await promptYesNo(
|
|
2668
|
+
"Prevent sleep when lid is closed? (keeps clautunnel running) [y/N]: "
|
|
2669
|
+
);
|
|
2670
|
+
if (enableSleep) {
|
|
2671
|
+
logger.info("");
|
|
2672
|
+
logger.info("Enabling sleep prevention...");
|
|
2673
|
+
if (!options.preventSleep) {
|
|
2674
|
+
logger.info("This requires sudo password (auto-restored on exit)");
|
|
2675
|
+
logger.info("");
|
|
2676
|
+
}
|
|
2677
|
+
sleepState.pmsetEnabled = enableSleepPrevention();
|
|
2678
|
+
if (sleepState.pmsetEnabled) {
|
|
2679
|
+
logger.info("\u2713 Lid-closed mode enabled");
|
|
2680
|
+
} else {
|
|
2681
|
+
logger.warn("Failed to enable lid-closed mode. Using basic mode.");
|
|
2682
|
+
}
|
|
2683
|
+
sleepState.caffeinateProcess = startCaffeinate();
|
|
2684
|
+
sleepState.caffeinateProcess.on("error", () => {
|
|
2685
|
+
logger.warn("Failed to start caffeinate");
|
|
2686
|
+
});
|
|
2687
|
+
logger.info("");
|
|
2688
|
+
}
|
|
2689
|
+
spinner.start();
|
|
2690
|
+
}
|
|
2691
|
+
const cleanup2 = () => {
|
|
2692
|
+
if (sleepState.pmsetEnabled) {
|
|
2693
|
+
console.log("Restoring sleep settings...");
|
|
2694
|
+
}
|
|
2695
|
+
cleanup(sleepState);
|
|
2696
|
+
};
|
|
2697
|
+
let isShuttingDown = false;
|
|
2698
|
+
const gracefulShutdown = async (signal) => {
|
|
2699
|
+
if (isShuttingDown) {
|
|
2700
|
+
console.log("\nForce exiting...");
|
|
2701
|
+
process.exit(1);
|
|
2702
|
+
}
|
|
2703
|
+
isShuttingDown = true;
|
|
2704
|
+
console.log(`
|
|
2705
|
+
[${signal}] Shutting down gracefully...`);
|
|
2706
|
+
try {
|
|
2707
|
+
for (const [sessionId, d] of daemons) {
|
|
2708
|
+
try {
|
|
2709
|
+
await d.stop();
|
|
2710
|
+
} catch {
|
|
2711
|
+
}
|
|
2712
|
+
daemons.delete(sessionId);
|
|
2713
|
+
}
|
|
2714
|
+
if (machineClient) {
|
|
2715
|
+
await machineClient.disconnect();
|
|
2716
|
+
machineClient = null;
|
|
2717
|
+
}
|
|
2718
|
+
console.log("[Cleanup] All sessions ended in database");
|
|
2719
|
+
await cleanup2();
|
|
2720
|
+
} catch (error) {
|
|
2721
|
+
console.error("[Cleanup] Error during shutdown:", error);
|
|
2722
|
+
}
|
|
2723
|
+
process.exit(0);
|
|
2724
|
+
};
|
|
2725
|
+
process.on("SIGINT", () => {
|
|
2726
|
+
gracefulShutdown("SIGINT").catch(console.error);
|
|
2727
|
+
});
|
|
2728
|
+
process.on("SIGTERM", () => {
|
|
2729
|
+
gracefulShutdown("SIGTERM").catch(console.error);
|
|
2730
|
+
});
|
|
2731
|
+
spinner.update("Registering machine...");
|
|
2732
|
+
const machineManager = new MachineManager({ supabase });
|
|
2733
|
+
const machine = await machineManager.registerMachine(
|
|
2734
|
+
user.id,
|
|
2735
|
+
options.name,
|
|
2736
|
+
config2.getMachineId()
|
|
2737
|
+
);
|
|
2738
|
+
config2.setMachineId(machine.id);
|
|
2739
|
+
spinner.update("Connecting to realtime...");
|
|
2740
|
+
machineClient = new MachineRealtimeClient({
|
|
2741
|
+
supabase,
|
|
2742
|
+
machineId: machine.id
|
|
2743
|
+
});
|
|
2744
|
+
const connected = await machineClient.connect();
|
|
2745
|
+
spinner.stop();
|
|
2746
|
+
if (!connected) {
|
|
2747
|
+
logger.error(
|
|
2748
|
+
"Failed to connect to realtime. Check your network connection."
|
|
2749
|
+
);
|
|
2750
|
+
process.exit(1);
|
|
2751
|
+
}
|
|
2752
|
+
logger.info("");
|
|
2753
|
+
logger.info("\u2713 ClauTunnel is ready!");
|
|
2754
|
+
logger.info(` Machine: ${machine.name}`);
|
|
2755
|
+
if (fdaStatus) {
|
|
2756
|
+
if (fdaStatus.enabled) {
|
|
2757
|
+
logger.info(` Full Disk Access: ${fdaStatus.label}`);
|
|
2758
|
+
} else {
|
|
2759
|
+
logger.warn(` Full Disk Access: ${fdaStatus.label}`);
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
if (sleepState.caffeinateProcess) {
|
|
2763
|
+
logger.info(
|
|
2764
|
+
` Sleep prevention: ${sleepState.pmsetEnabled ? "Lid-closed mode" : "Basic mode"}`
|
|
2765
|
+
);
|
|
2766
|
+
}
|
|
2767
|
+
logger.info("");
|
|
2768
|
+
logger.info("Open the mobile app to start a session.");
|
|
2769
|
+
logger.info("Press Ctrl+C to stop.");
|
|
2770
|
+
logger.info("");
|
|
2771
|
+
machineClient.on("command", async (cmd) => {
|
|
2772
|
+
if (cmd.type === "start-session") {
|
|
2773
|
+
logger.info("Starting session (requested from mobile)...");
|
|
2774
|
+
try {
|
|
2775
|
+
const newDaemon = new Daemon({
|
|
2776
|
+
supabase,
|
|
2777
|
+
userId: user.id,
|
|
2778
|
+
machineId: machine.id,
|
|
2779
|
+
machineName: options.name,
|
|
2780
|
+
cwd: process.cwd(),
|
|
2781
|
+
hybrid: false
|
|
2782
|
+
});
|
|
2783
|
+
newDaemon.on("started", async ({ session: session2 }) => {
|
|
2784
|
+
daemons.set(session2.id, newDaemon);
|
|
2785
|
+
logger.info(` Session: ${session2.id.slice(0, 8)}...`);
|
|
2786
|
+
logger.info(" Mobile sync: Enabled");
|
|
2787
|
+
logger.info(` Active sessions: ${daemons.size}`);
|
|
2788
|
+
logger.info("");
|
|
2789
|
+
await machineClient?.broadcastSessionStarted(
|
|
2790
|
+
session2.id,
|
|
2791
|
+
process.cwd()
|
|
2792
|
+
);
|
|
2793
|
+
});
|
|
2794
|
+
newDaemon.on("error", (error) => {
|
|
2795
|
+
logger.error(`Session error: ${error.message}`);
|
|
2796
|
+
});
|
|
2797
|
+
newDaemon.on("mobile-input", (prompt2, attachments) => {
|
|
2798
|
+
const hasImages = attachments && attachments.length > 0;
|
|
2799
|
+
const imageInfo = hasImages ? ` [+${attachments.length} image${attachments.length > 1 ? "s" : ""}]` : "";
|
|
2800
|
+
logger.info(`[Mobile] ${prompt2}${imageInfo}`);
|
|
2801
|
+
});
|
|
2802
|
+
newDaemon.on("mobile-output", (data) => {
|
|
2803
|
+
const trimmed = data.trim();
|
|
2804
|
+
if (trimmed) {
|
|
2805
|
+
logger.info(`[Claude] ${trimmed}`);
|
|
2806
|
+
}
|
|
2807
|
+
});
|
|
2808
|
+
newDaemon.on("mobile-disconnected", async () => {
|
|
2809
|
+
logger.info("Mobile disconnected. Ending session...");
|
|
2810
|
+
try {
|
|
2811
|
+
await newDaemon.stop();
|
|
2812
|
+
} catch {
|
|
2813
|
+
}
|
|
2814
|
+
});
|
|
2815
|
+
newDaemon.on("stopped", async () => {
|
|
2816
|
+
const sessionId = newDaemon.getSession()?.id;
|
|
2817
|
+
if (sessionId) {
|
|
2818
|
+
daemons.delete(sessionId);
|
|
2819
|
+
await machineClient?.broadcastSessionEnded(sessionId);
|
|
2820
|
+
}
|
|
2821
|
+
logger.info("Session ended.");
|
|
2822
|
+
logger.info(` Active sessions: ${daemons.size}`);
|
|
2823
|
+
logger.info("");
|
|
2824
|
+
if (daemons.size === 0) {
|
|
2825
|
+
logger.info("Waiting for next session request...");
|
|
2826
|
+
logger.info("");
|
|
2827
|
+
}
|
|
2828
|
+
});
|
|
2829
|
+
await newDaemon.start();
|
|
2830
|
+
} catch (error) {
|
|
2831
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
2832
|
+
logger.error(`Failed to start session: ${errorMessage}`);
|
|
2833
|
+
await machineClient?.broadcastError(errorMessage);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
if (cmd.type === "stop-session" && cmd.sessionId) {
|
|
2837
|
+
const targetDaemon = daemons.get(cmd.sessionId);
|
|
2838
|
+
if (targetDaemon) {
|
|
2839
|
+
logger.info(`Stopping session ${cmd.sessionId.slice(0, 8)}... (requested from mobile)`);
|
|
2840
|
+
try {
|
|
2841
|
+
await targetDaemon.stop();
|
|
2842
|
+
} catch {
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
});
|
|
2847
|
+
} catch (error) {
|
|
2848
|
+
if (error instanceof ConfigurationError) {
|
|
2849
|
+
spinner.stop();
|
|
2850
|
+
logger.error(error.message);
|
|
2851
|
+
process.exit(1);
|
|
2852
|
+
}
|
|
2853
|
+
spinner.fail("Failed to start");
|
|
2854
|
+
logger.error(
|
|
2855
|
+
`${error instanceof Error ? error.message : "Unknown error"}`
|
|
2856
|
+
);
|
|
2857
|
+
process.exit(1);
|
|
2858
|
+
}
|
|
2859
|
+
});
|
|
2860
|
+
return command;
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
// src/commands/stop.ts
|
|
2864
|
+
import { Command as Command2 } from "commander";
|
|
2865
|
+
import * as fs2 from "fs";
|
|
2866
|
+
import * as path3 from "path";
|
|
2867
|
+
import * as os4 from "os";
|
|
2868
|
+
var PID_FILE = path3.join(os4.homedir(), ".clautunnel", "daemon.pid");
|
|
2869
|
+
function createStopCommand() {
|
|
2870
|
+
const command = new Command2("stop");
|
|
2871
|
+
command.description("Stop the running daemon").action(async () => {
|
|
2872
|
+
const logger = new Logger();
|
|
2873
|
+
try {
|
|
2874
|
+
if (!fs2.existsSync(PID_FILE)) {
|
|
2875
|
+
logger.info("No daemon is running");
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
const pid = parseInt(fs2.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
2879
|
+
if (isNaN(pid)) {
|
|
2880
|
+
logger.error("Invalid PID file");
|
|
2881
|
+
fs2.unlinkSync(PID_FILE);
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
try {
|
|
2885
|
+
process.kill(pid, 0);
|
|
2886
|
+
process.kill(pid, "SIGTERM");
|
|
2887
|
+
logger.info(`Sent stop signal to daemon (PID: ${pid})`);
|
|
2888
|
+
let attempts = 0;
|
|
2889
|
+
while (attempts < 10) {
|
|
2890
|
+
await new Promise((resolve2) => setTimeout(resolve2, 500));
|
|
2891
|
+
try {
|
|
2892
|
+
process.kill(pid, 0);
|
|
2893
|
+
attempts++;
|
|
2894
|
+
} catch {
|
|
2895
|
+
break;
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
if (attempts >= 10) {
|
|
2899
|
+
logger.warn("Daemon did not stop gracefully, sending SIGKILL");
|
|
2900
|
+
process.kill(pid, "SIGKILL");
|
|
2901
|
+
}
|
|
2902
|
+
logger.info("Daemon stopped");
|
|
2903
|
+
} catch (err) {
|
|
2904
|
+
if (err.code === "ESRCH") {
|
|
2905
|
+
logger.info("Daemon process not found (already stopped)");
|
|
2906
|
+
} else {
|
|
2907
|
+
throw err;
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
if (fs2.existsSync(PID_FILE)) {
|
|
2911
|
+
fs2.unlinkSync(PID_FILE);
|
|
2912
|
+
}
|
|
2913
|
+
} catch (error) {
|
|
2914
|
+
logger.error(
|
|
2915
|
+
`Failed to stop daemon: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2916
|
+
);
|
|
2917
|
+
process.exit(1);
|
|
2918
|
+
}
|
|
2919
|
+
});
|
|
2920
|
+
return command;
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
// src/commands/status.ts
|
|
2924
|
+
import { Command as Command3 } from "commander";
|
|
2925
|
+
function createStatusCommand() {
|
|
2926
|
+
const command = new Command3("status");
|
|
2927
|
+
command.description("Show connection status").action(async () => {
|
|
2928
|
+
const config2 = new Config();
|
|
2929
|
+
const logger = new Logger();
|
|
2930
|
+
try {
|
|
2931
|
+
const supabase = createSupabaseClient(
|
|
2932
|
+
config2.getSupabaseUrl(),
|
|
2933
|
+
config2.getSupabaseAnonKey()
|
|
2934
|
+
);
|
|
2935
|
+
const session = await restoreSession(supabase, config2);
|
|
2936
|
+
if (!session) {
|
|
2937
|
+
logger.info("Status: Not authenticated");
|
|
2938
|
+
logger.info('Run "clautunnel login" to authenticate');
|
|
2939
|
+
return;
|
|
2940
|
+
}
|
|
2941
|
+
const { user } = session;
|
|
2942
|
+
logger.info(`User: ${user.email}`);
|
|
2943
|
+
const machineId = config2.getMachineId();
|
|
2944
|
+
if (!machineId) {
|
|
2945
|
+
logger.info("Machine: Not registered");
|
|
2946
|
+
logger.info('Run "clautunnel start" to register this machine');
|
|
2947
|
+
return;
|
|
2948
|
+
}
|
|
2949
|
+
const machineManager = new MachineManager({ supabase });
|
|
2950
|
+
const machine = await machineManager.getMachine(machineId);
|
|
2951
|
+
if (!machine) {
|
|
2952
|
+
logger.info(`Machine ID: ${machineId} (not found)`);
|
|
2953
|
+
return;
|
|
2954
|
+
}
|
|
2955
|
+
logger.info(`Machine: ${machine.name} (${machine.id})`);
|
|
2956
|
+
logger.info(`Status: ${machine.status}`);
|
|
2957
|
+
logger.info(`Last seen: ${machine.last_seen_at}`);
|
|
2958
|
+
} catch (error) {
|
|
2959
|
+
logger.error(
|
|
2960
|
+
`Failed to get status: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2961
|
+
);
|
|
2962
|
+
process.exit(1);
|
|
2963
|
+
}
|
|
2964
|
+
});
|
|
2965
|
+
return command;
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
// src/commands/login.ts
|
|
2969
|
+
import { Command as Command4 } from "commander";
|
|
2970
|
+
|
|
2971
|
+
// src/utils/prompt.ts
|
|
2972
|
+
import * as readline2 from "readline";
|
|
2973
|
+
function prompt(question) {
|
|
2974
|
+
const rl = readline2.createInterface({
|
|
2975
|
+
input: process.stdin,
|
|
2976
|
+
output: process.stdout
|
|
2977
|
+
});
|
|
2978
|
+
return new Promise((resolve2) => {
|
|
2979
|
+
rl.question(question, (answer) => {
|
|
2980
|
+
rl.close();
|
|
2981
|
+
resolve2(answer);
|
|
2982
|
+
});
|
|
2983
|
+
});
|
|
2984
|
+
}
|
|
2985
|
+
function promptHidden(question) {
|
|
2986
|
+
const rl = readline2.createInterface({
|
|
2987
|
+
input: process.stdin,
|
|
2988
|
+
output: process.stdout
|
|
2989
|
+
});
|
|
2990
|
+
return new Promise((resolve2) => {
|
|
2991
|
+
process.stdout.write(question);
|
|
2992
|
+
if (process.stdin.isTTY) {
|
|
2993
|
+
process.stdin.setRawMode(true);
|
|
2994
|
+
}
|
|
2995
|
+
let password = "";
|
|
2996
|
+
const onData = (char) => {
|
|
2997
|
+
const c = char.toString();
|
|
2998
|
+
if (c === "\n" || c === "\r") {
|
|
2999
|
+
process.stdin.removeListener("data", onData);
|
|
3000
|
+
if (process.stdin.isTTY) {
|
|
3001
|
+
process.stdin.setRawMode(false);
|
|
3002
|
+
}
|
|
3003
|
+
process.stdout.write("\n");
|
|
3004
|
+
rl.close();
|
|
3005
|
+
resolve2(password);
|
|
3006
|
+
} else if (c === "") {
|
|
3007
|
+
process.exit(0);
|
|
3008
|
+
} else if (c === "\x7F" || c === "\b") {
|
|
3009
|
+
if (password.length > 0) {
|
|
3010
|
+
password = password.slice(0, -1);
|
|
3011
|
+
}
|
|
3012
|
+
} else {
|
|
3013
|
+
password += c;
|
|
3014
|
+
}
|
|
3015
|
+
};
|
|
3016
|
+
process.stdin.on("data", onData);
|
|
3017
|
+
process.stdin.resume();
|
|
3018
|
+
});
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
// src/commands/login.ts
|
|
3022
|
+
function createLoginCommand() {
|
|
3023
|
+
const command = new Command4("login");
|
|
3024
|
+
command.description("Authenticate with ClauTunnel").action(async () => {
|
|
3025
|
+
const config2 = new Config();
|
|
3026
|
+
const logger = new Logger();
|
|
3027
|
+
try {
|
|
3028
|
+
config2.requireConfiguration();
|
|
3029
|
+
const supabase = createSupabaseClient(
|
|
3030
|
+
config2.getSupabaseUrl(),
|
|
3031
|
+
config2.getSupabaseAnonKey()
|
|
3032
|
+
);
|
|
3033
|
+
const email = await prompt("Email: ");
|
|
3034
|
+
const password = await promptHidden("Password: ");
|
|
3035
|
+
if (!email || !password) {
|
|
3036
|
+
logger.error("Email and password are required");
|
|
3037
|
+
process.exit(1);
|
|
3038
|
+
}
|
|
3039
|
+
const { data, error } = await supabase.auth.signInWithPassword({
|
|
3040
|
+
email,
|
|
3041
|
+
password
|
|
3042
|
+
});
|
|
3043
|
+
if (error) {
|
|
3044
|
+
logger.error(`Login failed: ${error.message}`);
|
|
3045
|
+
process.exit(1);
|
|
3046
|
+
}
|
|
3047
|
+
if (data.user) {
|
|
3048
|
+
logger.info(`Logged in as ${data.user.email}`);
|
|
3049
|
+
if (data.session) {
|
|
3050
|
+
config2.setSession({
|
|
3051
|
+
accessToken: data.session.access_token,
|
|
3052
|
+
refreshToken: data.session.refresh_token
|
|
3053
|
+
});
|
|
3054
|
+
logger.info("Session saved");
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
} catch (error) {
|
|
3058
|
+
if (error instanceof ConfigurationError) {
|
|
3059
|
+
logger.error(error.message);
|
|
3060
|
+
process.exit(1);
|
|
3061
|
+
}
|
|
3062
|
+
logger.error(
|
|
3063
|
+
`Login failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
3064
|
+
);
|
|
3065
|
+
process.exit(1);
|
|
3066
|
+
}
|
|
3067
|
+
});
|
|
3068
|
+
return command;
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
// src/commands/setup.ts
|
|
3072
|
+
import { Command as Command5 } from "commander";
|
|
3073
|
+
function createSetupCommand() {
|
|
3074
|
+
const command = new Command5("setup");
|
|
3075
|
+
command.description("Configure ClauTunnel with Supabase credentials").action(async () => {
|
|
3076
|
+
const config2 = new Config();
|
|
3077
|
+
const logger = new Logger();
|
|
3078
|
+
try {
|
|
3079
|
+
logger.info("ClauTunnel Setup");
|
|
3080
|
+
logger.info("================");
|
|
3081
|
+
logger.info("");
|
|
3082
|
+
logger.info("Enter your Supabase credentials to connect ClauTunnel.");
|
|
3083
|
+
logger.info("");
|
|
3084
|
+
logger.info("To find your credentials:");
|
|
3085
|
+
logger.info(" 1. Go to your Supabase project dashboard");
|
|
3086
|
+
logger.info(' 2. Settings > General > Copy "Project URL"');
|
|
3087
|
+
logger.info(' 3. Settings > API Keys > Copy "anon public" key');
|
|
3088
|
+
logger.info("");
|
|
3089
|
+
const url = await prompt("Supabase Project URL (e.g., https://xxxx.supabase.co): ");
|
|
3090
|
+
if (!url) {
|
|
3091
|
+
logger.error("Supabase URL is required");
|
|
3092
|
+
process.exit(1);
|
|
3093
|
+
}
|
|
3094
|
+
try {
|
|
3095
|
+
const parsedUrl = new URL(url);
|
|
3096
|
+
if (parsedUrl.hostname === "supabase.com") {
|
|
3097
|
+
logger.error("");
|
|
3098
|
+
logger.error("This looks like a dashboard URL, not the API URL.");
|
|
3099
|
+
logger.error("");
|
|
3100
|
+
logger.error("Please use the Project URL from Settings > API, which looks like:");
|
|
3101
|
+
logger.error(" https://your-project-id.supabase.co");
|
|
3102
|
+
logger.error("");
|
|
3103
|
+
logger.error("NOT the dashboard URL:");
|
|
3104
|
+
logger.error(" https://supabase.com/dashboard/project/...");
|
|
3105
|
+
process.exit(1);
|
|
3106
|
+
}
|
|
3107
|
+
if (!parsedUrl.hostname.endsWith(".supabase.co")) {
|
|
3108
|
+
logger.error("");
|
|
3109
|
+
logger.error("Invalid Supabase URL format.");
|
|
3110
|
+
logger.error("The URL should end with .supabase.co");
|
|
3111
|
+
logger.error("Example: https://your-project-id.supabase.co");
|
|
3112
|
+
process.exit(1);
|
|
3113
|
+
}
|
|
3114
|
+
} catch {
|
|
3115
|
+
logger.error("Invalid URL format. Please enter a valid URL (e.g., https://xxxx.supabase.co)");
|
|
3116
|
+
process.exit(1);
|
|
3117
|
+
}
|
|
3118
|
+
const anonKey = await prompt("Supabase Anon Key: ");
|
|
3119
|
+
if (!anonKey) {
|
|
3120
|
+
logger.error("Supabase Anon Key is required");
|
|
3121
|
+
process.exit(1);
|
|
3122
|
+
}
|
|
3123
|
+
config2.setSupabaseCredentials({ url, anonKey });
|
|
3124
|
+
logger.info("");
|
|
3125
|
+
logger.info("\u2713 Configuration saved successfully!");
|
|
3126
|
+
logger.info("");
|
|
3127
|
+
logger.info("Next steps:");
|
|
3128
|
+
logger.info(' 1. Run "clautunnel login" to authenticate');
|
|
3129
|
+
logger.info(' 2. Run "clautunnel start" to begin a session');
|
|
3130
|
+
} catch (error) {
|
|
3131
|
+
logger.error(
|
|
3132
|
+
`Setup failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
3133
|
+
);
|
|
3134
|
+
process.exit(1);
|
|
3135
|
+
}
|
|
3136
|
+
});
|
|
3137
|
+
return command;
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3140
|
+
// src/index.ts
|
|
3141
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
3142
|
+
var __dirname = dirname2(__filename);
|
|
3143
|
+
config({ path: resolve(__dirname, "../.env"), quiet: true });
|
|
3144
|
+
var packageJson = JSON.parse(
|
|
3145
|
+
readFileSync5(resolve(__dirname, "../package.json"), "utf-8")
|
|
3146
|
+
);
|
|
3147
|
+
var version = packageJson.version || "0.0.0";
|
|
3148
|
+
var program = new Command6();
|
|
3149
|
+
program.name("clautunnel").description("Remote control for Claude Code CLI").version(version);
|
|
3150
|
+
program.addCommand(createSetupCommand());
|
|
3151
|
+
program.addCommand(createStartCommand());
|
|
3152
|
+
program.addCommand(createStopCommand());
|
|
3153
|
+
program.addCommand(createStatusCommand());
|
|
3154
|
+
program.addCommand(createLoginCommand());
|
|
3155
|
+
if (process.argv[1]?.includes("clautunnel") || process.argv[1]?.endsWith("/index.js") || process.argv[1]?.endsWith("/index.ts")) {
|
|
3156
|
+
program.parse();
|
|
3157
|
+
}
|
|
3158
|
+
export {
|
|
3159
|
+
Config,
|
|
3160
|
+
Daemon,
|
|
3161
|
+
Logger,
|
|
3162
|
+
MachineManager,
|
|
3163
|
+
RealtimeClient,
|
|
3164
|
+
SessionManager,
|
|
3165
|
+
getConfig,
|
|
3166
|
+
getLogger
|
|
3167
|
+
};
|
|
3168
|
+
//# sourceMappingURL=index.js.map
|