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