cli-remote-agent 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,1921 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+
29
+ // src/config.ts
30
+ function defaultAgentId() {
31
+ return import_os.default.hostname().toLowerCase().replace(/[^a-z0-9-]/g, "-");
32
+ }
33
+ function defaultConfig() {
34
+ return {
35
+ agentId: defaultAgentId(),
36
+ // No serverUrl by default — the agent must be enrolled via `crc-agent setup`.
37
+ serverUrl: "",
38
+ // Random secret so an unconfigured install never ships a known secret.
39
+ secret: import_crypto.default.randomBytes(24).toString("hex"),
40
+ shell: "auto"
41
+ };
42
+ }
43
+ function isConfigured() {
44
+ if (!import_fs.default.existsSync(CONFIG_FILE)) return false;
45
+ try {
46
+ const raw = import_fs.default.readFileSync(CONFIG_FILE, "utf-8");
47
+ const cfg = JSON.parse(raw.replace(/^/, ""));
48
+ if (!cfg.serverUrl || !cfg.secret) return false;
49
+ if (cfg.secret === PLACEHOLDER_SECRET) return false;
50
+ return true;
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+ function loadConfig() {
56
+ if (!import_fs.default.existsSync(CONFIG_FILE)) {
57
+ const config2 = defaultConfig();
58
+ import_fs.default.mkdirSync(CONFIG_DIR, { recursive: true });
59
+ import_fs.default.writeFileSync(CONFIG_FILE, JSON.stringify(config2, null, 2));
60
+ console.log(`Created default config at ${CONFIG_FILE} \u2014 run \`crc-agent setup\` to enroll.`);
61
+ return config2;
62
+ }
63
+ try {
64
+ const raw = import_fs.default.readFileSync(CONFIG_FILE, "utf-8");
65
+ return JSON.parse(raw.replace(/^/, ""));
66
+ } catch (err) {
67
+ console.error(
68
+ `Failed to read/parse config at ${CONFIG_FILE}: ${err.message}
69
+ Falling back to default config. Fix the file and restart to apply your settings.`
70
+ );
71
+ return defaultConfig();
72
+ }
73
+ }
74
+ var import_fs, import_path, import_os, import_crypto, CONFIG_DIR, CONFIG_FILE, PLACEHOLDER_SECRET, LOCAL_CONTROL_PORT;
75
+ var init_config = __esm({
76
+ "src/config.ts"() {
77
+ "use strict";
78
+ import_fs = __toESM(require("fs"));
79
+ import_path = __toESM(require("path"));
80
+ import_os = __toESM(require("os"));
81
+ import_crypto = __toESM(require("crypto"));
82
+ CONFIG_DIR = import_path.default.join(import_os.default.homedir(), ".crc-agent");
83
+ CONFIG_FILE = import_path.default.join(CONFIG_DIR, "config.json");
84
+ PLACEHOLDER_SECRET = "changeme";
85
+ LOCAL_CONTROL_PORT = Number(process.env.CRC_LOCAL_PORT) || 47600;
86
+ }
87
+ });
88
+
89
+ // src/setup.ts
90
+ var setup_exports = {};
91
+ function parseArgs(argv) {
92
+ const out = {};
93
+ for (let i = 0; i < argv.length; i++) {
94
+ const a = argv[i];
95
+ if (a === "--token" || a === "-t") {
96
+ out.token = argv[++i];
97
+ } else if (a.startsWith("--token=")) {
98
+ out.token = a.slice("--token=".length);
99
+ } else if (a === "--help" || a === "-h") {
100
+ out.help = true;
101
+ }
102
+ }
103
+ return out;
104
+ }
105
+ function decodeToken(token) {
106
+ const json = Buffer.from(token, "base64url").toString("utf-8");
107
+ let parsed;
108
+ try {
109
+ parsed = JSON.parse(json);
110
+ } catch {
111
+ throw new Error("Invalid token: not valid base64url-encoded JSON.");
112
+ }
113
+ if (!parsed || typeof parsed !== "object") throw new Error("Invalid token payload.");
114
+ if (!parsed.serverUrl || !parsed.agentId || !parsed.secret) {
115
+ throw new Error("Invalid token: missing serverUrl, agentId, or secret.");
116
+ }
117
+ return parsed;
118
+ }
119
+ function ask(rl, question) {
120
+ return new Promise((resolve) => rl.question(question, (answer) => resolve(answer)));
121
+ }
122
+ function writeConfig(cfg) {
123
+ import_fs2.default.mkdirSync(CONFIG_DIR, { recursive: true });
124
+ import_fs2.default.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
125
+ console.log(`Saved. Start the agent with: crc-agent`);
126
+ }
127
+ async function main() {
128
+ const args = parseArgs(process.argv.slice(2));
129
+ if (args.help) {
130
+ console.log(
131
+ [
132
+ "Usage: crc-agent setup [--token <base64url>]",
133
+ "",
134
+ "Enroll this machine as a Claude Remote agent.",
135
+ "",
136
+ "Modes:",
137
+ ' --token <token> Enroll from the token shown in the web "Add agent" dialog.',
138
+ " (no args) Interactive prompts for serverUrl, agentId, secret.",
139
+ "",
140
+ "Environment overrides (skip prompts): CRC_SERVER_URL, CRC_AGENT_ID, CRC_SECRET",
141
+ "",
142
+ `Config is written to ${CONFIG_FILE}`
143
+ ].join("\n")
144
+ );
145
+ return;
146
+ }
147
+ const exists = import_fs2.default.existsSync(CONFIG_FILE);
148
+ if (args.token) {
149
+ const t = decodeToken(args.token);
150
+ writeConfig({ agentId: t.agentId, serverUrl: t.serverUrl, secret: t.secret, shell: "auto" });
151
+ return;
152
+ }
153
+ const envServer = process.env.CRC_SERVER_URL;
154
+ const envAgent = process.env.CRC_AGENT_ID;
155
+ const envSecret = process.env.CRC_SECRET;
156
+ if (envServer && envSecret) {
157
+ writeConfig({
158
+ agentId: envAgent || defaultAgentId(),
159
+ serverUrl: envServer,
160
+ secret: envSecret,
161
+ shell: "auto"
162
+ });
163
+ return;
164
+ }
165
+ const rl = import_readline.default.createInterface({ input: process.stdin, output: process.stdout });
166
+ try {
167
+ if (exists) {
168
+ const overwrite = await ask(rl, `Config already exists at ${CONFIG_FILE}. Overwrite? (y/N) `);
169
+ if (!/^y(es)?$/i.test(overwrite.trim())) {
170
+ console.log("Aborted. Existing config left unchanged.");
171
+ return;
172
+ }
173
+ }
174
+ const serverUrl = (envServer || await ask(rl, "Server URL (e.g. wss://crc.example.com): ")).trim();
175
+ if (!serverUrl) throw new Error("Server URL is required.");
176
+ const agentIdRaw = envAgent || await ask(rl, `Agent ID [${defaultAgentId()}]: `);
177
+ const agentId = (agentIdRaw || "").trim() || defaultAgentId();
178
+ const secret = (envSecret || await ask(rl, "Agent secret: ")).trim();
179
+ if (!secret) throw new Error("Secret is required.");
180
+ writeConfig({ agentId, serverUrl, secret, shell: "auto" });
181
+ } finally {
182
+ rl.close();
183
+ }
184
+ }
185
+ var import_fs2, import_readline;
186
+ var init_setup = __esm({
187
+ "src/setup.ts"() {
188
+ "use strict";
189
+ import_fs2 = __toESM(require("fs"));
190
+ import_readline = __toESM(require("readline"));
191
+ init_config();
192
+ main().catch((err) => {
193
+ console.error(`Setup failed: ${err instanceof Error ? err.message : String(err)}`);
194
+ process.exit(1);
195
+ });
196
+ }
197
+ });
198
+
199
+ // ../shared/src/types.ts
200
+ var init_types = __esm({
201
+ "../shared/src/types.ts"() {
202
+ "use strict";
203
+ }
204
+ });
205
+
206
+ // ../shared/src/protocol.ts
207
+ var AGENT_HEARTBEAT, TERMINAL_OUTPUT, TERMINAL_EXIT, TERMINAL_OPEN, TERMINAL_INPUT, TERMINAL_RESIZE, TERMINAL_CLOSE, SESSION_ATTACH, SESSION_DETACH, SESSION_BUFFER, SESSION_SYNC, SESSION_SYNC_RESULT, VPN_LIST, VPN_CONNECT, VPN_DISCONNECT, VPN_UPDATE, FILES_LIST, FILES_DOWNLOAD, FILES_LIST_RESULT, FILES_DOWNLOAD_READY, FILES_DOWNLOAD_ERROR, AGENT_EXEC, AGENT_EXEC_RESULT, CLAUDE_CONV_READ, CLAUDE_CONV_DATA, CLAUDE_SESSIONS_LIST, CLAUDE_SESSIONS_RESULT, CLAUDE_HOOK, TMUX_LIST, TMUX_LIST_RESULT;
208
+ var init_protocol = __esm({
209
+ "../shared/src/protocol.ts"() {
210
+ "use strict";
211
+ AGENT_HEARTBEAT = "agent:heartbeat";
212
+ TERMINAL_OUTPUT = "terminal:output";
213
+ TERMINAL_EXIT = "terminal:exit";
214
+ TERMINAL_OPEN = "terminal:open";
215
+ TERMINAL_INPUT = "terminal:input";
216
+ TERMINAL_RESIZE = "terminal:resize";
217
+ TERMINAL_CLOSE = "terminal:close";
218
+ SESSION_ATTACH = "session:attach";
219
+ SESSION_DETACH = "session:detach";
220
+ SESSION_BUFFER = "session:buffer";
221
+ SESSION_SYNC = "session:sync";
222
+ SESSION_SYNC_RESULT = "session:sync:result";
223
+ VPN_LIST = "vpn:list";
224
+ VPN_CONNECT = "vpn:connect";
225
+ VPN_DISCONNECT = "vpn:disconnect";
226
+ VPN_UPDATE = "vpn:update";
227
+ FILES_LIST = "files:list";
228
+ FILES_DOWNLOAD = "files:download";
229
+ FILES_LIST_RESULT = "files:list:result";
230
+ FILES_DOWNLOAD_READY = "files:download:ready";
231
+ FILES_DOWNLOAD_ERROR = "files:download:error";
232
+ AGENT_EXEC = "agent:exec";
233
+ AGENT_EXEC_RESULT = "agent:exec:result";
234
+ CLAUDE_CONV_READ = "claude:conv:read";
235
+ CLAUDE_CONV_DATA = "claude:conv:data";
236
+ CLAUDE_SESSIONS_LIST = "claude:sessions:list";
237
+ CLAUDE_SESSIONS_RESULT = "claude:sessions:result";
238
+ CLAUDE_HOOK = "claude:hook";
239
+ TMUX_LIST = "tmux:list";
240
+ TMUX_LIST_RESULT = "tmux:list:result";
241
+ }
242
+ });
243
+
244
+ // ../shared/src/constants.ts
245
+ var HEARTBEAT_INTERVAL, SESSION_BUFFER_SIZE, FILE_MAX_SIZE;
246
+ var init_constants = __esm({
247
+ "../shared/src/constants.ts"() {
248
+ "use strict";
249
+ HEARTBEAT_INTERVAL = 15e3;
250
+ SESSION_BUFFER_SIZE = 5e4;
251
+ FILE_MAX_SIZE = 524288e3;
252
+ }
253
+ });
254
+
255
+ // ../shared/src/index.ts
256
+ var init_src = __esm({
257
+ "../shared/src/index.ts"() {
258
+ "use strict";
259
+ init_types();
260
+ init_protocol();
261
+ init_constants();
262
+ }
263
+ });
264
+
265
+ // src/logger.ts
266
+ var import_pino, logger;
267
+ var init_logger = __esm({
268
+ "src/logger.ts"() {
269
+ "use strict";
270
+ import_pino = __toESM(require("pino"));
271
+ logger = (0, import_pino.default)({
272
+ level: "info",
273
+ transport: { target: "pino-pretty", options: { colorize: true } }
274
+ });
275
+ }
276
+ });
277
+
278
+ // src/claude-plugin-installer.ts
279
+ function getSettingsPath() {
280
+ const home = process.env.HOME || process.env.USERPROFILE || "";
281
+ return (0, import_path2.join)(home, ".claude", "settings.json");
282
+ }
283
+ function installClaudeHooks(port) {
284
+ const settingsPath = getSettingsPath();
285
+ const url = `http://127.0.0.1:${port}/hook`;
286
+ const command = `curl -s -m 2 -X POST -H "Content-Type: application/json" --data-binary @- ${url} >/dev/null 2>&1 || true`;
287
+ let settings = {};
288
+ try {
289
+ if ((0, import_fs3.existsSync)(settingsPath)) {
290
+ settings = JSON.parse((0, import_fs3.readFileSync)(settingsPath, "utf-8"));
291
+ }
292
+ } catch (err) {
293
+ logger.warn({ err }, "Could not parse ~/.claude/settings.json \u2014 skipping hook install");
294
+ return;
295
+ }
296
+ const hooks = settings.hooks || (settings.hooks = {});
297
+ const isOurs = (group) => JSON.stringify(group).includes(url);
298
+ const desired = { hooks: [{ type: "command", command }] };
299
+ let changed = false;
300
+ for (const ev of MANAGED_EVENTS) {
301
+ const existed = Array.isArray(hooks[ev]);
302
+ const kept = (existed ? hooks[ev] : []).filter((g) => !isOurs(g));
303
+ const next = INSTALL_EVENTS.includes(ev) ? [...kept, desired] : kept;
304
+ const before = existed ? JSON.stringify(hooks[ev]) : "";
305
+ if (next.length === 0) {
306
+ if (existed) {
307
+ delete hooks[ev];
308
+ changed = true;
309
+ }
310
+ } else if (before !== JSON.stringify(next)) {
311
+ hooks[ev] = next;
312
+ changed = true;
313
+ }
314
+ }
315
+ if (!changed) {
316
+ logger.info("CRC Claude notify hooks already present in settings.json");
317
+ return;
318
+ }
319
+ try {
320
+ (0, import_fs3.mkdirSync)((0, import_path2.dirname)(settingsPath), { recursive: true });
321
+ (0, import_fs3.writeFileSync)(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
322
+ logger.info({ install: INSTALL_EVENTS, url }, "Installed CRC Claude notify hooks into settings.json (restart claude to apply)");
323
+ } catch (err) {
324
+ logger.warn({ err }, "Failed to write CRC notify hooks to settings.json");
325
+ }
326
+ }
327
+ function normalizeClaudeHook(raw) {
328
+ if (!raw || typeof raw !== "object") return null;
329
+ if (typeof raw.event === "string" && !raw.hook_event_name) return raw;
330
+ const name = raw.hook_event_name;
331
+ if (name === "Stop" || name === "SubagentStop") {
332
+ return {
333
+ event: "stop",
334
+ projectPath: typeof raw.cwd === "string" ? raw.cwd : void 0,
335
+ claudeSessionId: typeof raw.session_id === "string" ? raw.session_id : void 0
336
+ };
337
+ }
338
+ return null;
339
+ }
340
+ function lastAssistantSummary(transcriptPath) {
341
+ if (!transcriptPath) return void 0;
342
+ try {
343
+ const lines = (0, import_fs3.readFileSync)(transcriptPath, "utf-8").split("\n");
344
+ for (let i = lines.length - 1; i >= 0; i--) {
345
+ const line = lines[i].trim();
346
+ if (!line) continue;
347
+ try {
348
+ const obj = JSON.parse(line);
349
+ if (obj?.type === "assistant" && obj.message?.content) {
350
+ const c = obj.message.content;
351
+ const text = Array.isArray(c) ? c.filter((x) => x?.type === "text").map((x) => x.text).join(" ") : typeof c === "string" ? c : "";
352
+ if (text.trim()) return text.trim().replace(/\s+/g, " ").slice(0, 180);
353
+ }
354
+ } catch {
355
+ }
356
+ }
357
+ } catch {
358
+ }
359
+ return void 0;
360
+ }
361
+ var import_fs3, import_path2, INSTALL_EVENTS, MANAGED_EVENTS;
362
+ var init_claude_plugin_installer = __esm({
363
+ "src/claude-plugin-installer.ts"() {
364
+ "use strict";
365
+ import_fs3 = require("fs");
366
+ import_path2 = require("path");
367
+ init_logger();
368
+ INSTALL_EVENTS = ["Stop"];
369
+ MANAGED_EVENTS = ["Stop", "Notification", "SubagentStop"];
370
+ }
371
+ });
372
+
373
+ // src/local-control.ts
374
+ function startLocalControl(port, onHook) {
375
+ const server = import_http.default.createServer((req, res) => {
376
+ if (req.method === "POST" && req.url === "/hook") {
377
+ let body = "";
378
+ req.on("data", (chunk) => {
379
+ body += chunk;
380
+ if (body.length > 64 * 1024) req.destroy();
381
+ });
382
+ req.on("end", () => {
383
+ try {
384
+ const payload = JSON.parse(body);
385
+ if (payload && typeof payload === "object") onHook(payload);
386
+ } catch {
387
+ }
388
+ res.writeHead(204);
389
+ res.end();
390
+ });
391
+ req.on("error", () => {
392
+ res.writeHead(400);
393
+ res.end();
394
+ });
395
+ return;
396
+ }
397
+ res.writeHead(404);
398
+ res.end();
399
+ });
400
+ server.on("error", (err) => {
401
+ logger.warn({ err, port }, "Local control server error (Claude hooks may not reach push)");
402
+ });
403
+ server.listen(port, "127.0.0.1", () => {
404
+ logger.info({ port }, "Local control endpoint listening (127.0.0.1) for Claude hooks");
405
+ });
406
+ return server;
407
+ }
408
+ var import_http;
409
+ var init_local_control = __esm({
410
+ "src/local-control.ts"() {
411
+ "use strict";
412
+ import_http = __toESM(require("http"));
413
+ init_logger();
414
+ }
415
+ });
416
+
417
+ // src/claude-live.ts
418
+ function isAlive(pid) {
419
+ try {
420
+ process.kill(pid, 0);
421
+ return true;
422
+ } catch {
423
+ return false;
424
+ }
425
+ }
426
+ function getLiveClaudeSessions() {
427
+ let files;
428
+ try {
429
+ files = import_fs4.default.readdirSync(LIVE_SESSIONS_DIR).filter((f) => f.endsWith(".json"));
430
+ } catch {
431
+ return [];
432
+ }
433
+ const out = [];
434
+ for (const f of files) {
435
+ try {
436
+ const obj = JSON.parse(import_fs4.default.readFileSync(import_path3.default.join(LIVE_SESSIONS_DIR, f), "utf-8"));
437
+ if (typeof obj?.pid !== "number" || !isAlive(obj.pid)) continue;
438
+ out.push({
439
+ pid: obj.pid,
440
+ cwd: typeof obj.cwd === "string" ? obj.cwd : "",
441
+ name: typeof obj.name === "string" ? obj.name : void 0,
442
+ status: typeof obj.status === "string" ? obj.status : void 0,
443
+ updatedAt: typeof obj.updatedAt === "number" ? obj.updatedAt : void 0
444
+ });
445
+ } catch {
446
+ }
447
+ }
448
+ out.sort((a, b) => (a.updatedAt || 0) - (b.updatedAt || 0));
449
+ return out;
450
+ }
451
+ async function getPidParents() {
452
+ const map = /* @__PURE__ */ new Map();
453
+ try {
454
+ const { stdout } = await execFileAsync("ps", ["-axo", "pid=,ppid="], { timeout: 5e3 });
455
+ for (const line of stdout.split("\n")) {
456
+ const m = line.trim().match(/^(\d+)\s+(\d+)$/);
457
+ if (m) map.set(parseInt(m[1], 10), parseInt(m[2], 10));
458
+ }
459
+ } catch {
460
+ }
461
+ return map;
462
+ }
463
+ function isDescendantOf(pid, ancestor, parents) {
464
+ let cur = pid;
465
+ for (let i = 0; i < 25 && cur !== void 0 && cur > 1; i++) {
466
+ if (cur === ancestor) return true;
467
+ cur = parents.get(cur);
468
+ }
469
+ return false;
470
+ }
471
+ var import_fs4, import_path3, import_os2, import_child_process, import_util, execFileAsync, LIVE_SESSIONS_DIR;
472
+ var init_claude_live = __esm({
473
+ "src/claude-live.ts"() {
474
+ "use strict";
475
+ import_fs4 = __toESM(require("fs"));
476
+ import_path3 = __toESM(require("path"));
477
+ import_os2 = __toESM(require("os"));
478
+ import_child_process = require("child_process");
479
+ import_util = require("util");
480
+ execFileAsync = (0, import_util.promisify)(import_child_process.execFile);
481
+ LIVE_SESSIONS_DIR = import_path3.default.join(import_os2.default.homedir(), ".claude", "sessions");
482
+ }
483
+ });
484
+
485
+ // src/tmux.ts
486
+ function tmuxCmd(args) {
487
+ return IS_WIN ? { file: "wsl.exe", args: ["tmux", ...args] } : { file: "tmux", args };
488
+ }
489
+ async function listTmuxSessions() {
490
+ const fmt = "#{session_name} #{session_windows} #{session_attached} #{session_activity}";
491
+ const { file, args } = tmuxCmd(["list-sessions", "-F", fmt]);
492
+ try {
493
+ const { stdout } = await execFileAsync2(file, args, { timeout: 6e3 });
494
+ const sessions2 = stdout.split("\n").map((line) => line.replace(/\r$/, "").trim()).filter(Boolean).map((line) => {
495
+ const [name, windows, attached, activity] = line.split(" ");
496
+ return {
497
+ name,
498
+ windows: parseInt(windows, 10) || 1,
499
+ attached: attached === "1",
500
+ activity: parseInt(activity, 10) || void 0
501
+ };
502
+ });
503
+ await enrichWithPanesAndClaude(sessions2);
504
+ return sessions2;
505
+ } catch {
506
+ return [];
507
+ }
508
+ }
509
+ async function enrichWithPanesAndClaude(sessions2) {
510
+ if (IS_WIN || sessions2.length === 0) return;
511
+ try {
512
+ const paneFmt = "#{session_name} #{pane_pid} #{pane_current_path} #{window_active}#{pane_active}";
513
+ const { file, args } = tmuxCmd(["list-panes", "-a", "-F", paneFmt]);
514
+ const { stdout } = await execFileAsync2(file, args, { timeout: 6e3 });
515
+ const panes = stdout.split("\n").map((line) => line.replace(/\r$/, "").trim()).filter(Boolean).map((line) => {
516
+ const [session, pid, path8, activeFlags] = line.split(" ");
517
+ return { session, pid: parseInt(pid, 10) || 0, path: path8 || "", active: activeFlags === "11" };
518
+ });
519
+ const byName = new Map(sessions2.map((s) => [s.name, s]));
520
+ for (const pane of panes) {
521
+ const s = byName.get(pane.session);
522
+ if (s && (!s.path || pane.active)) s.path = pane.path || s.path;
523
+ }
524
+ const live = getLiveClaudeSessions();
525
+ if (live.length === 0) return;
526
+ const parents = await getPidParents();
527
+ for (const cs of live) {
528
+ const target = parents.size > 0 ? panes.find((p) => p.pid > 0 && isDescendantOf(cs.pid, p.pid, parents)) : panes.find((p) => cs.cwd && p.path === cs.cwd);
529
+ if (!target) continue;
530
+ const s = byName.get(target.session);
531
+ if (!s) continue;
532
+ s.claudeTitle = cs.name;
533
+ s.claudeStatus = cs.status;
534
+ if (cs.cwd) s.path = cs.cwd;
535
+ }
536
+ } catch {
537
+ }
538
+ }
539
+ function shq(v) {
540
+ return `'${v.replace(/'/g, `'\\''`)}'`;
541
+ }
542
+ function buildTmuxLaunch(name, launch, shell) {
543
+ const trimmed = launch && launch.trim() ? launch.trim() : void 0;
544
+ if (IS_WIN) {
545
+ const args = ["tmux", "new-session", "-A", "-s", name];
546
+ if (trimmed) args.push(trimmed);
547
+ return { file: "wsl.exe", args };
548
+ }
549
+ let cmd2 = `tmux set-option -g aggressive-resize on 2>/dev/null; exec tmux new-session -A -s ${shq(name)}`;
550
+ if (trimmed) cmd2 += ` ${shq(trimmed)}`;
551
+ return { file: shell, args: ["-l", "-c", cmd2] };
552
+ }
553
+ var import_child_process2, import_util2, execFileAsync2, IS_WIN;
554
+ var init_tmux = __esm({
555
+ "src/tmux.ts"() {
556
+ "use strict";
557
+ import_child_process2 = require("child_process");
558
+ import_util2 = require("util");
559
+ init_claude_live();
560
+ execFileAsync2 = (0, import_util2.promisify)(import_child_process2.execFile);
561
+ IS_WIN = process.platform === "win32";
562
+ }
563
+ });
564
+
565
+ // src/shell.ts
566
+ function detectShell(preference) {
567
+ if (preference !== "auto") return preference;
568
+ if (process.platform === "win32") {
569
+ try {
570
+ (0, import_child_process3.execSync)("where pwsh", { stdio: "ignore" });
571
+ return "pwsh.exe";
572
+ } catch {
573
+ return "powershell.exe";
574
+ }
575
+ }
576
+ return process.env.SHELL || "/bin/bash";
577
+ }
578
+ var import_child_process3;
579
+ var init_shell = __esm({
580
+ "src/shell.ts"() {
581
+ "use strict";
582
+ import_child_process3 = require("child_process");
583
+ }
584
+ });
585
+
586
+ // src/pty-session.ts
587
+ function ensureInitFiles() {
588
+ const versionFile = (0, import_path4.join)(INIT_DIR, ".version");
589
+ if ((0, import_fs5.existsSync)(versionFile)) {
590
+ try {
591
+ if ((0, import_fs5.readFileSync)(versionFile, "utf-8").trim() === INIT_VERSION) return;
592
+ } catch {
593
+ }
594
+ }
595
+ (0, import_fs5.mkdirSync)(ZSH_DIR, { recursive: true });
596
+ (0, import_fs5.writeFileSync)(BASH_INIT, [
597
+ "[ -f ~/.bash_profile ] && source ~/.bash_profile",
598
+ "[ -f ~/.bashrc ] && source ~/.bashrc",
599
+ "__crc_s=$SECONDS",
600
+ "__crc_elapsed=0",
601
+ "__crc_armed=0",
602
+ '__crc_orig_pc="$PROMPT_COMMAND"',
603
+ `trap '__crc_elapsed=$((SECONDS - __crc_s)); __crc_s=$SECONDS' DEBUG`,
604
+ `PROMPT_COMMAND='[ "$__crc_armed" = 1 ] && [ \${__crc_elapsed:-0} -ge ${THRESHOLD} ] && printf "\\a"; __crc_armed=1; eval "$__crc_orig_pc"'`,
605
+ ""
606
+ ].join("\n"), "utf-8");
607
+ (0, import_fs5.writeFileSync)(ZSH_ENV, [
608
+ '[ -f "${ZDOTDIR_ORIG:-$HOME}/.zshenv" ] && source "${ZDOTDIR_ORIG:-$HOME}/.zshenv"',
609
+ ""
610
+ ].join("\n"), "utf-8");
611
+ (0, import_fs5.writeFileSync)(ZSH_RC, [
612
+ '[ -f "${ZDOTDIR_ORIG:-$HOME}/.zshrc" ] && source "${ZDOTDIR_ORIG:-$HOME}/.zshrc"',
613
+ "__crc_s=$SECONDS",
614
+ "__crc_armed=0",
615
+ `__crc_preexec() { __crc_s=$SECONDS; }`,
616
+ `__crc_precmd() { (( __crc_armed )) && (( SECONDS - __crc_s >= ${THRESHOLD} )) && printf '\\a'; __crc_armed=1; }`,
617
+ "autoload -Uz add-zsh-hook 2>/dev/null",
618
+ "if type add-zsh-hook &>/dev/null; then",
619
+ " add-zsh-hook preexec __crc_preexec",
620
+ " add-zsh-hook precmd __crc_precmd",
621
+ "fi",
622
+ ""
623
+ ].join("\n"), "utf-8");
624
+ (0, import_fs5.writeFileSync)(versionFile, INIT_VERSION, "utf-8");
625
+ }
626
+ function resolveCwd(cwd) {
627
+ const candidates = process.platform === "win32" ? [cwd, (0, import_os3.homedir)(), process.env.USERPROFILE, process.env.HOME] : [cwd, (0, import_os3.homedir)(), process.env.HOME, process.env.USERPROFILE];
628
+ for (const c of candidates) {
629
+ if (c) {
630
+ try {
631
+ if ((0, import_fs5.existsSync)(c)) return c;
632
+ } catch {
633
+ }
634
+ }
635
+ }
636
+ return (0, import_os3.homedir)();
637
+ }
638
+ var pty, import_fs5, import_path4, import_os3, THRESHOLD, INIT_DIR, BASH_INIT, ZSH_DIR, ZSH_RC, ZSH_ENV, INIT_VERSION, PtySession;
639
+ var init_pty_session = __esm({
640
+ "src/pty-session.ts"() {
641
+ "use strict";
642
+ pty = __toESM(require("node-pty"));
643
+ import_fs5 = require("fs");
644
+ import_path4 = require("path");
645
+ import_os3 = require("os");
646
+ init_src();
647
+ init_shell();
648
+ THRESHOLD = 3;
649
+ INIT_DIR = (0, import_path4.join)((0, import_os3.tmpdir)(), "crc-shell-init");
650
+ BASH_INIT = (0, import_path4.join)(INIT_DIR, "bashrc");
651
+ ZSH_DIR = (0, import_path4.join)(INIT_DIR, "zsh");
652
+ ZSH_RC = (0, import_path4.join)(ZSH_DIR, ".zshrc");
653
+ ZSH_ENV = (0, import_path4.join)(ZSH_DIR, ".zshenv");
654
+ INIT_VERSION = "2";
655
+ PtySession = class {
656
+ id;
657
+ pty;
658
+ buffer = "";
659
+ maxBufferSize = SESSION_BUFFER_SIZE;
660
+ _attached = true;
661
+ _detachedAt = 0;
662
+ constructor(id, cols, rows, shellPreference, cwd, launch) {
663
+ this.id = id;
664
+ const shell = detectShell(shellPreference);
665
+ const isBash = /bash(\.exe)?$/.test(shell);
666
+ const isZsh = /zsh(\.exe)?$/.test(shell);
667
+ const env = { ...process.env, TERM: "xterm-256color" };
668
+ let file = shell;
669
+ const args = [];
670
+ if (launch) {
671
+ file = launch.file;
672
+ args.push(...launch.args);
673
+ } else {
674
+ ensureInitFiles();
675
+ if (isBash) {
676
+ args.push("--rcfile", BASH_INIT);
677
+ } else if (isZsh) {
678
+ env.ZDOTDIR_ORIG = process.env.ZDOTDIR || process.env.HOME || "";
679
+ env.ZDOTDIR = ZSH_DIR;
680
+ }
681
+ }
682
+ const resolvedCwd = resolveCwd(cwd);
683
+ try {
684
+ this.pty = pty.spawn(file, args, {
685
+ name: "xterm-256color",
686
+ cols,
687
+ rows,
688
+ cwd: resolvedCwd,
689
+ env
690
+ });
691
+ } catch (err) {
692
+ console.error(`[pty] spawn failed (file=${file}, cwd=${resolvedCwd}):`, err);
693
+ this.pty = pty.spawn(file, args, {
694
+ name: "xterm-256color",
695
+ cols,
696
+ rows,
697
+ cwd: (0, import_os3.homedir)(),
698
+ env
699
+ });
700
+ }
701
+ }
702
+ write(data) {
703
+ this.pty.write(data);
704
+ }
705
+ resize(cols, rows) {
706
+ this.pty.resize(cols, rows);
707
+ }
708
+ kill() {
709
+ this.pty.kill();
710
+ }
711
+ onData(callback) {
712
+ this.pty.onData(callback);
713
+ }
714
+ onExit(callback) {
715
+ this.pty.onExit(callback);
716
+ }
717
+ appendToBuffer(data) {
718
+ this.buffer += data;
719
+ if (this.buffer.length > this.maxBufferSize) {
720
+ let sliced = this.buffer.slice(this.buffer.length - this.maxBufferSize);
721
+ const nl = sliced.indexOf("\n");
722
+ const esc = sliced.indexOf("\x1B");
723
+ if (nl !== -1 && (esc === -1 || nl < esc)) {
724
+ sliced = sliced.slice(nl + 1);
725
+ } else if (esc > 0) {
726
+ sliced = sliced.slice(esc);
727
+ }
728
+ this.buffer = sliced;
729
+ }
730
+ }
731
+ getAndClearBuffer() {
732
+ const data = this.buffer;
733
+ this.buffer = "";
734
+ return data;
735
+ }
736
+ setAttached(v) {
737
+ this._attached = v;
738
+ this._detachedAt = v ? 0 : Date.now();
739
+ }
740
+ isAttached() {
741
+ return this._attached;
742
+ }
743
+ // Timestamp (ms) when this session was last detached, or 0 if attached.
744
+ getDetachedAt() {
745
+ return this._detachedAt;
746
+ }
747
+ };
748
+ }
749
+ });
750
+
751
+ // src/terminal-manager.ts
752
+ function createTerminalSession(sessionId, cols, rows, shellPreference, cwd, onData, onExit, launch) {
753
+ const session = new PtySession(sessionId, cols, rows, shellPreference, cwd, launch);
754
+ session.onData((data) => {
755
+ session.appendToBuffer(data);
756
+ if (session.isAttached()) {
757
+ onData(sessionId, data);
758
+ }
759
+ });
760
+ session.onExit((exit) => {
761
+ sessions.delete(sessionId);
762
+ onExit(sessionId, exit.exitCode);
763
+ logger.info({ sessionId, exitCode: exit.exitCode }, "PTY exited");
764
+ });
765
+ sessions.set(sessionId, session);
766
+ logger.info({ sessionId, cols, rows }, "PTY session created");
767
+ }
768
+ function writeToSession(sessionId, data) {
769
+ sessions.get(sessionId)?.write(data);
770
+ }
771
+ function resizeSession(sessionId, cols, rows) {
772
+ sessions.get(sessionId)?.resize(cols, rows);
773
+ }
774
+ function detachSession(sessionId) {
775
+ const session = sessions.get(sessionId);
776
+ if (session) {
777
+ session.setAttached(false);
778
+ logger.info({ sessionId }, "PTY session detached");
779
+ }
780
+ }
781
+ function attachSession(sessionId, cols, rows) {
782
+ const session = sessions.get(sessionId);
783
+ if (!session) return "";
784
+ session.setAttached(true);
785
+ session.resize(cols, rows);
786
+ const buffered = session.getAndClearBuffer();
787
+ logger.info({ sessionId, bufferSize: buffered.length }, "PTY session reattached");
788
+ return buffered;
789
+ }
790
+ function closeSession(sessionId) {
791
+ const session = sessions.get(sessionId);
792
+ if (session) {
793
+ session.kill();
794
+ sessions.delete(sessionId);
795
+ logger.info({ sessionId }, "PTY session closed");
796
+ }
797
+ }
798
+ function closeAllSessions() {
799
+ for (const [id, session] of sessions) {
800
+ session.kill();
801
+ logger.info({ sessionId: id }, "PTY session force-closed");
802
+ }
803
+ sessions.clear();
804
+ }
805
+ function detachAllSessions() {
806
+ for (const [id, session] of sessions) {
807
+ session.setAttached(false);
808
+ logger.info({ sessionId: id }, "PTY session detached (disconnect)");
809
+ }
810
+ }
811
+ function getAliveSessionIds() {
812
+ return Array.from(sessions.keys());
813
+ }
814
+ function getActiveSessionCount() {
815
+ return sessions.size;
816
+ }
817
+ function reapDetachedSessions(maxIdleMs) {
818
+ const now = Date.now();
819
+ const reaped = [];
820
+ for (const [id, session] of sessions) {
821
+ if (session.isAttached()) continue;
822
+ const detachedAt = session.getDetachedAt();
823
+ if (detachedAt > 0 && now - detachedAt > maxIdleMs) {
824
+ session.kill();
825
+ sessions.delete(id);
826
+ reaped.push(id);
827
+ logger.info({ sessionId: id, idleMs: now - detachedAt }, "PTY session reaped (idle)");
828
+ }
829
+ }
830
+ return reaped;
831
+ }
832
+ var sessions;
833
+ var init_terminal_manager = __esm({
834
+ "src/terminal-manager.ts"() {
835
+ "use strict";
836
+ init_pty_session();
837
+ init_logger();
838
+ sessions = /* @__PURE__ */ new Map();
839
+ }
840
+ });
841
+
842
+ // src/heartbeat.ts
843
+ function getCpuUsage() {
844
+ const cpus = import_os4.default.cpus();
845
+ let idle = 0;
846
+ let total = 0;
847
+ for (const cpu of cpus) {
848
+ for (const type of Object.values(cpu.times)) {
849
+ total += type;
850
+ }
851
+ idle += cpu.times.idle;
852
+ }
853
+ const prev = prevCpu;
854
+ prevCpu = { idle, total };
855
+ if (!prev) return 0;
856
+ const idleDelta = idle - prev.idle;
857
+ const totalDelta = total - prev.total;
858
+ if (totalDelta <= 0) return 0;
859
+ return Math.round((1 - idleDelta / totalDelta) * 100);
860
+ }
861
+ function getRootPaths() {
862
+ if (process.platform === "win32") {
863
+ try {
864
+ const output = (0, import_child_process4.execSync)("wmic logicaldisk get name", { encoding: "utf-8" });
865
+ return output.split("\n").map((l) => l.trim()).filter((l) => /^[A-Z]:$/.test(l)).map((l) => l + "\\");
866
+ } catch {
867
+ return ["C:\\"];
868
+ }
869
+ }
870
+ return ["/"];
871
+ }
872
+ function buildHeartbeat(homeDir) {
873
+ const totalMem = import_os4.default.totalmem();
874
+ const freeMem = import_os4.default.freemem();
875
+ return {
876
+ hostname: import_os4.default.hostname(),
877
+ platform: process.platform,
878
+ arch: import_os4.default.arch(),
879
+ cpuUsage: getCpuUsage(),
880
+ memoryUsage: Math.round((totalMem - freeMem) / totalMem * 100),
881
+ uptime: Math.round(import_os4.default.uptime()),
882
+ activeSessions: getActiveSessionCount(),
883
+ pathSeparator: import_path5.default.sep,
884
+ homeDirectory: homeDir || import_os4.default.homedir(),
885
+ rootPaths: getRootPaths(),
886
+ capabilities: { terminal: true, fileTransfer: false }
887
+ };
888
+ }
889
+ var import_os4, import_path5, import_child_process4, prevCpu;
890
+ var init_heartbeat = __esm({
891
+ "src/heartbeat.ts"() {
892
+ "use strict";
893
+ import_os4 = __toESM(require("os"));
894
+ import_path5 = __toESM(require("path"));
895
+ import_child_process4 = require("child_process");
896
+ init_terminal_manager();
897
+ prevCpu = null;
898
+ }
899
+ });
900
+
901
+ // src/file-explorer.ts
902
+ function isInSecretDir(resolved) {
903
+ const rel = import_path6.default.relative(SECRET_DIR, resolved);
904
+ return rel === "" || !rel.startsWith(".." + import_path6.default.sep) && rel !== ".." && !import_path6.default.isAbsolute(rel);
905
+ }
906
+ function listDirectory(dirPath) {
907
+ try {
908
+ const resolved = import_path6.default.resolve(dirPath);
909
+ if (isInSecretDir(resolved)) {
910
+ return { entries: [], error: "Access denied" };
911
+ }
912
+ if (!import_fs6.default.existsSync(resolved)) {
913
+ return { entries: [], error: "Path does not exist" };
914
+ }
915
+ const stat = import_fs6.default.statSync(resolved);
916
+ if (!stat.isDirectory()) {
917
+ return { entries: [], error: "Not a directory" };
918
+ }
919
+ const dirents = import_fs6.default.readdirSync(resolved, { withFileTypes: true });
920
+ const entries = [];
921
+ for (const dirent of dirents) {
922
+ try {
923
+ const fullPath = import_path6.default.join(resolved, dirent.name);
924
+ const s = import_fs6.default.statSync(fullPath);
925
+ entries.push({
926
+ name: dirent.name,
927
+ isDirectory: dirent.isDirectory(),
928
+ size: dirent.isDirectory() ? 0 : s.size,
929
+ modified: s.mtimeMs
930
+ });
931
+ } catch {
932
+ }
933
+ }
934
+ entries.sort((a, b) => {
935
+ if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1;
936
+ return a.name.localeCompare(b.name);
937
+ });
938
+ return { entries };
939
+ } catch (err) {
940
+ logger.error({ path: dirPath, error: err.message }, "Failed to list directory");
941
+ return { entries: [], error: err.message };
942
+ }
943
+ }
944
+ async function downloadFile(filePath, serverUrl, secret, agentId) {
945
+ try {
946
+ const resolved = import_path6.default.resolve(filePath);
947
+ if (isInSecretDir(resolved)) {
948
+ return { error: "Access denied" };
949
+ }
950
+ if (!import_fs6.default.existsSync(resolved)) {
951
+ return { error: "File does not exist" };
952
+ }
953
+ const stat = import_fs6.default.statSync(resolved);
954
+ if (stat.isDirectory()) {
955
+ return { error: "Cannot download a directory" };
956
+ }
957
+ if (stat.size > FILE_MAX_SIZE) {
958
+ return { error: `File too large (max ${FILE_MAX_SIZE} bytes)` };
959
+ }
960
+ const fileName = import_path6.default.basename(resolved);
961
+ const fileStream = import_fs6.default.createReadStream(resolved);
962
+ const baseUrl = serverUrl.replace("wss://", "https://").replace("ws://", "http://");
963
+ const url = `${baseUrl}/api/files/receive`;
964
+ const res = await fetch(url, {
965
+ method: "POST",
966
+ headers: {
967
+ "X-File-Name": fileName,
968
+ "X-Agent-Id": agentId,
969
+ "X-Agent-Secret": secret,
970
+ "Content-Length": String(stat.size)
971
+ },
972
+ body: fileStream,
973
+ duplex: "half"
974
+ });
975
+ if (!res.ok) {
976
+ return { error: `Upload failed: ${res.status}` };
977
+ }
978
+ const result = await res.json();
979
+ return {
980
+ fileId: result.fileId,
981
+ fileName: result.fileName,
982
+ downloadUrl: result.downloadUrl,
983
+ size: stat.size
984
+ };
985
+ } catch (err) {
986
+ logger.error({ path: filePath, error: err.message }, "Failed to download file");
987
+ return { error: err.message };
988
+ }
989
+ }
990
+ var import_fs6, import_path6, import_os5, SECRET_DIR;
991
+ var init_file_explorer = __esm({
992
+ "src/file-explorer.ts"() {
993
+ "use strict";
994
+ import_fs6 = __toESM(require("fs"));
995
+ import_path6 = __toESM(require("path"));
996
+ import_os5 = __toESM(require("os"));
997
+ init_logger();
998
+ init_src();
999
+ SECRET_DIR = import_path6.default.join(import_os5.default.homedir(), ".crc-agent");
1000
+ }
1001
+ });
1002
+
1003
+ // src/vpn-manager.ts
1004
+ function shq2(value) {
1005
+ return value.replace(/'/g, "'\\''");
1006
+ }
1007
+ function psq(value) {
1008
+ return value.replace(/'/g, "''");
1009
+ }
1010
+ async function runCmd(cmd2, shell) {
1011
+ try {
1012
+ return await execAsync(cmd2, {
1013
+ timeout: 2e4,
1014
+ shell: shell || (isWindows ? "powershell.exe" : "/bin/sh")
1015
+ });
1016
+ } catch (err) {
1017
+ return { stdout: err.stdout || "", stderr: err.stderr || err.message };
1018
+ }
1019
+ }
1020
+ function getConfigPath(profile) {
1021
+ const file = profile.configFile || `${profile.id}.conf`;
1022
+ if (import_path7.default.isAbsolute(file)) return file;
1023
+ return import_path7.default.join(VPN_DIR, file);
1024
+ }
1025
+ async function getScutilStatus(serviceName) {
1026
+ const { stdout } = await runCmd(`scutil --nc status '${shq2(serviceName)}'`);
1027
+ const firstLine = stdout.trim().split("\n")[0];
1028
+ if (firstLine === "Connected") return "connected";
1029
+ if (firstLine === "Connecting") return "connecting";
1030
+ if (firstLine === "Disconnecting") return "disconnecting";
1031
+ return "disconnected";
1032
+ }
1033
+ async function scutilStart(serviceName) {
1034
+ const { stderr } = await runCmd(`scutil --nc start '${shq2(serviceName)}'`);
1035
+ return stderr || null;
1036
+ }
1037
+ async function scutilStop(serviceName) {
1038
+ const { stderr } = await runCmd(`scutil --nc stop '${shq2(serviceName)}'`);
1039
+ return stderr || null;
1040
+ }
1041
+ async function runOsascript(script) {
1042
+ return runCmd(
1043
+ `osascript -e 'with timeout of 15 seconds' -e ${JSON.stringify(script)} -e 'end timeout'`
1044
+ );
1045
+ }
1046
+ function asq(value) {
1047
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1048
+ }
1049
+ async function getTunnelblickStatus(configName) {
1050
+ const { stdout } = await runOsascript(
1051
+ `tell application "Tunnelblick" to get state of first configuration where name = "${asq(configName)}"`
1052
+ );
1053
+ const s = stdout.trim();
1054
+ if (s === "CONNECTED") return "connected";
1055
+ if (s === "CONNECTING" || s === "RECONNECTING") return "connecting";
1056
+ if (s === "DISCONNECTING") return "disconnecting";
1057
+ return "disconnected";
1058
+ }
1059
+ async function tunnelblickConnect(configName) {
1060
+ const { stderr } = await runOsascript(
1061
+ `tell application "Tunnelblick" to connect "${asq(configName)}"`
1062
+ );
1063
+ return stderr || null;
1064
+ }
1065
+ async function tunnelblickDisconnect(configName) {
1066
+ const { stderr } = await runOsascript(
1067
+ `tell application "Tunnelblick" to disconnect "${asq(configName)}"`
1068
+ );
1069
+ return stderr || null;
1070
+ }
1071
+ async function getWireGuardStatus(profile) {
1072
+ if (isWindows) {
1073
+ const tunnelName2 = profile.tunnelName || profile.id;
1074
+ const { stdout: stdout2 } = await runCmd(
1075
+ `Get-Service -Name 'WireGuardTunnel$${psq(tunnelName2)}' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Status`
1076
+ );
1077
+ const s = stdout2.trim();
1078
+ if (s === "Running") return "connected";
1079
+ if (s === "StartPending") return "connecting";
1080
+ if (s === "StopPending") return "disconnecting";
1081
+ return "disconnected";
1082
+ }
1083
+ if (profile.serviceName) {
1084
+ return getScutilStatus(profile.serviceName);
1085
+ }
1086
+ const tunnelName = profile.tunnelName || profile.id;
1087
+ const { stdout } = await runCmd(`wg show '${shq2(tunnelName)}' 2>/dev/null`);
1088
+ return stdout.trim() ? "connected" : "disconnected";
1089
+ }
1090
+ async function getOpenVpnStatus(profile) {
1091
+ if (isWindows) {
1092
+ const { stdout: stdout2 } = await runCmd(
1093
+ `Get-Service -Name 'ovpnconnector' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Status`
1094
+ );
1095
+ if (stdout2.trim() === "Running") {
1096
+ const { stdout: netOut } = await runCmd(
1097
+ `Get-NetAdapter | Where-Object { $_.InterfaceDescription -like '*OpenVPN*' -and $_.Status -eq 'Up' } | Measure-Object | Select-Object -ExpandProperty Count`
1098
+ );
1099
+ return parseInt(netOut.trim(), 10) > 0 ? "connected" : "connecting";
1100
+ }
1101
+ return "disconnected";
1102
+ }
1103
+ if (profile.tunnelblickName) {
1104
+ return getTunnelblickStatus(profile.tunnelblickName);
1105
+ }
1106
+ if (profile.serviceName) {
1107
+ return getScutilStatus(profile.serviceName);
1108
+ }
1109
+ const { stdout } = await runCmd('pgrep -f "openvpn.*\\.ovpn" | head -1');
1110
+ return stdout.trim() ? "connected" : "disconnected";
1111
+ }
1112
+ async function getAzureStatus(profile) {
1113
+ if (isWindows) {
1114
+ return getOpenVpnStatus(profile);
1115
+ }
1116
+ if (profile.serviceName) {
1117
+ return getScutilStatus(profile.serviceName);
1118
+ }
1119
+ return "disconnected";
1120
+ }
1121
+ async function getStatus(profile) {
1122
+ try {
1123
+ if (profile.type === "wireguard") {
1124
+ return await getWireGuardStatus(profile);
1125
+ }
1126
+ if (profile.type === "azure") {
1127
+ return await getAzureStatus(profile);
1128
+ }
1129
+ return await getOpenVpnStatus(profile);
1130
+ } catch (err) {
1131
+ logger.error({ profile: profile.id, error: err.message }, "Status check failed");
1132
+ return "disconnected";
1133
+ }
1134
+ }
1135
+ async function connectWireGuard(profile) {
1136
+ const tunnelName = profile.tunnelName || profile.id;
1137
+ if (isWindows) {
1138
+ const confPath2 = getConfigPath(profile);
1139
+ if (!import_fs7.default.existsSync(confPath2)) {
1140
+ return `Config file not found: ${confPath2}`;
1141
+ }
1142
+ const script = `
1143
+ & 'C:\\Program Files\\WireGuard\\wireguard.exe' /installtunnelservice '${confPath2.replace(/'/g, "''")}';
1144
+ Start-Sleep -Seconds 2;
1145
+ Set-Service -Name 'WireGuardTunnel$${psq(tunnelName)}' -StartupType Manual -ErrorAction SilentlyContinue
1146
+ `.trim();
1147
+ await runCmd(
1148
+ `Start-Process powershell -ArgumentList '-NoProfile','-Command','${script.replace(/'/g, "''")}' -Verb RunAs -Wait -ErrorAction Stop 2>&1`
1149
+ );
1150
+ await new Promise((r) => setTimeout(r, 2e3));
1151
+ const status = await getWireGuardStatus(profile);
1152
+ if (status !== "connected") return "Tunnel installed but not connected \u2014 check WireGuard GUI";
1153
+ return null;
1154
+ }
1155
+ if (profile.serviceName) {
1156
+ return scutilStart(profile.serviceName);
1157
+ }
1158
+ const confPath = getConfigPath(profile);
1159
+ const { stderr } = await runCmd(`sudo wg-quick up '${shq2(confPath)}'`);
1160
+ return stderr || null;
1161
+ }
1162
+ async function connectOpenVpn(profile) {
1163
+ if (isWindows) {
1164
+ const confPath2 = getConfigPath(profile);
1165
+ if (!import_fs7.default.existsSync(confPath2)) {
1166
+ return `Config file not found: ${confPath2}`;
1167
+ }
1168
+ const connector = "C:\\Program Files\\OpenVPN Connect\\ovpnconnector.exe";
1169
+ await runCmd(`Stop-Process -Name 'OpenVPNConnect' -Force -ErrorAction SilentlyContinue`);
1170
+ await runCmd(`& '${connector}' install 2>&1`);
1171
+ await runCmd(`& '${connector}' stop 2>&1`);
1172
+ await new Promise((r) => setTimeout(r, 1e3));
1173
+ const { stdout: setOut, stderr: setErr } = await runCmd(`& '${connector}' set-config profile '${psq(confPath2)}' 2>&1`);
1174
+ const setOutput = setOut + setErr;
1175
+ if (setOutput.toLowerCase().includes("failed")) {
1176
+ return `Failed to set profile: ${setOutput.trim()}`;
1177
+ }
1178
+ const { stdout: startOut, stderr: startErr } = await runCmd(`& '${connector}' start 2>&1`);
1179
+ const startOutput = startOut + startErr;
1180
+ if (startOutput.toLowerCase().includes("error") || startOutput.toLowerCase().includes("aborting")) {
1181
+ return startOutput.trim();
1182
+ }
1183
+ return null;
1184
+ }
1185
+ if (profile.tunnelblickName) {
1186
+ return tunnelblickConnect(profile.tunnelblickName);
1187
+ }
1188
+ if (profile.serviceName) {
1189
+ return scutilStart(profile.serviceName);
1190
+ }
1191
+ const confPath = getConfigPath(profile);
1192
+ if (!import_fs7.default.existsSync(confPath)) {
1193
+ return `Config file not found: ${confPath}`;
1194
+ }
1195
+ const { stderr } = await runCmd(`sudo openvpn --config '${shq2(confPath)}' --daemon`);
1196
+ return stderr || null;
1197
+ }
1198
+ async function connectAzure(profile) {
1199
+ if (isWindows) {
1200
+ const confPath = getConfigPath(profile);
1201
+ if (!import_fs7.default.existsSync(confPath)) {
1202
+ return `Config file not found: ${confPath}`;
1203
+ }
1204
+ await runCmd(`& 'AzureVpn.exe' -i '${psq(confPath)}' 2>&1`);
1205
+ await new Promise((r) => setTimeout(r, 2e3));
1206
+ await runCmd(`Start-Process 'shell:AppsFolder\\Microsoft.AzureVpn_8wekyb3d8bbwe!App'`);
1207
+ return "Azure VPN app opened \u2014 requires Azure AD sign-in to connect";
1208
+ }
1209
+ if (profile.serviceName) {
1210
+ const err = await scutilStart(profile.serviceName);
1211
+ if (err) return err;
1212
+ await runCmd(`open -a 'Azure VPN Client'`);
1213
+ return null;
1214
+ }
1215
+ return "Azure VPN: set serviceName in config (from scutil --nc list)";
1216
+ }
1217
+ async function disconnectAzure(profile) {
1218
+ if (isWindows) {
1219
+ await runCmd(`Start-Process 'shell:AppsFolder\\Microsoft.AzureVpn_8wekyb3d8bbwe!App'`);
1220
+ return "Azure VPN app opened \u2014 disconnect from the app";
1221
+ }
1222
+ if (profile.serviceName) {
1223
+ return scutilStop(profile.serviceName);
1224
+ }
1225
+ return "Azure VPN: set serviceName in config (from scutil --nc list)";
1226
+ }
1227
+ async function disconnectWireGuard(profile) {
1228
+ const tunnelName = profile.tunnelName || profile.id;
1229
+ if (isWindows) {
1230
+ const script = `& 'C:\\Program Files\\WireGuard\\wireguard.exe' /uninstalltunnelservice '${psq(tunnelName)}'`;
1231
+ await runCmd(
1232
+ `Start-Process powershell -ArgumentList '-NoProfile','-Command','${script.replace(/'/g, "''")}' -Verb RunAs -Wait -ErrorAction Stop 2>&1`
1233
+ );
1234
+ await new Promise((r) => setTimeout(r, 2e3));
1235
+ const status = await getWireGuardStatus(profile);
1236
+ if (status === "connected") {
1237
+ return "Failed to disconnect \u2014 try disconnecting from WireGuard GUI";
1238
+ }
1239
+ return null;
1240
+ }
1241
+ if (profile.serviceName) {
1242
+ return scutilStop(profile.serviceName);
1243
+ }
1244
+ const confPath = getConfigPath(profile);
1245
+ const { stderr } = await runCmd(`sudo wg-quick down '${shq2(confPath)}'`);
1246
+ return stderr || null;
1247
+ }
1248
+ async function disconnectOpenVpn(profile) {
1249
+ if (isWindows) {
1250
+ const connector = "C:\\Program Files\\OpenVPN Connect\\ovpnconnector.exe";
1251
+ const { stderr, stdout } = await runCmd(`& '${connector}' stop 2>&1`);
1252
+ const output = stdout + stderr;
1253
+ if (output.toLowerCase().includes("error")) return output.trim();
1254
+ return null;
1255
+ }
1256
+ if (profile.tunnelblickName) {
1257
+ return tunnelblickDisconnect(profile.tunnelblickName);
1258
+ }
1259
+ if (profile.serviceName) {
1260
+ return scutilStop(profile.serviceName);
1261
+ }
1262
+ await runCmd('sudo pkill -f "openvpn.*\\.ovpn"');
1263
+ return null;
1264
+ }
1265
+ async function getProfiles(configs) {
1266
+ const profiles = [];
1267
+ for (const cfg of configs) {
1268
+ const status = await getStatus(cfg);
1269
+ profiles.push({ id: cfg.id, name: cfg.name, type: cfg.type, status });
1270
+ }
1271
+ return profiles;
1272
+ }
1273
+ async function connectVpn(configs, profileId) {
1274
+ const cfg = configs.find((c) => c.id === profileId);
1275
+ if (!cfg) return getProfiles(configs);
1276
+ logger.info({ profileId, type: cfg.type }, "Connecting VPN");
1277
+ let error = null;
1278
+ if (cfg.type === "wireguard") {
1279
+ error = await connectWireGuard(cfg);
1280
+ } else if (cfg.type === "azure") {
1281
+ error = await connectAzure(cfg);
1282
+ } else {
1283
+ error = await connectOpenVpn(cfg);
1284
+ }
1285
+ if (error) logger.error({ profileId, error }, "VPN connect failed");
1286
+ await new Promise((r) => setTimeout(r, 3e3));
1287
+ const profiles = await getProfiles(configs);
1288
+ if (error) {
1289
+ const p = profiles.find((p2) => p2.id === profileId);
1290
+ if (p) {
1291
+ p.status = "error";
1292
+ p.error = error.slice(0, 200);
1293
+ }
1294
+ }
1295
+ return profiles;
1296
+ }
1297
+ async function disconnectVpn(configs, profileId) {
1298
+ const cfg = configs.find((c) => c.id === profileId);
1299
+ if (!cfg) return getProfiles(configs);
1300
+ logger.info({ profileId, type: cfg.type }, "Disconnecting VPN");
1301
+ let error = null;
1302
+ if (cfg.type === "wireguard") {
1303
+ error = await disconnectWireGuard(cfg);
1304
+ } else if (cfg.type === "azure") {
1305
+ error = await disconnectAzure(cfg);
1306
+ } else {
1307
+ error = await disconnectOpenVpn(cfg);
1308
+ }
1309
+ if (error) logger.error({ profileId, error }, "VPN disconnect failed");
1310
+ await new Promise((r) => setTimeout(r, 2e3));
1311
+ const profiles = await getProfiles(configs);
1312
+ if (error) {
1313
+ const p = profiles.find((p2) => p2.id === profileId);
1314
+ if (p) {
1315
+ p.status = "error";
1316
+ p.error = error.slice(0, 200);
1317
+ }
1318
+ }
1319
+ return profiles;
1320
+ }
1321
+ var import_child_process5, import_util3, import_fs7, import_path7, import_os6, execAsync, isWindows, VPN_DIR;
1322
+ var init_vpn_manager = __esm({
1323
+ "src/vpn-manager.ts"() {
1324
+ "use strict";
1325
+ import_child_process5 = require("child_process");
1326
+ import_util3 = require("util");
1327
+ import_fs7 = __toESM(require("fs"));
1328
+ import_path7 = __toESM(require("path"));
1329
+ import_os6 = __toESM(require("os"));
1330
+ init_logger();
1331
+ execAsync = (0, import_util3.promisify)(import_child_process5.exec);
1332
+ isWindows = process.platform === "win32";
1333
+ VPN_DIR = import_path7.default.join(import_os6.default.homedir(), ".crc-agent", "vpn");
1334
+ }
1335
+ });
1336
+
1337
+ // src/claude-path.ts
1338
+ function encodeProjectPath(projectPath) {
1339
+ return projectPath.replace(/[^A-Za-z0-9]/g, "-");
1340
+ }
1341
+ function findProjectDirs(allDirs, filterProjectPath) {
1342
+ const encoded = encodeProjectPath(filterProjectPath);
1343
+ if (allDirs.includes(encoded)) {
1344
+ return [encoded];
1345
+ }
1346
+ const lowerEncoded = encoded.toLowerCase();
1347
+ const caseMatch = allDirs.filter((d) => d.toLowerCase() === lowerEncoded);
1348
+ if (caseMatch.length > 0) {
1349
+ return caseMatch;
1350
+ }
1351
+ const isAnchoredMatch = (a, b) => {
1352
+ if (a === b) return true;
1353
+ if (a.startsWith(b) && a[b.length] === "-") return true;
1354
+ if (b.startsWith(a) && b[a.length] === "-") return true;
1355
+ return false;
1356
+ };
1357
+ return allDirs.filter((d) => isAnchoredMatch(d.toLowerCase(), lowerEncoded));
1358
+ }
1359
+ var init_claude_path = __esm({
1360
+ "src/claude-path.ts"() {
1361
+ "use strict";
1362
+ }
1363
+ });
1364
+
1365
+ // src/claude-sessions.ts
1366
+ async function parseSessionFile(filePath) {
1367
+ try {
1368
+ const stream = import_fs8.default.createReadStream(filePath, { encoding: "utf-8" });
1369
+ const rl = import_readline2.default.createInterface({ input: stream, crlfDelay: Infinity });
1370
+ let firstMessage = "";
1371
+ let lastTimestamp = "";
1372
+ let messageCount = 0;
1373
+ let model;
1374
+ let slug;
1375
+ let gitBranch;
1376
+ let cwd;
1377
+ for await (const line of rl) {
1378
+ if (!line.trim()) continue;
1379
+ try {
1380
+ const obj = JSON.parse(line);
1381
+ if (obj.timestamp) lastTimestamp = obj.timestamp;
1382
+ if (obj.cwd && !cwd) cwd = obj.cwd;
1383
+ if (obj.type === "user" && !firstMessage) {
1384
+ const content = obj.message?.content;
1385
+ if (typeof content === "string") {
1386
+ firstMessage = content.slice(0, 200);
1387
+ } else if (Array.isArray(content)) {
1388
+ for (const block of content) {
1389
+ if (block?.type === "text" && block.text) {
1390
+ firstMessage = block.text.slice(0, 200);
1391
+ break;
1392
+ }
1393
+ }
1394
+ }
1395
+ messageCount++;
1396
+ } else if (obj.type === "user" || obj.type === "assistant") {
1397
+ messageCount++;
1398
+ if (obj.type === "assistant") {
1399
+ if (obj.message?.model) model = obj.message.model;
1400
+ if (obj.slug) slug = obj.slug;
1401
+ }
1402
+ if (obj.gitBranch) gitBranch = obj.gitBranch;
1403
+ }
1404
+ } catch {
1405
+ }
1406
+ }
1407
+ if (!firstMessage && messageCount === 0) return null;
1408
+ return { firstMessage: firstMessage || "(no message)", lastTimestamp, messageCount, model, slug, gitBranch, cwd };
1409
+ } catch (err) {
1410
+ logger.error({ filePath, error: err.message }, "Failed to parse session file");
1411
+ return null;
1412
+ }
1413
+ }
1414
+ async function listClaudeSessions(filterProjectPath) {
1415
+ const sessions2 = [];
1416
+ if (!import_fs8.default.existsSync(CLAUDE_PROJECTS_DIR)) return sessions2;
1417
+ const isDirSafe = (d) => {
1418
+ try {
1419
+ return import_fs8.default.statSync(import_path8.default.join(CLAUDE_PROJECTS_DIR, d)).isDirectory();
1420
+ } catch {
1421
+ return false;
1422
+ }
1423
+ };
1424
+ let allEntries;
1425
+ try {
1426
+ allEntries = import_fs8.default.readdirSync(CLAUDE_PROJECTS_DIR);
1427
+ } catch {
1428
+ return sessions2;
1429
+ }
1430
+ let projectDirs;
1431
+ if (filterProjectPath) {
1432
+ const allDirs = allEntries.filter(isDirSafe);
1433
+ projectDirs = findProjectDirs(allDirs, filterProjectPath);
1434
+ if (projectDirs.length === 0) return sessions2;
1435
+ } else {
1436
+ projectDirs = allEntries.filter((d) => isDirSafe(d) && !d.startsWith("-private-"));
1437
+ }
1438
+ for (const dirName of projectDirs) {
1439
+ const dirPath = import_path8.default.join(CLAUDE_PROJECTS_DIR, dirName);
1440
+ let files;
1441
+ try {
1442
+ files = import_fs8.default.readdirSync(dirPath).filter((f) => f.endsWith(".jsonl"));
1443
+ } catch {
1444
+ continue;
1445
+ }
1446
+ for (const file of files) {
1447
+ const sessionId = file.replace(".jsonl", "");
1448
+ const parsed = await parseSessionFile(import_path8.default.join(dirPath, file));
1449
+ if (!parsed) continue;
1450
+ const projectPath = parsed.cwd || dirName.replace(/^-/, "/").replace(/-/g, "/");
1451
+ sessions2.push({
1452
+ sessionId,
1453
+ projectPath,
1454
+ firstMessage: parsed.firstMessage,
1455
+ lastTimestamp: parsed.lastTimestamp,
1456
+ messageCount: parsed.messageCount,
1457
+ model: parsed.model,
1458
+ slug: parsed.slug,
1459
+ gitBranch: parsed.gitBranch
1460
+ });
1461
+ }
1462
+ }
1463
+ sessions2.sort((a, b) => b.lastTimestamp > a.lastTimestamp ? 1 : -1);
1464
+ return sessions2;
1465
+ }
1466
+ var import_fs8, import_path8, import_os7, import_readline2, CLAUDE_PROJECTS_DIR;
1467
+ var init_claude_sessions = __esm({
1468
+ "src/claude-sessions.ts"() {
1469
+ "use strict";
1470
+ import_fs8 = __toESM(require("fs"));
1471
+ import_path8 = __toESM(require("path"));
1472
+ import_os7 = __toESM(require("os"));
1473
+ import_readline2 = __toESM(require("readline"));
1474
+ init_logger();
1475
+ init_claude_path();
1476
+ CLAUDE_PROJECTS_DIR = import_path8.default.join(import_os7.default.homedir(), ".claude", "projects");
1477
+ }
1478
+ });
1479
+
1480
+ // src/claude-conversation.ts
1481
+ function findProjectDir(projectPath) {
1482
+ const encoded = encodeProjectPath(projectPath);
1483
+ const dirPath = import_path9.default.join(CLAUDE_PROJECTS_DIR2, encoded);
1484
+ if (import_fs9.default.existsSync(dirPath)) return dirPath;
1485
+ if (!import_fs9.default.existsSync(CLAUDE_PROJECTS_DIR2)) return null;
1486
+ let allDirs;
1487
+ try {
1488
+ allDirs = import_fs9.default.readdirSync(CLAUDE_PROJECTS_DIR2).filter((d) => {
1489
+ try {
1490
+ return import_fs9.default.statSync(import_path9.default.join(CLAUDE_PROJECTS_DIR2, d)).isDirectory();
1491
+ } catch {
1492
+ return false;
1493
+ }
1494
+ });
1495
+ } catch {
1496
+ return null;
1497
+ }
1498
+ const matches = findProjectDirs(allDirs, projectPath);
1499
+ if (matches.length > 0) {
1500
+ return import_path9.default.join(CLAUDE_PROJECTS_DIR2, matches[0]);
1501
+ }
1502
+ return null;
1503
+ }
1504
+ function findSessionFileById(sessionId) {
1505
+ if (!/^[A-Za-z0-9._-]+$/.test(sessionId)) return null;
1506
+ if (!import_fs9.default.existsSync(CLAUDE_PROJECTS_DIR2)) return null;
1507
+ let dirs;
1508
+ try {
1509
+ dirs = import_fs9.default.readdirSync(CLAUDE_PROJECTS_DIR2);
1510
+ } catch {
1511
+ return null;
1512
+ }
1513
+ for (const dir of dirs) {
1514
+ const candidate = import_path9.default.join(CLAUDE_PROJECTS_DIR2, dir, `${sessionId}.jsonl`);
1515
+ try {
1516
+ if (import_fs9.default.statSync(candidate).isFile()) return candidate;
1517
+ } catch {
1518
+ }
1519
+ }
1520
+ return null;
1521
+ }
1522
+ function findLatestSessionFile(projectPath) {
1523
+ const dirPath = findProjectDir(projectPath);
1524
+ if (!dirPath) return null;
1525
+ let entries;
1526
+ try {
1527
+ entries = import_fs9.default.readdirSync(dirPath);
1528
+ } catch {
1529
+ return null;
1530
+ }
1531
+ const files = entries.filter((f) => f.endsWith(".jsonl") && !f.includes("/")).map((f) => {
1532
+ try {
1533
+ return { name: f, mtime: import_fs9.default.statSync(import_path9.default.join(dirPath, f)).mtimeMs };
1534
+ } catch {
1535
+ return null;
1536
+ }
1537
+ }).filter((f) => f !== null).sort((a, b) => b.mtime - a.mtime);
1538
+ if (files.length === 0) return null;
1539
+ return {
1540
+ filePath: import_path9.default.join(dirPath, files[0].name),
1541
+ sessionId: files[0].name.replace(".jsonl", "")
1542
+ };
1543
+ }
1544
+ async function readConversation(projectPath, afterLine = 0, specificSessionId) {
1545
+ let filePath;
1546
+ let sessionId;
1547
+ if (specificSessionId) {
1548
+ sessionId = specificSessionId;
1549
+ const dirPath = findProjectDir(projectPath);
1550
+ const direct = dirPath ? import_path9.default.join(dirPath, `${specificSessionId}.jsonl`) : null;
1551
+ if (direct && import_fs9.default.existsSync(direct)) {
1552
+ filePath = direct;
1553
+ } else {
1554
+ const byId = findSessionFileById(specificSessionId);
1555
+ if (!byId) return null;
1556
+ filePath = byId;
1557
+ }
1558
+ } else {
1559
+ const found = findLatestSessionFile(projectPath);
1560
+ if (!found) return null;
1561
+ filePath = found.filePath;
1562
+ sessionId = found.sessionId;
1563
+ }
1564
+ try {
1565
+ const stream = import_fs9.default.createReadStream(filePath, { encoding: "utf-8" });
1566
+ const rl = import_readline3.default.createInterface({ input: stream, crlfDelay: Infinity });
1567
+ const messages = [];
1568
+ let lineNum = 0;
1569
+ for await (const line of rl) {
1570
+ lineNum++;
1571
+ if (lineNum <= afterLine) continue;
1572
+ if (!line.trim()) continue;
1573
+ try {
1574
+ const obj = JSON.parse(line);
1575
+ if (obj.type === "user") {
1576
+ const content = obj.message?.content;
1577
+ let text = "";
1578
+ if (typeof content === "string") {
1579
+ text = content;
1580
+ } else if (Array.isArray(content)) {
1581
+ text = content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
1582
+ }
1583
+ if (text) {
1584
+ messages.push({
1585
+ type: "user",
1586
+ content: text,
1587
+ timestamp: obj.timestamp
1588
+ });
1589
+ }
1590
+ } else if (obj.type === "assistant") {
1591
+ const content = obj.message?.content;
1592
+ if (!Array.isArray(content)) continue;
1593
+ for (const block of content) {
1594
+ if (block.type === "text" && block.text?.trim()) {
1595
+ messages.push({
1596
+ type: "assistant",
1597
+ content: block.text,
1598
+ timestamp: obj.timestamp,
1599
+ model: obj.message?.model
1600
+ });
1601
+ } else if (block.type === "tool_use") {
1602
+ let inputSummary = "";
1603
+ const input = block.input || {};
1604
+ if (input.command) inputSummary = input.command;
1605
+ else if (input.file_path) inputSummary = input.file_path;
1606
+ else if (input.pattern) inputSummary = input.pattern;
1607
+ else if (input.prompt) inputSummary = input.prompt.slice(0, 100);
1608
+ else {
1609
+ const keys = Object.keys(input);
1610
+ if (keys.length > 0) inputSummary = keys.join(", ");
1611
+ }
1612
+ messages.push({
1613
+ type: "tool_use",
1614
+ content: inputSummary,
1615
+ toolName: block.name,
1616
+ toolId: block.id,
1617
+ timestamp: obj.timestamp
1618
+ });
1619
+ } else if (block.type === "tool_result") {
1620
+ let resultText = "";
1621
+ if (typeof block.content === "string") {
1622
+ resultText = block.content;
1623
+ } else if (Array.isArray(block.content)) {
1624
+ resultText = block.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
1625
+ }
1626
+ if (resultText) {
1627
+ messages.push({
1628
+ type: "tool_result",
1629
+ content: resultText.length > 500 ? resultText.slice(0, 500) + "..." : resultText,
1630
+ toolId: block.tool_use_id,
1631
+ timestamp: obj.timestamp
1632
+ });
1633
+ }
1634
+ }
1635
+ }
1636
+ }
1637
+ } catch {
1638
+ }
1639
+ }
1640
+ return { sessionId, messages, totalLines: lineNum };
1641
+ } catch (err) {
1642
+ logger.error({ filePath, error: err.message }, "Failed to read conversation");
1643
+ return null;
1644
+ }
1645
+ }
1646
+ var import_fs9, import_path9, import_os8, import_readline3, CLAUDE_PROJECTS_DIR2;
1647
+ var init_claude_conversation = __esm({
1648
+ "src/claude-conversation.ts"() {
1649
+ "use strict";
1650
+ import_fs9 = __toESM(require("fs"));
1651
+ import_path9 = __toESM(require("path"));
1652
+ import_os8 = __toESM(require("os"));
1653
+ import_readline3 = __toESM(require("readline"));
1654
+ init_logger();
1655
+ init_claude_path();
1656
+ CLAUDE_PROJECTS_DIR2 = import_path9.default.join(import_os8.default.homedir(), ".claude", "projects");
1657
+ }
1658
+ });
1659
+
1660
+ // src/index.ts
1661
+ var index_exports = {};
1662
+ var import_socket, config, socket, SESSION_MAX_IDLE_MS, reaperInterval, vpnProfiles;
1663
+ var init_index = __esm({
1664
+ "src/index.ts"() {
1665
+ "use strict";
1666
+ import_socket = require("socket.io-client");
1667
+ init_src();
1668
+ init_config();
1669
+ init_logger();
1670
+ init_claude_plugin_installer();
1671
+ init_local_control();
1672
+ init_tmux();
1673
+ init_shell();
1674
+ init_heartbeat();
1675
+ init_file_explorer();
1676
+ init_vpn_manager();
1677
+ init_claude_sessions();
1678
+ init_claude_conversation();
1679
+ init_terminal_manager();
1680
+ if (!isConfigured()) {
1681
+ console.error("No agent configured. Run: crc-agent setup");
1682
+ process.exit(1);
1683
+ }
1684
+ config = loadConfig();
1685
+ logger.info({ agentId: config.agentId, serverUrl: config.serverUrl }, "Starting agent");
1686
+ process.on("unhandledRejection", (reason) => {
1687
+ logger.error({ reason }, "Unhandled promise rejection (agent kept alive)");
1688
+ });
1689
+ process.on("uncaughtException", (err) => {
1690
+ logger.error({ error: err instanceof Error ? err.message : String(err) }, "Uncaught exception (agent kept alive)");
1691
+ });
1692
+ installClaudeHooks(LOCAL_CONTROL_PORT);
1693
+ socket = (0, import_socket.io)(config.serverUrl + "/agent", {
1694
+ auth: { agentId: config.agentId, secret: config.secret },
1695
+ transports: ["websocket"],
1696
+ reconnection: true,
1697
+ reconnectionDelay: 1e3,
1698
+ reconnectionDelayMax: 3e4,
1699
+ reconnectionAttempts: Infinity
1700
+ });
1701
+ socket.on("connect", () => {
1702
+ logger.info("Connected to server");
1703
+ socket.emit(AGENT_HEARTBEAT, buildHeartbeat(config.homeDir));
1704
+ });
1705
+ socket.on("connect_error", (err) => {
1706
+ logger.error({ error: err.message }, "Connection error");
1707
+ });
1708
+ socket.on("disconnect", (reason) => {
1709
+ logger.warn({ reason }, "Disconnected from server");
1710
+ detachAllSessions();
1711
+ });
1712
+ setInterval(() => {
1713
+ if (socket.connected) {
1714
+ socket.emit(AGENT_HEARTBEAT, buildHeartbeat(config.homeDir));
1715
+ }
1716
+ }, HEARTBEAT_INTERVAL);
1717
+ SESSION_MAX_IDLE_MS = 6 * 60 * 60 * 1e3;
1718
+ reaperInterval = setInterval(() => {
1719
+ const reaped = reapDetachedSessions(SESSION_MAX_IDLE_MS);
1720
+ if (reaped.length > 0) {
1721
+ logger.info({ sessionIds: reaped, count: reaped.length }, "Reaped detached sessions");
1722
+ }
1723
+ }, 6e4);
1724
+ reaperInterval.unref();
1725
+ startLocalControl(LOCAL_CONTROL_PORT, (raw) => {
1726
+ const payload = normalizeClaudeHook(raw);
1727
+ if (!payload) return;
1728
+ logger.info(
1729
+ { rawEvent: raw?.hook_event_name || raw?.event, mapped: payload.event, project: payload.projectPath || null },
1730
+ "Claude hook received"
1731
+ );
1732
+ const transcript = raw?.transcript_path;
1733
+ if (payload.event === "stop" && typeof transcript === "string") {
1734
+ setTimeout(() => {
1735
+ payload.response = lastAssistantSummary(transcript);
1736
+ if (socket.connected) socket.emit(CLAUDE_HOOK, payload);
1737
+ }, 600);
1738
+ } else if (socket.connected) {
1739
+ socket.emit(CLAUDE_HOOK, payload);
1740
+ }
1741
+ });
1742
+ socket.on(TERMINAL_OPEN, (payload) => {
1743
+ const { sessionId, cols, rows, tmux, launch } = payload;
1744
+ if (!sessionId) return;
1745
+ const launchSpec = tmux ? buildTmuxLaunch(tmux, launch, detectShell(config.shell)) : void 0;
1746
+ createTerminalSession(
1747
+ sessionId,
1748
+ cols,
1749
+ rows,
1750
+ config.shell,
1751
+ config.homeDir,
1752
+ (sid, data) => {
1753
+ socket.emit(TERMINAL_OUTPUT, { sessionId: sid, data });
1754
+ },
1755
+ (sid, exitCode) => {
1756
+ socket.emit(TERMINAL_EXIT, { sessionId: sid, exitCode });
1757
+ },
1758
+ launchSpec
1759
+ );
1760
+ });
1761
+ socket.on(TMUX_LIST, async (payload) => {
1762
+ const { requestId } = payload;
1763
+ if (!requestId) return;
1764
+ try {
1765
+ const sessions2 = await listTmuxSessions();
1766
+ socket.emit(TMUX_LIST_RESULT, { requestId, sessions: sessions2 });
1767
+ } catch (err) {
1768
+ socket.emit(TMUX_LIST_RESULT, { requestId, sessions: [], error: err?.message || "tmux list failed" });
1769
+ }
1770
+ });
1771
+ socket.on(TERMINAL_INPUT, (payload) => {
1772
+ writeToSession(payload.sessionId, payload.data);
1773
+ });
1774
+ socket.on(TERMINAL_RESIZE, (payload) => {
1775
+ resizeSession(payload.sessionId, payload.cols, payload.rows);
1776
+ });
1777
+ socket.on(TERMINAL_CLOSE, (payload) => {
1778
+ closeSession(payload.sessionId);
1779
+ });
1780
+ socket.on(SESSION_SYNC, () => {
1781
+ const aliveIds = getAliveSessionIds();
1782
+ socket.emit(SESSION_SYNC_RESULT, { sessionIds: aliveIds });
1783
+ logger.info({ count: aliveIds.length }, "Session sync responded");
1784
+ });
1785
+ socket.on(SESSION_ATTACH, (payload) => {
1786
+ const buffered = attachSession(payload.sessionId, payload.cols, payload.rows);
1787
+ if (buffered) {
1788
+ socket.emit(SESSION_BUFFER, { sessionId: payload.sessionId, data: buffered });
1789
+ }
1790
+ });
1791
+ socket.on(SESSION_DETACH, (payload) => {
1792
+ detachSession(payload.sessionId);
1793
+ });
1794
+ socket.on(FILES_LIST, (payload) => {
1795
+ const { requestId, path: dirPath } = payload;
1796
+ if (!requestId) return;
1797
+ const result = listDirectory(dirPath);
1798
+ socket.emit(FILES_LIST_RESULT, {
1799
+ requestId,
1800
+ path: dirPath,
1801
+ entries: result.entries,
1802
+ error: result.error
1803
+ });
1804
+ });
1805
+ socket.on(FILES_DOWNLOAD, async (payload) => {
1806
+ const { requestId, path: filePath } = payload;
1807
+ if (!requestId) return;
1808
+ const result = await downloadFile(filePath, config.serverUrl, config.secret, config.agentId);
1809
+ if ("error" in result) {
1810
+ logger.error({ requestId, error: result.error }, "File download failed");
1811
+ socket.emit(FILES_DOWNLOAD_ERROR, { requestId, error: result.error });
1812
+ return;
1813
+ }
1814
+ socket.emit(FILES_DOWNLOAD_READY, {
1815
+ requestId,
1816
+ fileId: result.fileId,
1817
+ fileName: result.fileName,
1818
+ downloadUrl: result.downloadUrl,
1819
+ size: result.size
1820
+ });
1821
+ });
1822
+ vpnProfiles = process.platform === "win32" ? [] : config.vpn?.profiles || [];
1823
+ socket.on(VPN_LIST, async () => {
1824
+ const profiles = await getProfiles(vpnProfiles);
1825
+ socket.emit(VPN_UPDATE, { profiles });
1826
+ });
1827
+ socket.on(VPN_CONNECT, async (payload) => {
1828
+ const profiles = await connectVpn(vpnProfiles, payload.profileId);
1829
+ socket.emit(VPN_UPDATE, { profiles });
1830
+ });
1831
+ socket.on(VPN_DISCONNECT, async (payload) => {
1832
+ const profiles = await disconnectVpn(vpnProfiles, payload.profileId);
1833
+ socket.emit(VPN_UPDATE, { profiles });
1834
+ });
1835
+ socket.on(AGENT_EXEC, async (payload) => {
1836
+ const { requestId, command, cwd } = payload;
1837
+ if (!requestId) return;
1838
+ const { exec: exec2 } = await import("child_process");
1839
+ const { promisify: promisify4 } = await import("util");
1840
+ const execAsync2 = promisify4(exec2);
1841
+ try {
1842
+ const { stdout, stderr } = await execAsync2(command, { cwd, timeout: 3e4 });
1843
+ socket.emit(AGENT_EXEC_RESULT, { requestId, stdout: stdout || "", stderr: stderr || "" });
1844
+ } catch (err) {
1845
+ socket.emit(AGENT_EXEC_RESULT, {
1846
+ requestId,
1847
+ stdout: err.stdout || "",
1848
+ stderr: err.stderr || "",
1849
+ error: err.message
1850
+ });
1851
+ }
1852
+ });
1853
+ socket.on(CLAUDE_CONV_READ, async (payload) => {
1854
+ try {
1855
+ const result = await readConversation(payload.projectPath, payload.afterLine || 0, payload.sessionId);
1856
+ if (result) {
1857
+ socket.emit(CLAUDE_CONV_DATA, {
1858
+ sessionId: result.sessionId,
1859
+ messages: result.messages,
1860
+ totalLines: result.totalLines
1861
+ });
1862
+ } else {
1863
+ socket.emit(CLAUDE_CONV_DATA, {
1864
+ sessionId: "",
1865
+ messages: [],
1866
+ totalLines: 0,
1867
+ error: "No conversation found"
1868
+ });
1869
+ }
1870
+ } catch (err) {
1871
+ logger.error({ error: err?.message }, "Failed to read conversation");
1872
+ socket.emit(CLAUDE_CONV_DATA, {
1873
+ sessionId: "",
1874
+ messages: [],
1875
+ totalLines: 0,
1876
+ error: err?.message || "Failed to read conversation"
1877
+ });
1878
+ }
1879
+ });
1880
+ socket.on(CLAUDE_SESSIONS_LIST, async (payload) => {
1881
+ try {
1882
+ const sessions2 = await listClaudeSessions(payload.projectPath);
1883
+ socket.emit(CLAUDE_SESSIONS_RESULT, { sessions: sessions2 });
1884
+ } catch (err) {
1885
+ logger.error({ error: err?.message }, "Failed to list Claude sessions");
1886
+ socket.emit(CLAUDE_SESSIONS_RESULT, { sessions: [], error: err?.message || "Failed to list sessions" });
1887
+ }
1888
+ });
1889
+ process.on("SIGINT", () => {
1890
+ logger.info("Shutting down...");
1891
+ closeAllSessions();
1892
+ socket.disconnect();
1893
+ process.exit(0);
1894
+ });
1895
+ process.on("SIGTERM", () => {
1896
+ logger.info("Shutting down...");
1897
+ closeAllSessions();
1898
+ socket.disconnect();
1899
+ process.exit(0);
1900
+ });
1901
+ }
1902
+ });
1903
+
1904
+ // src/cli.ts
1905
+ var cmd = process.argv[2];
1906
+ if (cmd === "setup") {
1907
+ process.argv.splice(2, 1);
1908
+ Promise.resolve().then(() => init_setup());
1909
+ } else if (cmd === "--help" || cmd === "-h") {
1910
+ console.log(
1911
+ [
1912
+ "Usage: crc-agent [setup [--token <token>]]",
1913
+ "",
1914
+ "Run with no arguments to start the agent.",
1915
+ "Enroll this machine first with: crc-agent setup --help"
1916
+ ].join("\n")
1917
+ );
1918
+ } else {
1919
+ Promise.resolve().then(() => init_index());
1920
+ }
1921
+ //# sourceMappingURL=cli.js.map