@highflame/overwatch-v2 2.0.0-internal.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/daemon.js ADDED
@@ -0,0 +1,3578 @@
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 __commonJS = (cb, mod) => function __require() {
13
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
14
+ };
15
+ var __export = (target, all) => {
16
+ for (var name in all)
17
+ __defProp(target, name, { get: all[name], enumerable: true });
18
+ };
19
+ var __copyProps = (to, from, except, desc) => {
20
+ if (from && typeof from === "object" || typeof from === "function") {
21
+ for (let key of __getOwnPropNames(from))
22
+ if (!__hasOwnProp.call(to, key) && key !== except)
23
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
24
+ }
25
+ return to;
26
+ };
27
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
28
+ // If the importer is in node compatibility mode or this is not an ESM
29
+ // file that has been converted to a CommonJS file using a Babel-
30
+ // compatible transform (i.e. "__esModule" has not been set), then set
31
+ // "default" to the CommonJS "module.exports" for node compatibility.
32
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
33
+ mod
34
+ ));
35
+
36
+ // src/utils/logger.ts
37
+ var fs, path, os, LOG_FILE, Logger, logger;
38
+ var init_logger = __esm({
39
+ "src/utils/logger.ts"() {
40
+ "use strict";
41
+ fs = __toESM(require("fs"));
42
+ path = __toESM(require("path"));
43
+ os = __toESM(require("os"));
44
+ LOG_FILE = path.join(os.homedir(), ".overwatch", "guardian.log");
45
+ Logger = class {
46
+ constructor(level = 1 /* INFO */) {
47
+ this.fileEnabled = true;
48
+ this.level = level;
49
+ this.ensureLogDir();
50
+ }
51
+ ensureLogDir() {
52
+ try {
53
+ const dir = path.dirname(LOG_FILE);
54
+ if (!fs.existsSync(dir)) {
55
+ fs.mkdirSync(dir, { recursive: true });
56
+ }
57
+ } catch {
58
+ this.fileEnabled = false;
59
+ }
60
+ }
61
+ setLevel(level) {
62
+ this.level = level;
63
+ }
64
+ getCallerFile(offset) {
65
+ const err = new Error();
66
+ const stack = err.stack?.split("\n");
67
+ if (stack && stack.length > offset) {
68
+ const callerLine = stack[offset] || "";
69
+ const match = callerLine.match(/at\s+(?:.*\s+\()?(.+?):\d+:\d+\)?$/);
70
+ if (match && match[1]) {
71
+ return path.basename(match[1]);
72
+ }
73
+ }
74
+ return "unknown";
75
+ }
76
+ formatArgs(args) {
77
+ if (args.length === 0)
78
+ return "";
79
+ return args.map((arg) => {
80
+ if (typeof arg === "object" && arg !== null) {
81
+ try {
82
+ return JSON.stringify(arg).replace(/^{|}$/g, "").replace(/"([^"]+)":/g, "$1=");
83
+ } catch {
84
+ return String(arg);
85
+ }
86
+ }
87
+ return String(arg);
88
+ }).join(" ");
89
+ }
90
+ writeToFile(level, message, args) {
91
+ if (!this.fileEnabled)
92
+ return;
93
+ try {
94
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
95
+ const callerFile = this.getCallerFile(4);
96
+ const formattedArgs = this.formatArgs(args);
97
+ const argsStr = formattedArgs ? " " + formattedArgs : "";
98
+ const line = `${timestamp} [${level.padEnd(5)}] [${callerFile}] ${message}${argsStr}
99
+ `;
100
+ fs.appendFileSync(LOG_FILE, line);
101
+ } catch {
102
+ }
103
+ }
104
+ debug(message, ...args) {
105
+ if (this.level <= 0 /* DEBUG */) {
106
+ const callerFile = this.getCallerFile(3);
107
+ console.debug(`[DEBUG] [${callerFile}] ${message}`, ...args);
108
+ this.writeToFile("DEBUG", message, args);
109
+ }
110
+ }
111
+ info(message, ...args) {
112
+ if (this.level <= 1 /* INFO */) {
113
+ const callerFile = this.getCallerFile(3);
114
+ console.info(`[INFO ] [${callerFile}] ${message}`, ...args);
115
+ this.writeToFile("INFO", message, args);
116
+ }
117
+ }
118
+ warn(message, ...args) {
119
+ if (this.level <= 2 /* WARN */) {
120
+ const callerFile = this.getCallerFile(3);
121
+ console.warn(`[WARN ] [${callerFile}] ${message}`, ...args);
122
+ this.writeToFile("WARN", message, args);
123
+ }
124
+ }
125
+ error(message, ...args) {
126
+ if (this.level <= 3 /* ERROR */) {
127
+ const callerFile = this.getCallerFile(3);
128
+ console.error(`[ERROR] [${callerFile}] ${message}`, ...args);
129
+ this.writeToFile("ERROR", message, args);
130
+ }
131
+ }
132
+ };
133
+ logger = new Logger();
134
+ }
135
+ });
136
+
137
+ // src/utils/performance.ts
138
+ var init_performance = __esm({
139
+ "src/utils/performance.ts"() {
140
+ "use strict";
141
+ }
142
+ });
143
+
144
+ // src/utils/index.ts
145
+ var init_utils = __esm({
146
+ "src/utils/index.ts"() {
147
+ "use strict";
148
+ init_logger();
149
+ init_performance();
150
+ }
151
+ });
152
+
153
+ // src/auth/pkce.ts
154
+ function base64URLEncode(buffer) {
155
+ return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
156
+ }
157
+ function generateCodeVerifier() {
158
+ const buffer = crypto.randomBytes(32);
159
+ return base64URLEncode(buffer);
160
+ }
161
+ function generateCodeChallenge(verifier) {
162
+ const hash = crypto.createHash("sha256").update(verifier).digest();
163
+ return base64URLEncode(hash);
164
+ }
165
+ function generateState() {
166
+ return crypto.randomBytes(16).toString("hex");
167
+ }
168
+ var crypto;
169
+ var init_pkce = __esm({
170
+ "src/auth/pkce.ts"() {
171
+ "use strict";
172
+ crypto = __toESM(require("crypto"));
173
+ }
174
+ });
175
+
176
+ // src/version.ts
177
+ var VERSION, HIGHFLAME_API_URL, HIGHFLAME_OAUTH_URL, HIGHFLAME_CERBERUS_URL;
178
+ var init_version = __esm({
179
+ "src/version.ts"() {
180
+ "use strict";
181
+ VERSION = true ? "2.0.0-internal.1" : "0.0.0-dev";
182
+ HIGHFLAME_API_URL = "https://api.highflame.ai";
183
+ HIGHFLAME_OAUTH_URL = "https://studio.highflame.ai";
184
+ HIGHFLAME_CERBERUS_URL = "https://cerberus.highflame.ai";
185
+ }
186
+ });
187
+
188
+ // src/types/config.ts
189
+ var DEFAULT_OVERWATCH_CONFIG;
190
+ var init_config = __esm({
191
+ "src/types/config.ts"() {
192
+ "use strict";
193
+ init_version();
194
+ DEFAULT_OVERWATCH_CONFIG = {
195
+ version: VERSION,
196
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
197
+ highflame: {
198
+ baseUrl: HIGHFLAME_API_URL,
199
+ oauthUrl: HIGHFLAME_OAUTH_URL,
200
+ cerberusUrl: HIGHFLAME_CERBERUS_URL
201
+ },
202
+ engines: {
203
+ javelin: {
204
+ enabled: true
205
+ }
206
+ },
207
+ daemon: {
208
+ autoStart: true,
209
+ logLevel: "info"
210
+ },
211
+ enabled: true
212
+ };
213
+ }
214
+ });
215
+
216
+ // src/types/events.ts
217
+ var init_events = __esm({
218
+ "src/types/events.ts"() {
219
+ "use strict";
220
+ }
221
+ });
222
+
223
+ // src/types/remote-policy.ts
224
+ var init_remote_policy = __esm({
225
+ "src/types/remote-policy.ts"() {
226
+ "use strict";
227
+ }
228
+ });
229
+
230
+ // src/types/index.ts
231
+ var init_types = __esm({
232
+ "src/types/index.ts"() {
233
+ "use strict";
234
+ init_config();
235
+ init_events();
236
+ init_remote_policy();
237
+ }
238
+ });
239
+
240
+ // src/auth/token-store.ts
241
+ function loadSession() {
242
+ try {
243
+ if (fs2.existsSync(SESSION_PATH)) {
244
+ const data = fs2.readFileSync(SESSION_PATH, "utf-8");
245
+ return JSON.parse(data);
246
+ }
247
+ } catch (e) {
248
+ logger.debug("Failed to load session file", {
249
+ error: e instanceof Error ? e.message : String(e),
250
+ path: SESSION_PATH
251
+ });
252
+ }
253
+ return null;
254
+ }
255
+ function writeSession(session) {
256
+ if (!fs2.existsSync(CONFIG_DIR)) {
257
+ fs2.mkdirSync(CONFIG_DIR, { recursive: true });
258
+ }
259
+ const data = JSON.stringify(session, null, 2);
260
+ fs2.writeFileSync(SESSION_PATH, data, { mode: SESSION_FILE_MODE });
261
+ try {
262
+ fs2.chmodSync(SESSION_PATH, SESSION_FILE_MODE);
263
+ } catch (e) {
264
+ logger.debug("Failed to set session file permissions", {
265
+ error: e instanceof Error ? e.message : String(e)
266
+ });
267
+ }
268
+ }
269
+ function saveTokens(tokens, userInfo2) {
270
+ const expiresAt = tokens.expires_at ?? Date.now() + TOKEN_EXPIRY_SECONDS * 1e3;
271
+ const auth = {
272
+ access_token: tokens.access_token,
273
+ refresh_token: tokens.refresh_token || "",
274
+ expires_at: expiresAt,
275
+ token_type: tokens.token_type,
276
+ user: userInfo2 ? {
277
+ email: userInfo2.email
278
+ } : void 0,
279
+ authenticated_at: (/* @__PURE__ */ new Date()).toISOString()
280
+ };
281
+ writeSession(auth);
282
+ }
283
+ function loadTokens() {
284
+ return loadSession();
285
+ }
286
+ async function getValidAccessToken() {
287
+ const auth = loadTokens();
288
+ if (!auth)
289
+ return null;
290
+ if (isTokenExpired(auth.expires_at)) {
291
+ if (auth.refresh_token) {
292
+ const { refreshAccessToken: refreshAccessToken2 } = await Promise.resolve().then(() => (init_oauth(), oauth_exports));
293
+ const refreshed = await refreshAccessToken2(auth.refresh_token);
294
+ if (refreshed) {
295
+ saveTokens(refreshed);
296
+ return refreshed.access_token;
297
+ }
298
+ }
299
+ return null;
300
+ }
301
+ return auth.access_token;
302
+ }
303
+ function getAuthStatus() {
304
+ const auth = loadTokens();
305
+ if (!auth) {
306
+ return { authenticated: false };
307
+ }
308
+ return {
309
+ authenticated: true,
310
+ email: auth.user?.email,
311
+ name: auth.user?.name,
312
+ orgName: auth.user?.org_name,
313
+ expiresAt: auth.expires_at,
314
+ expired: isTokenExpired(auth.expires_at),
315
+ expiringSoon: isTokenExpiringSoon(auth.expires_at)
316
+ };
317
+ }
318
+ var fs2, path2, os2, CONFIG_DIR, SESSION_PATH, SESSION_FILE_MODE;
319
+ var init_token_store = __esm({
320
+ "src/auth/token-store.ts"() {
321
+ "use strict";
322
+ fs2 = __toESM(require("fs"));
323
+ path2 = __toESM(require("path"));
324
+ os2 = __toESM(require("os"));
325
+ init_oauth();
326
+ init_utils();
327
+ CONFIG_DIR = path2.join(os2.homedir(), ".overwatch");
328
+ SESSION_PATH = path2.join(CONFIG_DIR, "session.json");
329
+ SESSION_FILE_MODE = 384;
330
+ }
331
+ });
332
+
333
+ // src/config/manager.ts
334
+ function loadConfig() {
335
+ try {
336
+ if (fs3.existsSync(CONFIG_PATH)) {
337
+ const data = fs3.readFileSync(CONFIG_PATH, "utf-8");
338
+ return JSON.parse(data);
339
+ }
340
+ } catch (e) {
341
+ console.error("Failed to load config:", e);
342
+ }
343
+ return null;
344
+ }
345
+ function getBaseUrl() {
346
+ const config = loadConfig();
347
+ return config?.highflame?.baseUrl || process.env.HIGHFLAME_BASE_URL || HIGHFLAME_API_URL;
348
+ }
349
+ function getOAuthUrl() {
350
+ const config = loadConfig();
351
+ return config?.highflame?.oauthUrl || HIGHFLAME_OAUTH_URL;
352
+ }
353
+ function getCerberusUrl() {
354
+ const config = loadConfig();
355
+ return config?.highflame?.cerberusUrl || process.env.CERBERUS_BASE_URL || HIGHFLAME_CERBERUS_URL;
356
+ }
357
+ function getLLMConfig() {
358
+ const config = loadConfig();
359
+ const envKey = process.env.LLM_API_KEY || process.env.OPENAI_API_KEY;
360
+ if (envKey) {
361
+ return {
362
+ apiKey: envKey,
363
+ provider: config?.llm?.provider || "openai"
364
+ };
365
+ }
366
+ return config?.llm || null;
367
+ }
368
+ var fs3, path3, os3, CONFIG_DIR2, CONFIG_PATH;
369
+ var init_manager = __esm({
370
+ "src/config/manager.ts"() {
371
+ "use strict";
372
+ fs3 = __toESM(require("fs"));
373
+ path3 = __toESM(require("path"));
374
+ os3 = __toESM(require("os"));
375
+ init_types();
376
+ init_token_store();
377
+ init_version();
378
+ CONFIG_DIR2 = path3.join(os3.homedir(), ".overwatch");
379
+ CONFIG_PATH = path3.join(CONFIG_DIR2, "config.json");
380
+ }
381
+ });
382
+
383
+ // src/config/index.ts
384
+ var init_config2 = __esm({
385
+ "src/config/index.ts"() {
386
+ "use strict";
387
+ init_manager();
388
+ }
389
+ });
390
+
391
+ // src/auth/oauth.ts
392
+ var oauth_exports = {};
393
+ __export(oauth_exports, {
394
+ DEFAULT_EXPIRY_BUFFER_SECONDS: () => DEFAULT_EXPIRY_BUFFER_SECONDS,
395
+ EXPIRY_WARN_THRESHOLD_SECONDS: () => EXPIRY_WARN_THRESHOLD_SECONDS,
396
+ OAUTH_CONFIG: () => OAUTH_CONFIG,
397
+ TOKEN_EXPIRY_SECONDS: () => TOKEN_EXPIRY_SECONDS,
398
+ buildAuthorizationUrl: () => buildAuthorizationUrl,
399
+ createOAuthState: () => createOAuthState,
400
+ exchangeCodeForTokens: () => exchangeCodeForTokens,
401
+ isTokenExpired: () => isTokenExpired,
402
+ isTokenExpiringSoon: () => isTokenExpiringSoon,
403
+ refreshAccessToken: () => refreshAccessToken
404
+ });
405
+ function buildAuthorizationUrl(state, codeChallenge, redirectUri) {
406
+ const params = new URLSearchParams({
407
+ response_type: "code",
408
+ client_id: OAUTH_CONFIG.clientId,
409
+ redirect_uri: redirectUri,
410
+ code_challenge: codeChallenge,
411
+ code_challenge_method: "S256",
412
+ state,
413
+ scope: OAUTH_CONFIG.scopes.join(" ")
414
+ });
415
+ return `${OAUTH_CONFIG.authUrl}?${params.toString()}`;
416
+ }
417
+ async function exchangeCodeForTokens(code, codeVerifier, redirectUri) {
418
+ logger.debug("[OAuth] Exchanging code for tokens", {
419
+ tokenUrl: OAUTH_CONFIG.tokenUrl
420
+ });
421
+ const response = await fetch(OAUTH_CONFIG.tokenUrl, {
422
+ method: "POST",
423
+ headers: {
424
+ "Content-Type": "application/json",
425
+ Accept: "application/json"
426
+ },
427
+ body: JSON.stringify({
428
+ grant_type: "authorization_code",
429
+ client_id: OAUTH_CONFIG.clientId,
430
+ code,
431
+ redirect_uri: redirectUri,
432
+ code_verifier: codeVerifier
433
+ }),
434
+ signal: AbortSignal.timeout(3e4)
435
+ });
436
+ if (!response.ok) {
437
+ const errorText = await response.text();
438
+ logger.error("[OAuth] Token exchange failed", {
439
+ status: response.status,
440
+ body: errorText.substring(0, 500)
441
+ });
442
+ throw new Error(`Token exchange failed: ${response.status} ${errorText}`);
443
+ }
444
+ const data = await response.json();
445
+ logger.debug("[OAuth] Token exchange successful");
446
+ const expiresInSeconds = data.expires_in || TOKEN_EXPIRY_SECONDS;
447
+ data.expires_at = Date.now() + expiresInSeconds * 1e3;
448
+ const userInfo2 = {
449
+ id: data.user?.id || "",
450
+ email: data.user?.email || ""
451
+ };
452
+ return { tokens: data, userInfo: userInfo2 };
453
+ }
454
+ async function refreshAccessToken(_refreshToken) {
455
+ return null;
456
+ }
457
+ function isTokenExpired(expiresAt, bufferSeconds = DEFAULT_EXPIRY_BUFFER_SECONDS) {
458
+ return Date.now() >= expiresAt - bufferSeconds * 1e3;
459
+ }
460
+ function isTokenExpiringSoon(expiresAt, thresholdSeconds = EXPIRY_WARN_THRESHOLD_SECONDS) {
461
+ if (isTokenExpired(expiresAt))
462
+ return false;
463
+ return Date.now() >= expiresAt - thresholdSeconds * 1e3;
464
+ }
465
+ function createOAuthState(guardianPort) {
466
+ const state = generateState();
467
+ const codeVerifier = generateCodeVerifier();
468
+ const redirectUri = `http://127.0.0.1:${guardianPort}/oauth/callback`;
469
+ return {
470
+ state,
471
+ codeVerifier,
472
+ redirectUri,
473
+ createdAt: Date.now(),
474
+ status: "pending"
475
+ };
476
+ }
477
+ var OAUTH_CONFIG, TOKEN_EXPIRY_SECONDS, DEFAULT_EXPIRY_BUFFER_SECONDS, EXPIRY_WARN_THRESHOLD_SECONDS;
478
+ var init_oauth = __esm({
479
+ "src/auth/oauth.ts"() {
480
+ "use strict";
481
+ init_pkce();
482
+ init_config2();
483
+ init_utils();
484
+ OAUTH_CONFIG = {
485
+ get authUrl() {
486
+ return `${getOAuthUrl()}/cli-auth`;
487
+ },
488
+ get tokenUrl() {
489
+ return `${getOAuthUrl()}/api/cli-auth/token`;
490
+ },
491
+ clientId: "overwatch-cli",
492
+ scopes: ["account:read", "gateway:read"]
493
+ };
494
+ TOKEN_EXPIRY_SECONDS = 90 * 24 * 60 * 60;
495
+ DEFAULT_EXPIRY_BUFFER_SECONDS = 5 * 60;
496
+ EXPIRY_WARN_THRESHOLD_SECONDS = 7 * 24 * 60 * 60;
497
+ }
498
+ });
499
+
500
+ // lib/platform-loader.js
501
+ var require_platform_loader = __commonJS({
502
+ "lib/platform-loader.js"(exports2, module2) {
503
+ "use strict";
504
+ var path14 = require("path");
505
+ var fs15 = require("fs");
506
+ var os14 = require("os");
507
+ var PLATFORM_MAP = {
508
+ "darwin-arm64": {
509
+ package: "@highflame/overwatch-v2-darwin-arm64",
510
+ rustTarget: "aarch64-apple-darwin",
511
+ binaryName: "ramparts-aarch64-apple-darwin"
512
+ },
513
+ "darwin-x64": {
514
+ package: "@highflame/overwatch-v2-darwin-x64",
515
+ rustTarget: "x86_64-apple-darwin",
516
+ binaryName: "ramparts-x86_64-apple-darwin"
517
+ },
518
+ "linux-x64": {
519
+ package: "@highflame/overwatch-v2-linux-x64",
520
+ rustTarget: "x86_64-unknown-linux-gnu",
521
+ binaryName: "ramparts-x86_64-unknown-linux-gnu"
522
+ },
523
+ "linux-arm64": {
524
+ package: "@highflame/overwatch-v2-linux-arm64",
525
+ rustTarget: "aarch64-unknown-linux-gnu",
526
+ binaryName: "ramparts-aarch64-unknown-linux-gnu"
527
+ },
528
+ "win32-x64": {
529
+ package: "@highflame/overwatch-v2-win32-x64",
530
+ rustTarget: "x86_64-pc-windows-msvc",
531
+ binaryName: "ramparts-x86_64-pc-windows-msvc.exe"
532
+ },
533
+ "win32-arm64": {
534
+ package: "@highflame/overwatch-v2-win32-arm64",
535
+ rustTarget: "aarch64-pc-windows-msvc",
536
+ binaryName: "ramparts-aarch64-pc-windows-msvc.exe"
537
+ }
538
+ };
539
+ function getPlatformKey() {
540
+ return `${process.platform}-${process.arch}`;
541
+ }
542
+ function getPlatformConfig() {
543
+ const key = getPlatformKey();
544
+ return PLATFORM_MAP[key] || null;
545
+ }
546
+ function loadPlatformPackage() {
547
+ const config = getPlatformConfig();
548
+ if (!config) {
549
+ return null;
550
+ }
551
+ try {
552
+ const dynamicRequire = typeof __non_webpack_require__ !== "undefined" ? __non_webpack_require__ : require;
553
+ return dynamicRequire(config.package);
554
+ } catch (error) {
555
+ return null;
556
+ }
557
+ }
558
+ function getPackageRoot() {
559
+ let currentDir = __dirname;
560
+ const baseName = path14.basename(currentDir);
561
+ if (baseName === "lib" || baseName === "dist") {
562
+ return path14.resolve(currentDir, "..");
563
+ }
564
+ return path14.resolve(currentDir, "..");
565
+ }
566
+ function getRampartsBinaryPath() {
567
+ const config = getPlatformConfig();
568
+ const platformPkg = loadPlatformPackage();
569
+ if (platformPkg && typeof platformPkg.getRampartsBinaryPath === "function") {
570
+ const binaryPath = platformPkg.getRampartsBinaryPath();
571
+ if (binaryPath && fs15.existsSync(binaryPath)) {
572
+ return binaryPath;
573
+ }
574
+ }
575
+ const packageRoot = getPackageRoot();
576
+ const localPaths = [];
577
+ const isWindows = process.platform === "win32";
578
+ const genericBinaryName = isWindows ? "ramparts.exe" : "ramparts";
579
+ if (config) {
580
+ localPaths.push(path14.join(packageRoot, "bin", config.binaryName));
581
+ }
582
+ localPaths.push(
583
+ path14.join(packageRoot, "bin", genericBinaryName),
584
+ path14.join(packageRoot, "dist", "bin", genericBinaryName)
585
+ );
586
+ for (const p of localPaths) {
587
+ if (fs15.existsSync(p)) {
588
+ return p;
589
+ }
590
+ }
591
+ const overwatchPaths = [];
592
+ if (config) {
593
+ overwatchPaths.push(
594
+ path14.join(os14.homedir(), ".overwatch", "bin", config.binaryName)
595
+ );
596
+ }
597
+ overwatchPaths.push(
598
+ path14.join(os14.homedir(), ".overwatch", "bin", genericBinaryName)
599
+ );
600
+ for (const p of overwatchPaths) {
601
+ if (fs15.existsSync(p)) {
602
+ return p;
603
+ }
604
+ }
605
+ try {
606
+ const { execSync } = require("child_process");
607
+ const findCmd = isWindows ? `where ${genericBinaryName}` : `which ${genericBinaryName}`;
608
+ const result = execSync(findCmd, { encoding: "utf-8" }).trim();
609
+ const foundPath = result.split(/\r?\n/)[0];
610
+ if (foundPath && fs15.existsSync(foundPath)) {
611
+ return foundPath;
612
+ }
613
+ } catch {
614
+ }
615
+ return null;
616
+ }
617
+ function isPlatformSupported() {
618
+ return getPlatformConfig() !== null;
619
+ }
620
+ function getPlatformNotFoundMessage() {
621
+ const key = getPlatformKey();
622
+ const config = getPlatformConfig();
623
+ if (!config) {
624
+ return `Unsupported platform: ${key}. Supported platforms: ${Object.keys(PLATFORM_MAP).join(", ")}`;
625
+ }
626
+ return `Platform binaries not found for ${key}.
627
+
628
+ To fix this, either:
629
+ 1. Install the platform package: npm install ${config.package}
630
+ 2. Run the native deps build: ./scripts/build-native-deps.sh
631
+ 3. Place binaries in ~/.overwatch/bin/
632
+ `;
633
+ }
634
+ module2.exports = {
635
+ getPlatformKey,
636
+ getPlatformConfig,
637
+ loadPlatformPackage,
638
+ getRampartsBinaryPath,
639
+ isPlatformSupported,
640
+ getPlatformNotFoundMessage,
641
+ PLATFORM_MAP
642
+ };
643
+ }
644
+ });
645
+
646
+ // src/daemon.ts
647
+ var fs14 = __toESM(require("fs"));
648
+ var path13 = __toESM(require("path"));
649
+ var os13 = __toESM(require("os"));
650
+
651
+ // src/http/server.ts
652
+ var http = __toESM(require("http"));
653
+ init_utils();
654
+ var GuardianHttpServer = class {
655
+ constructor(port) {
656
+ this.requestListeners = /* @__PURE__ */ new Map();
657
+ this.requestCount = 0;
658
+ this.port = port;
659
+ logger.debug("Creating server instance", {
660
+ requestedPort: port,
661
+ isEphemeral: port === 0
662
+ });
663
+ this.server = http.createServer(this.handleRequest.bind(this));
664
+ }
665
+ /**
666
+ * Start the server
667
+ */
668
+ start() {
669
+ logger.info("Starting server...");
670
+ return new Promise((resolve, reject) => {
671
+ this.server.listen(this.port, "127.0.0.1", () => {
672
+ const address = this.server.address();
673
+ const originalPort = this.port;
674
+ if (address && typeof address === "object") {
675
+ this.port = address.port;
676
+ }
677
+ logger.info("Server started successfully", {
678
+ requestedPort: originalPort,
679
+ assignedPort: this.port,
680
+ host: "127.0.0.1",
681
+ registeredPaths: Array.from(this.requestListeners.keys())
682
+ });
683
+ resolve();
684
+ });
685
+ this.server.on("error", (error) => {
686
+ logger.error("Server startup error:", {
687
+ code: error.code,
688
+ message: error.message,
689
+ port: this.port
690
+ });
691
+ reject(error);
692
+ });
693
+ this.server.on("close", () => {
694
+ logger.debug("Server closed");
695
+ });
696
+ this.server.on("connection", (socket) => {
697
+ logger.debug("New connection", {
698
+ remoteAddress: socket.remoteAddress,
699
+ remotePort: socket.remotePort
700
+ });
701
+ });
702
+ });
703
+ }
704
+ /**
705
+ * Stop the server
706
+ */
707
+ stop() {
708
+ logger.info("Stopping server...");
709
+ return new Promise((resolve) => {
710
+ if (this.server.listening) {
711
+ this.server.close(() => {
712
+ logger.info("Server stopped", {
713
+ totalRequestsServed: this.requestCount
714
+ });
715
+ resolve();
716
+ });
717
+ } else {
718
+ logger.debug("Server was not listening");
719
+ resolve();
720
+ }
721
+ });
722
+ }
723
+ /**
724
+ * Register a request handler for a specific path
725
+ */
726
+ on(path14, listener) {
727
+ this.requestListeners.set(path14, listener);
728
+ logger.debug("Registered handler", {
729
+ path: path14,
730
+ totalHandlers: this.requestListeners.size
731
+ });
732
+ }
733
+ /**
734
+ * Handle incoming requests
735
+ */
736
+ handleRequest(req, res) {
737
+ const startTime = Date.now();
738
+ const url = new URL(req.url || "", `http://${req.headers.host}`);
739
+ logger.debug("Incoming request", {
740
+ method: req.method,
741
+ path: url.pathname,
742
+ query: url.search || "(none)",
743
+ userAgent: req.headers["user-agent"],
744
+ contentLength: req.headers["content-length"]
745
+ });
746
+ let listener = this.requestListeners.get(url.pathname);
747
+ let matchedPath = url.pathname;
748
+ if (!listener) {
749
+ for (const [path14, handler] of this.requestListeners.entries()) {
750
+ if (url.pathname.startsWith(path14 + "/") || url.pathname === path14) {
751
+ listener = handler;
752
+ matchedPath = path14;
753
+ break;
754
+ }
755
+ }
756
+ }
757
+ if (listener) {
758
+ logger.debug(`Handler found for ${matchedPath}`);
759
+ const originalEnd = res.end.bind(res);
760
+ res.end = (...args) => {
761
+ const duration = Date.now() - startTime;
762
+ logger.debug("Response sent", {
763
+ statusCode: res.statusCode,
764
+ duration
765
+ });
766
+ return originalEnd(...args);
767
+ };
768
+ listener(req, res);
769
+ } else {
770
+ const duration = Date.now() - startTime;
771
+ logger.warn(`No handler for path: ${url.pathname}`, {
772
+ availablePaths: Array.from(this.requestListeners.keys()),
773
+ duration
774
+ });
775
+ res.writeHead(404, { "Content-Type": "application/json" });
776
+ res.end(JSON.stringify({ error: "Not Found", path: url.pathname }));
777
+ }
778
+ }
779
+ /**
780
+ * Get the port the server is listening on
781
+ */
782
+ getPort() {
783
+ return this.port;
784
+ }
785
+ /**
786
+ * Check if server is currently listening
787
+ */
788
+ isListening() {
789
+ return this.server.listening;
790
+ }
791
+ };
792
+
793
+ // src/javelin/config-reader.ts
794
+ var fs4 = __toESM(require("fs"));
795
+ var path4 = __toESM(require("path"));
796
+ var os4 = __toESM(require("os"));
797
+ init_utils();
798
+
799
+ // src/auth/index.ts
800
+ init_pkce();
801
+ init_oauth();
802
+
803
+ // src/auth/cli-oauth.ts
804
+ init_pkce();
805
+ init_oauth();
806
+ init_token_store();
807
+
808
+ // src/auth/html-utils.ts
809
+ function escapeHtml(unsafe) {
810
+ return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
811
+ }
812
+ var COMMON_STYLES = `
813
+ * {
814
+ margin: 0;
815
+ padding: 0;
816
+ box-sizing: border-box;
817
+ }
818
+ body {
819
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
820
+ -webkit-font-smoothing: antialiased;
821
+ -moz-osx-font-smoothing: grayscale;
822
+ background: #ffffff;
823
+ min-height: 100vh;
824
+ display: flex;
825
+ align-items: center;
826
+ justify-content: center;
827
+ padding: 16px;
828
+ }
829
+ .container {
830
+ background: white;
831
+ border-radius: 12px;
832
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
833
+ width: 100%;
834
+ max-width: 448px;
835
+ padding: 0;
836
+ overflow: hidden;
837
+ }
838
+ .card-header {
839
+ text-align: center;
840
+ padding: 32px 24px 24px;
841
+ }
842
+ .icon-container {
843
+ margin: 0 auto 16px;
844
+ width: 64px;
845
+ height: 64px;
846
+ display: flex;
847
+ align-items: center;
848
+ justify-content: center;
849
+ border-radius: 50%;
850
+ }
851
+ .success .icon-container {
852
+ background: #dcfce7;
853
+ }
854
+ .error .icon-container {
855
+ background: #fee2e2;
856
+ }
857
+ .icon {
858
+ width: 32px;
859
+ height: 32px;
860
+ }
861
+ .success .icon {
862
+ color: #16a34a;
863
+ }
864
+ .error .icon {
865
+ color: #dc2626;
866
+ }
867
+ h1 {
868
+ font-size: 24px;
869
+ font-weight: 600;
870
+ line-height: 1.2;
871
+ margin-bottom: 8px;
872
+ }
873
+ .success h1 {
874
+ color: #16a34a;
875
+ }
876
+ .error h1 {
877
+ color: #dc2626;
878
+ }
879
+ .description {
880
+ color: #6b7280;
881
+ font-size: 14px;
882
+ line-height: 1.5;
883
+ margin-top: 8px;
884
+ }
885
+ .email {
886
+ font-weight: 500;
887
+ color: #111827;
888
+ }
889
+ .error-message {
890
+ color: #dc2626;
891
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
892
+ font-size: 13px;
893
+ background: #fef2f2;
894
+ padding: 12px;
895
+ border-radius: 6px;
896
+ margin: 16px 24px;
897
+ border: 1px solid #fecaca;
898
+ word-break: break-word;
899
+ }
900
+ `;
901
+ function generateSuccessHtml(message, email) {
902
+ const safeMessage = escapeHtml(message);
903
+ const safeEmail = email ? escapeHtml(email) : "user";
904
+ return `<!DOCTYPE html>
905
+ <html lang="en">
906
+ <head>
907
+ <meta charset="UTF-8">
908
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
909
+ <title>Authorization Successful</title>
910
+ <style>${COMMON_STYLES}</style>
911
+ </head>
912
+ <body>
913
+ <div class="container success">
914
+ <div class="card-header">
915
+ <div class="icon-container">
916
+ <svg class="icon" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
917
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
918
+ </svg>
919
+ </div>
920
+ <h1>Authorization Successful!</h1>
921
+ <p class="description">
922
+ ${email ? `Welcome, <span class="email">${safeEmail}</span>!` : ""}
923
+ ${safeMessage}
924
+ </p>
925
+ </div>
926
+ </div>
927
+ </body>
928
+ </html>`;
929
+ }
930
+ function generateErrorHtml(error) {
931
+ const safeError = escapeHtml(error);
932
+ return `<!DOCTYPE html>
933
+ <html lang="en">
934
+ <head>
935
+ <meta charset="UTF-8">
936
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
937
+ <title>Authorization Failed</title>
938
+ <style>${COMMON_STYLES}</style>
939
+ </head>
940
+ <body>
941
+ <div class="container error">
942
+ <div class="card-header">
943
+ <div class="icon-container">
944
+ <svg class="icon" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
945
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
946
+ </svg>
947
+ </div>
948
+ <h1>Authorization Failed</h1>
949
+ <p class="description">Please try again from the terminal.</p>
950
+ </div>
951
+ <div class="error-message">${safeError}</div>
952
+ </div>
953
+ </body>
954
+ </html>`;
955
+ }
956
+
957
+ // src/auth/cli-oauth.ts
958
+ var OAUTH_TIMEOUT_MS = 5 * 60 * 1e3;
959
+
960
+ // src/auth/index.ts
961
+ init_token_store();
962
+
963
+ // src/javelin/config-reader.ts
964
+ init_config2();
965
+ var ConfigReader = class {
966
+ constructor(configPath) {
967
+ this.rawConfig = null;
968
+ this.configPath = configPath || path4.join(os4.homedir(), ".overwatch", "config.json");
969
+ logger.debug("Initialized", { configPath: this.configPath });
970
+ }
971
+ /**
972
+ * Get Highflame API configuration
973
+ */
974
+ getHighflameConfig() {
975
+ const envBaseUrl = process.env.HIGHFLAME_BASE_URL;
976
+ return {
977
+ baseUrl: envBaseUrl || getBaseUrl(),
978
+ enabled: this.rawConfig?.engines?.javelin?.enabled !== false
979
+ };
980
+ }
981
+ /**
982
+ * Get a fresh, validated JWT token for API authentication.
983
+ * Re-reads session.json each time, checks expiry, and refreshes if needed.
984
+ */
985
+ async getJwtToken() {
986
+ const envToken = process.env.HIGHFLAME_TOKEN;
987
+ if (envToken) {
988
+ return envToken;
989
+ }
990
+ const token = await getValidAccessToken();
991
+ if (!token) {
992
+ logger.warn(
993
+ "No valid authentication token. Admin API calls will be skipped. Run `overwatch login` to authenticate."
994
+ );
995
+ }
996
+ return token;
997
+ }
998
+ /**
999
+ * Get the full overwatch config
1000
+ */
1001
+ getOverwatchConfig() {
1002
+ return this.rawConfig;
1003
+ }
1004
+ /**
1005
+ * Load raw config from file
1006
+ */
1007
+ async loadRawConfig() {
1008
+ if (!fs4.existsSync(this.configPath)) {
1009
+ logger.warn(`Config file not found: ${this.configPath}`);
1010
+ this.rawConfig = null;
1011
+ return;
1012
+ }
1013
+ try {
1014
+ const configData = await fs4.promises.readFile(this.configPath, "utf-8");
1015
+ this.rawConfig = JSON.parse(configData);
1016
+ logger.info("Config loaded:", {
1017
+ version: this.rawConfig.version,
1018
+ hasHighflame: !!this.rawConfig.highflame
1019
+ });
1020
+ } catch (error) {
1021
+ logger.error("Failed to load config:", {
1022
+ error: String(error)
1023
+ });
1024
+ this.rawConfig = null;
1025
+ }
1026
+ }
1027
+ /**
1028
+ * Get admin API configuration (derived from Highflame config)
1029
+ */
1030
+ getAdminConfig() {
1031
+ const highflameConfig = this.getHighflameConfig();
1032
+ return {
1033
+ enabled: highflameConfig.enabled,
1034
+ baseUrl: highflameConfig.baseUrl || null
1035
+ };
1036
+ }
1037
+ /**
1038
+ * Get admin token for API authentication
1039
+ */
1040
+ async getAdminToken() {
1041
+ return await this.getJwtToken();
1042
+ }
1043
+ };
1044
+
1045
+ // src/javelin/admin-client.ts
1046
+ init_utils();
1047
+ var DEFAULT_TIMEOUT = 5e3;
1048
+ var AdminClient = class {
1049
+ constructor(baseUrl, tokenGetter, timeout = DEFAULT_TIMEOUT) {
1050
+ this.baseUrl = baseUrl.replace(/\/$/, "");
1051
+ this.tokenGetter = typeof tokenGetter === "string" ? () => Promise.resolve(tokenGetter) : tokenGetter;
1052
+ this.timeout = timeout;
1053
+ logger.debug("Initialized", {
1054
+ baseUrl: this.baseUrl,
1055
+ timeout: this.timeout
1056
+ });
1057
+ }
1058
+ async getToken() {
1059
+ return this.tokenGetter();
1060
+ }
1061
+ /**
1062
+ * Record installation event
1063
+ * Called on first event from a new user/IDE combination
1064
+ */
1065
+ async recordInstallation(event) {
1066
+ const url = `${this.baseUrl}/v1/admin/guardian/installations`;
1067
+ const token = await this.getToken();
1068
+ if (!token) {
1069
+ logger.warn("No valid auth token, skipping installation recording");
1070
+ return false;
1071
+ }
1072
+ logger.info("Recording installation", {
1073
+ ide_type: event.ide_type,
1074
+ user_email: event.user_email
1075
+ });
1076
+ try {
1077
+ const response = await fetch(url, {
1078
+ method: "POST",
1079
+ headers: {
1080
+ "Content-Type": "application/json",
1081
+ Authorization: `Bearer ${token}`
1082
+ },
1083
+ body: JSON.stringify(event),
1084
+ signal: AbortSignal.timeout(this.timeout)
1085
+ });
1086
+ if (!response.ok) {
1087
+ const errorText = await response.text();
1088
+ logger.error("Failed to record installation", {
1089
+ status: response.status,
1090
+ error: errorText
1091
+ });
1092
+ return false;
1093
+ }
1094
+ logger.info("Installation recorded successfully");
1095
+ return true;
1096
+ } catch (error) {
1097
+ logger.error("Error recording installation", {
1098
+ error: String(error)
1099
+ });
1100
+ return false;
1101
+ }
1102
+ }
1103
+ /**
1104
+ * Report coverage metrics to admin API.
1105
+ * Called periodically by GuardianModule (default cadence: every 10
1106
+ * minutes; see COVERAGE_INTERVAL_MS in module.ts) to report agent
1107
+ * detection coverage.
1108
+ */
1109
+ async reportCoverageMetrics(report) {
1110
+ const url = `${this.baseUrl}/v1/admin/guardian/coverage`;
1111
+ const token = await this.getToken();
1112
+ if (!token) {
1113
+ logger.warn("No valid auth token, skipping coverage reporting");
1114
+ return false;
1115
+ }
1116
+ logger.info("Reporting coverage metrics", {
1117
+ installed_count: report.installed_count,
1118
+ monitored_count: report.monitored_count,
1119
+ coverage_percentage: report.coverage_percentage,
1120
+ platform: report.platform
1121
+ });
1122
+ try {
1123
+ const response = await fetch(url, {
1124
+ method: "POST",
1125
+ headers: {
1126
+ "Content-Type": "application/json",
1127
+ Authorization: `Bearer ${token}`
1128
+ },
1129
+ body: JSON.stringify(report),
1130
+ signal: AbortSignal.timeout(this.timeout)
1131
+ });
1132
+ if (!response.ok) {
1133
+ const errorText = await response.text();
1134
+ logger.error("Failed to report coverage metrics", {
1135
+ status: response.status,
1136
+ error: errorText
1137
+ });
1138
+ return false;
1139
+ }
1140
+ logger.info("Coverage metrics reported successfully");
1141
+ return true;
1142
+ } catch (error) {
1143
+ logger.error("Error reporting coverage metrics", {
1144
+ error: String(error)
1145
+ });
1146
+ return false;
1147
+ }
1148
+ }
1149
+ /**
1150
+ * Health check - verify connection to admin API
1151
+ */
1152
+ async healthCheck() {
1153
+ const token = await this.getToken();
1154
+ if (!token)
1155
+ return false;
1156
+ try {
1157
+ const response = await fetch(
1158
+ `${this.baseUrl}/v1/admin/applications?type=code_agent&limit=1`,
1159
+ {
1160
+ method: "GET",
1161
+ headers: {
1162
+ "Content-Type": "application/json",
1163
+ Authorization: `Bearer ${token}`
1164
+ },
1165
+ signal: AbortSignal.timeout(5e3)
1166
+ }
1167
+ );
1168
+ const healthy = response.ok;
1169
+ logger.debug(`Health check: ${healthy ? "OK" : "FAILED"}`);
1170
+ return healthy;
1171
+ } catch (error) {
1172
+ logger.debug("Health check failed", {
1173
+ error: String(error)
1174
+ });
1175
+ return false;
1176
+ }
1177
+ }
1178
+ };
1179
+
1180
+ // src/module.ts
1181
+ init_utils();
1182
+
1183
+ // src/scanner.ts
1184
+ var import_child_process = require("child_process");
1185
+ var fs5 = __toESM(require("fs"));
1186
+ init_utils();
1187
+ init_config2();
1188
+ var platformLoader = require_platform_loader();
1189
+ var MCPScanner = class {
1190
+ constructor() {
1191
+ this.rampartsPath = null;
1192
+ this.rampartsPath = this.findRampartsBinary();
1193
+ }
1194
+ /**
1195
+ * Find the ramparts binary based on platform
1196
+ * Uses platform-loader for cross-platform discovery.
1197
+ *
1198
+ * Search order:
1199
+ * 1. Platform package (npm optionalDependency)
1200
+ * 2. Local bin/ directory (dev mode)
1201
+ * 3. ~/.overwatch/bin/
1202
+ * 4. System PATH
1203
+ */
1204
+ findRampartsBinary() {
1205
+ if (!platformLoader.isPlatformSupported()) {
1206
+ const key = platformLoader.getPlatformKey();
1207
+ logger.warn(`Unsupported platform: ${key}`);
1208
+ return null;
1209
+ }
1210
+ const binaryPath = platformLoader.getRampartsBinaryPath();
1211
+ if (binaryPath) {
1212
+ logger.info(`Found ramparts at: ${binaryPath}`);
1213
+ return binaryPath;
1214
+ }
1215
+ logger.warn("ramparts binary not found");
1216
+ logger.warn(platformLoader.getPlatformNotFoundMessage());
1217
+ return null;
1218
+ }
1219
+ /**
1220
+ * Check if scanner is available
1221
+ */
1222
+ isAvailable() {
1223
+ return this.rampartsPath !== null;
1224
+ }
1225
+ /**
1226
+ * Get the path to ramparts binary
1227
+ */
1228
+ getBinaryPath() {
1229
+ return this.rampartsPath;
1230
+ }
1231
+ /**
1232
+ * Run MCP config scan
1233
+ * @param rulesDir Optional custom YARA rules directory
1234
+ */
1235
+ async runScan(rulesDir) {
1236
+ if (!this.rampartsPath) {
1237
+ throw new Error("ramparts binary not found");
1238
+ }
1239
+ const args = ["scan-config", "--format", "json"];
1240
+ const env = {
1241
+ ...process.env,
1242
+ JAVELIN_BYPASS: "true"
1243
+ // Skip Javelin checks during scan
1244
+ };
1245
+ if (rulesDir && fs5.existsSync(rulesDir)) {
1246
+ env.RAMPARTS_RULES_DIR = rulesDir;
1247
+ logger.debug(`Using rules from: ${rulesDir}`);
1248
+ }
1249
+ const llmConfig = getLLMConfig();
1250
+ if (llmConfig?.apiKey) {
1251
+ env.LLM_API_KEY = llmConfig.apiKey;
1252
+ if (llmConfig.provider === "openai") {
1253
+ env.OPENAI_API_KEY = llmConfig.apiKey;
1254
+ } else if (llmConfig.provider === "anthropic") {
1255
+ env.ANTHROPIC_API_KEY = llmConfig.apiKey;
1256
+ }
1257
+ logger.debug(
1258
+ `LLM API key configured for ramparts (${llmConfig.provider || "openai"})`
1259
+ );
1260
+ }
1261
+ logger.info(`Running: ${this.rampartsPath} ${args.join(" ")}`);
1262
+ return new Promise((resolve, reject) => {
1263
+ const proc = (0, import_child_process.spawn)(this.rampartsPath, args, {
1264
+ env,
1265
+ stdio: ["ignore", "pipe", "pipe"]
1266
+ });
1267
+ let stdout = "";
1268
+ let stderr = "";
1269
+ proc.stdout?.on("data", (data) => {
1270
+ stdout += data.toString();
1271
+ });
1272
+ proc.stderr?.on("data", (data) => {
1273
+ stderr += data.toString();
1274
+ });
1275
+ proc.on("close", (code) => {
1276
+ if (code !== 0) {
1277
+ logger.warn(`ramparts exited with code ${code}`);
1278
+ if (stderr) {
1279
+ logger.debug(`stderr: ${stderr}`);
1280
+ }
1281
+ }
1282
+ try {
1283
+ const result = this.parseOutput(stdout);
1284
+ resolve(result);
1285
+ } catch (error) {
1286
+ const errorMsg = error instanceof Error ? error.message : String(error);
1287
+ logger.error(`Failed to parse output: ${errorMsg}`);
1288
+ reject(new Error(`Failed to parse scan output: ${errorMsg}`));
1289
+ }
1290
+ });
1291
+ proc.on("error", (err) => {
1292
+ logger.error(`Process error: ${err.message}`);
1293
+ reject(err);
1294
+ });
1295
+ setTimeout(() => {
1296
+ proc.kill();
1297
+ reject(new Error("Scan timed out after 600 seconds"));
1298
+ }, 6e5);
1299
+ });
1300
+ }
1301
+ /**
1302
+ * Parse ramparts CLI output
1303
+ * Handles cases where CLI prints banner/logs before JSON
1304
+ */
1305
+ parseOutput(output) {
1306
+ try {
1307
+ return JSON.parse(output);
1308
+ } catch {
1309
+ }
1310
+ const cleanOutput = output.replace(/\x1b\[[0-9;]*m/g, "");
1311
+ let searchIndex = cleanOutput.indexOf('"scan_type"');
1312
+ if (searchIndex === -1) {
1313
+ searchIndex = cleanOutput.indexOf('"results"');
1314
+ }
1315
+ if (searchIndex === -1) {
1316
+ logger.warn("Could not find JSON content in output");
1317
+ return this.getEmptyResult();
1318
+ }
1319
+ let startIndex = -1;
1320
+ for (let i = searchIndex; i >= 0; i--) {
1321
+ if (cleanOutput[i] === "{") {
1322
+ startIndex = i;
1323
+ break;
1324
+ }
1325
+ }
1326
+ if (startIndex !== -1) {
1327
+ let braceCount = 0;
1328
+ let endIndex = -1;
1329
+ for (let i = startIndex; i < cleanOutput.length; i++) {
1330
+ if (cleanOutput[i] === "{")
1331
+ braceCount++;
1332
+ else if (cleanOutput[i] === "}")
1333
+ braceCount--;
1334
+ if (braceCount === 0) {
1335
+ endIndex = i + 1;
1336
+ break;
1337
+ }
1338
+ }
1339
+ if (endIndex !== -1) {
1340
+ const jsonStr = cleanOutput.substring(startIndex, endIndex);
1341
+ try {
1342
+ return JSON.parse(jsonStr);
1343
+ } catch (error) {
1344
+ const errorMsg = error instanceof Error ? error.message : String(error);
1345
+ logger.warn(`Failed to parse extracted JSON: ${errorMsg}`);
1346
+ }
1347
+ }
1348
+ }
1349
+ logger.warn("Could not parse JSON, returning empty result");
1350
+ return this.getEmptyResult();
1351
+ }
1352
+ getEmptyResult() {
1353
+ return {
1354
+ scan_type: "mcp_config",
1355
+ total_servers: 0,
1356
+ results: []
1357
+ };
1358
+ }
1359
+ };
1360
+
1361
+ // src/module.ts
1362
+ init_version();
1363
+
1364
+ // src/cerberus/client.ts
1365
+ init_utils();
1366
+ init_version();
1367
+
1368
+ // src/handlers/utils.ts
1369
+ async function parseBody(req) {
1370
+ return new Promise((resolve) => {
1371
+ let body = "";
1372
+ req.on("data", (chunk) => {
1373
+ body += chunk.toString();
1374
+ });
1375
+ req.on("end", () => {
1376
+ try {
1377
+ resolve(JSON.parse(body || "{}"));
1378
+ } catch {
1379
+ resolve({});
1380
+ }
1381
+ });
1382
+ req.on("error", () => resolve({}));
1383
+ });
1384
+ }
1385
+ function getOfflineFailOpen(_source) {
1386
+ return {};
1387
+ }
1388
+
1389
+ // src/cerberus/client.ts
1390
+ var DEFAULT_TIMEOUT2 = 1e4;
1391
+ var CerberusClient = class {
1392
+ constructor(baseUrl, tokenGetter, machineId, timeout = DEFAULT_TIMEOUT2) {
1393
+ this.baseUrl = baseUrl.replace(/\/$/, "");
1394
+ this.tokenGetter = tokenGetter;
1395
+ this.machineId = machineId;
1396
+ this.timeout = timeout;
1397
+ logger.debug("CerberusClient initialized", {
1398
+ baseUrl: this.baseUrl,
1399
+ timeout: this.timeout
1400
+ });
1401
+ }
1402
+ /**
1403
+ * Forward a hook event to Cerberus for evaluation.
1404
+ * Returns the Cerberus response, or a fail-open allow on error.
1405
+ */
1406
+ async evaluate(req) {
1407
+ const url = `${this.baseUrl}/v1/hooks/evaluate`;
1408
+ const startTime = Date.now();
1409
+ const body = {
1410
+ source: req.source,
1411
+ event: req.event,
1412
+ payload: req.payload,
1413
+ client: {
1414
+ version: VERSION,
1415
+ machine_id: this.machineId,
1416
+ platform: process.platform,
1417
+ ...req.client
1418
+ }
1419
+ };
1420
+ const token = await this.tokenGetter();
1421
+ if (!token) {
1422
+ logger.warn("No auth token \u2014 allowing (fail-open)");
1423
+ return this.failOpen(req.source);
1424
+ }
1425
+ const headers = {
1426
+ "Content-Type": "application/json",
1427
+ Authorization: `Bearer ${token}`,
1428
+ "User-Agent": `overwatch/${VERSION}`
1429
+ };
1430
+ const requestBody = JSON.stringify(body);
1431
+ logger.info("Cerberus request", {
1432
+ url,
1433
+ source: req.source,
1434
+ event: req.event,
1435
+ bodySize: requestBody.length
1436
+ });
1437
+ logger.debug("Cerberus request body (shape)", {
1438
+ payloadKeys: Object.keys(body.payload || {}),
1439
+ bodySize: requestBody.length
1440
+ });
1441
+ try {
1442
+ const response = await fetch(url, {
1443
+ method: "POST",
1444
+ headers,
1445
+ body: requestBody,
1446
+ signal: AbortSignal.timeout(this.timeout)
1447
+ });
1448
+ const duration = Date.now() - startTime;
1449
+ const responseText = await response.text();
1450
+ logger.debug("Cerberus raw response", {
1451
+ status: response.status,
1452
+ duration: `${duration}ms`,
1453
+ body: responseText.substring(0, 1e3)
1454
+ });
1455
+ if (!response.ok) {
1456
+ logger.error("Cerberus returned error \u2014 allowing (fail-open)", {
1457
+ status: response.status,
1458
+ error: responseText.substring(0, 500),
1459
+ duration: `${duration}ms`
1460
+ });
1461
+ return this.failOpen(req.source);
1462
+ }
1463
+ const result = JSON.parse(responseText);
1464
+ logger.info(`Cerberus response in ${duration}ms`, {
1465
+ decision: result.decision,
1466
+ allowed: result.allowed,
1467
+ reason: result.reason,
1468
+ event_id: result.event_id,
1469
+ ide_response: result.ide_response
1470
+ });
1471
+ return result;
1472
+ } catch (error) {
1473
+ const duration = Date.now() - startTime;
1474
+ const errorMessage = error instanceof Error ? error.message : String(error);
1475
+ logger.error("Cerberus call failed \u2014 allowing (fail-open)", {
1476
+ error: errorMessage,
1477
+ duration: `${duration}ms`,
1478
+ url
1479
+ });
1480
+ return this.failOpen(req.source);
1481
+ }
1482
+ }
1483
+ failOpen(source) {
1484
+ return {
1485
+ allowed: true,
1486
+ decision: "allow",
1487
+ reason: "Cerberus unavailable \u2014 fail-open",
1488
+ ide_response: getOfflineFailOpen(source)
1489
+ };
1490
+ }
1491
+ };
1492
+
1493
+ // src/module.ts
1494
+ init_config2();
1495
+
1496
+ // src/data/ingestor.ts
1497
+ var fs6 = __toESM(require("fs"));
1498
+ var path5 = __toESM(require("path"));
1499
+ var os5 = __toESM(require("os"));
1500
+ init_utils();
1501
+ var MAX_RETRIES = 5;
1502
+ var RETRY_INTERVAL_MS = 3e4;
1503
+ var AdminIngestor = class {
1504
+ constructor(baseUrl, tokenGetter, endpoint, applicationIdResolver) {
1505
+ this.retryTimer = null;
1506
+ // Mid-flight guard so boot + interval calls don't overlap on the pending file.
1507
+ this.processing = false;
1508
+ this.applicationIdResolver = null;
1509
+ this.baseUrl = baseUrl;
1510
+ this.tokenGetter = tokenGetter;
1511
+ this.endpoint = endpoint;
1512
+ this.applicationIdResolver = applicationIdResolver || null;
1513
+ this.pendingFile = path5.join(
1514
+ os5.homedir(),
1515
+ ".overwatch",
1516
+ `pending-${endpoint}.jsonl`
1517
+ );
1518
+ this.deadLetterFile = path5.join(
1519
+ os5.homedir(),
1520
+ ".overwatch",
1521
+ `dead-letter-${endpoint}.jsonl`
1522
+ );
1523
+ logger.info(`Initialized`, {
1524
+ baseUrl: this.baseUrl,
1525
+ endpoint: this.endpoint,
1526
+ pendingFile: this.pendingFile,
1527
+ deadLetterFile: this.deadLetterFile
1528
+ });
1529
+ }
1530
+ start() {
1531
+ if (this.retryTimer) {
1532
+ logger.debug("Already started");
1533
+ return;
1534
+ }
1535
+ logger.info("Starting", {
1536
+ retryIntervalMs: RETRY_INTERVAL_MS
1537
+ });
1538
+ this.processQueue();
1539
+ this.retryTimer = setInterval(() => this.processQueue(), RETRY_INTERVAL_MS);
1540
+ }
1541
+ stop() {
1542
+ if (this.retryTimer) {
1543
+ logger.info("Stopping");
1544
+ clearInterval(this.retryTimer);
1545
+ this.retryTimer = null;
1546
+ }
1547
+ }
1548
+ /** Fire-and-forget record ingestion */
1549
+ ingest(record) {
1550
+ const recordObj = record;
1551
+ logger.debug("Ingesting record", {
1552
+ recordSize: JSON.stringify(record).length,
1553
+ recordKeys: Object.keys(recordObj),
1554
+ hasResults: "results" in recordObj,
1555
+ resultsType: recordObj.results ? Array.isArray(recordObj.results) ? "array" : typeof recordObj.results : "missing",
1556
+ resultsLength: Array.isArray(recordObj.results) ? recordObj.results.length : "N/A"
1557
+ });
1558
+ this.send(record).catch((error) => {
1559
+ logger.warn(
1560
+ `[AdminIngestor:${this.endpoint}] Initial send failed, saving for retry`,
1561
+ {
1562
+ error: String(error),
1563
+ errorMessage: error?.message
1564
+ }
1565
+ );
1566
+ this.saveForRetry(record, 0);
1567
+ });
1568
+ }
1569
+ async send(record) {
1570
+ const url = `${this.baseUrl}/v1/admin/guardian/${this.endpoint}`;
1571
+ const body = JSON.stringify(record);
1572
+ const token = await this.tokenGetter();
1573
+ if (!token) {
1574
+ logger.warn(`[AdminIngestor:${this.endpoint}] No valid auth token, skipping send`);
1575
+ return;
1576
+ }
1577
+ const applicationId = this.applicationIdResolver ? this.applicationIdResolver(record) : null;
1578
+ logger.debug("Sending record", {
1579
+ url,
1580
+ method: "POST",
1581
+ bodySize: body.length,
1582
+ hasApplicationId: !!applicationId
1583
+ });
1584
+ const headers = {
1585
+ "Content-Type": "application/json",
1586
+ Authorization: `Bearer ${token}`
1587
+ };
1588
+ try {
1589
+ const res = await fetch(url, {
1590
+ method: "POST",
1591
+ headers,
1592
+ body,
1593
+ signal: AbortSignal.timeout(3e4)
1594
+ });
1595
+ if (!res.ok) {
1596
+ const errorText = await res.text().catch(() => "");
1597
+ logger.error("Send failed", {
1598
+ status: res.status,
1599
+ statusText: res.statusText,
1600
+ errorBody: errorText.substring(0, 200),
1601
+ url
1602
+ });
1603
+ throw new Error(`${res.status} ${res.statusText}`);
1604
+ }
1605
+ logger.info("Successfully sent record", {
1606
+ status: res.status,
1607
+ url
1608
+ });
1609
+ } catch (error) {
1610
+ const errorObj = error;
1611
+ logger.error("Send error", {
1612
+ error: String(error),
1613
+ errorMessage: errorObj?.message,
1614
+ errorName: errorObj?.name,
1615
+ url
1616
+ });
1617
+ throw error;
1618
+ }
1619
+ }
1620
+ saveForRetry(record, retryCount) {
1621
+ try {
1622
+ this.ensureDir(this.pendingFile);
1623
+ fs6.appendFileSync(
1624
+ this.pendingFile,
1625
+ JSON.stringify({ record, retryCount }) + "\n"
1626
+ );
1627
+ logger.info("Saved record for retry", {
1628
+ retryCount,
1629
+ pendingFile: this.pendingFile
1630
+ });
1631
+ } catch (e) {
1632
+ logger.error(
1633
+ `[AdminIngestor:${this.endpoint}] Failed to save for retry`,
1634
+ {
1635
+ error: String(e),
1636
+ retryCount,
1637
+ pendingFile: this.pendingFile
1638
+ }
1639
+ );
1640
+ }
1641
+ }
1642
+ async processQueue() {
1643
+ if (this.processing) {
1644
+ logger.debug(
1645
+ `[AdminIngestor:${this.endpoint}] processQueue already running, skipping this tick`
1646
+ );
1647
+ return;
1648
+ }
1649
+ this.processing = true;
1650
+ try {
1651
+ await this.processQueueInner();
1652
+ } finally {
1653
+ this.processing = false;
1654
+ }
1655
+ }
1656
+ async processQueueInner() {
1657
+ if (!fs6.existsSync(this.pendingFile)) {
1658
+ logger.debug(
1659
+ `[AdminIngestor:${this.endpoint}] No pending file, skipping queue processing`
1660
+ );
1661
+ return;
1662
+ }
1663
+ const pending = this.loadPending();
1664
+ if (pending.length === 0) {
1665
+ logger.debug(
1666
+ `[AdminIngestor:${this.endpoint}] No pending records to process`
1667
+ );
1668
+ return;
1669
+ }
1670
+ logger.info("Processing queue", {
1671
+ pendingCount: pending.length
1672
+ });
1673
+ const remaining = [];
1674
+ for (const item of pending) {
1675
+ try {
1676
+ logger.debug("Retrying record", {
1677
+ retryCount: item.retryCount
1678
+ });
1679
+ await this.send(item.record);
1680
+ logger.info(
1681
+ `[AdminIngestor:${this.endpoint}] Successfully retried record`,
1682
+ {
1683
+ retryCount: item.retryCount
1684
+ }
1685
+ );
1686
+ } catch (e) {
1687
+ const newRetryCount = item.retryCount + 1;
1688
+ if (newRetryCount >= MAX_RETRIES) {
1689
+ logger.error(
1690
+ `[AdminIngestor:${this.endpoint}] Max retries reached, moving to dead letter`,
1691
+ {
1692
+ retryCount: item.retryCount,
1693
+ maxRetries: MAX_RETRIES,
1694
+ error: String(e)
1695
+ }
1696
+ );
1697
+ this.moveToDeadLetter(item, String(e));
1698
+ } else {
1699
+ logger.warn(
1700
+ `[AdminIngestor:${this.endpoint}] Retry failed, will retry again`,
1701
+ {
1702
+ retryCount: item.retryCount,
1703
+ newRetryCount,
1704
+ maxRetries: MAX_RETRIES,
1705
+ error: String(e)
1706
+ }
1707
+ );
1708
+ remaining.push({ record: item.record, retryCount: newRetryCount });
1709
+ }
1710
+ }
1711
+ }
1712
+ if (remaining.length > 0) {
1713
+ logger.info(
1714
+ `[AdminIngestor:${this.endpoint}] Queue processing complete, ${remaining.length} records remaining`,
1715
+ {
1716
+ remainingCount: remaining.length,
1717
+ processedCount: pending.length - remaining.length
1718
+ }
1719
+ );
1720
+ } else {
1721
+ logger.info(
1722
+ `[AdminIngestor:${this.endpoint}] Queue processing complete, all records processed`,
1723
+ {
1724
+ processedCount: pending.length
1725
+ }
1726
+ );
1727
+ }
1728
+ this.savePending(remaining);
1729
+ }
1730
+ loadPending() {
1731
+ try {
1732
+ const content = fs6.readFileSync(this.pendingFile, "utf-8");
1733
+ return content.split("\n").filter(Boolean).map((line) => {
1734
+ try {
1735
+ return JSON.parse(line);
1736
+ } catch {
1737
+ return null;
1738
+ }
1739
+ }).filter(Boolean);
1740
+ } catch {
1741
+ return [];
1742
+ }
1743
+ }
1744
+ savePending(items) {
1745
+ try {
1746
+ if (items.length === 0) {
1747
+ if (fs6.existsSync(this.pendingFile))
1748
+ fs6.unlinkSync(this.pendingFile);
1749
+ } else {
1750
+ fs6.writeFileSync(
1751
+ this.pendingFile,
1752
+ items.map((i) => JSON.stringify(i)).join("\n") + "\n"
1753
+ );
1754
+ }
1755
+ } catch (e) {
1756
+ logger.error("Failed to save pending", {
1757
+ error: String(e)
1758
+ });
1759
+ }
1760
+ }
1761
+ moveToDeadLetter(item, reason) {
1762
+ try {
1763
+ this.ensureDir(this.deadLetterFile);
1764
+ fs6.appendFileSync(
1765
+ this.deadLetterFile,
1766
+ JSON.stringify({
1767
+ ...item,
1768
+ reason,
1769
+ failedAt: (/* @__PURE__ */ new Date()).toISOString()
1770
+ }) + "\n"
1771
+ );
1772
+ logger.error(
1773
+ `[AdminIngestor:${this.endpoint}] Moved record to dead letter`,
1774
+ {
1775
+ retryCount: item.retryCount,
1776
+ reason,
1777
+ deadLetterFile: this.deadLetterFile
1778
+ }
1779
+ );
1780
+ } catch (e) {
1781
+ logger.error(
1782
+ `[AdminIngestor:${this.endpoint}] Failed to write dead letter`,
1783
+ {
1784
+ error: String(e),
1785
+ deadLetterFile: this.deadLetterFile
1786
+ }
1787
+ );
1788
+ }
1789
+ }
1790
+ ensureDir(filePath) {
1791
+ const dir = path5.dirname(filePath);
1792
+ if (!fs6.existsSync(dir))
1793
+ fs6.mkdirSync(dir, { recursive: true });
1794
+ }
1795
+ };
1796
+
1797
+ // src/data/installation-recorder.ts
1798
+ var InstallationRecorder = class {
1799
+ constructor(send) {
1800
+ this.send = send;
1801
+ this.seen = /* @__PURE__ */ new Set();
1802
+ this.inflight = /* @__PURE__ */ new Map();
1803
+ }
1804
+ async tryRecord(source, userEmail) {
1805
+ const key = `${userEmail}:${source}`;
1806
+ if (this.seen.has(key))
1807
+ return;
1808
+ let p = this.inflight.get(key);
1809
+ if (!p) {
1810
+ p = this.send(source, userEmail);
1811
+ this.inflight.set(key, p);
1812
+ void p.finally(() => {
1813
+ this.inflight.delete(key);
1814
+ }).catch(() => {
1815
+ });
1816
+ }
1817
+ try {
1818
+ const ok = await p;
1819
+ if (ok)
1820
+ this.seen.add(key);
1821
+ } catch {
1822
+ }
1823
+ }
1824
+ // Test-only accessors keyed as `${userEmail}:${source}`.
1825
+ has(key) {
1826
+ return this.seen.has(key);
1827
+ }
1828
+ inflightCount() {
1829
+ return this.inflight.size;
1830
+ }
1831
+ };
1832
+
1833
+ // src/pipeline/hook-pipeline.ts
1834
+ init_utils();
1835
+ var HookPipeline = class {
1836
+ constructor(cerberusClient, onFirstInstall) {
1837
+ this.cerberusClient = cerberusClient;
1838
+ this.onFirstInstall = onFirstInstall;
1839
+ }
1840
+ async process(source, event, input) {
1841
+ logger.debug("Pipeline processing", { source, event });
1842
+ try {
1843
+ this.onFirstInstall(source);
1844
+ } catch (err) {
1845
+ logger.error("onFirstInstall threw, continuing", {
1846
+ source,
1847
+ error: err instanceof Error ? err.message : String(err)
1848
+ });
1849
+ }
1850
+ try {
1851
+ const cerberusResponse = await this.cerberusClient.evaluate({
1852
+ source,
1853
+ event,
1854
+ payload: input
1855
+ });
1856
+ return {
1857
+ response: cerberusResponse.ide_response,
1858
+ allowed: cerberusResponse.allowed,
1859
+ decision: cerberusResponse.decision,
1860
+ reason: cerberusResponse.reason,
1861
+ eventId: cerberusResponse.event_id
1862
+ };
1863
+ } catch (error) {
1864
+ const errorMessage = error instanceof Error ? error.message : String(error);
1865
+ logger.error("Pipeline error \u2014 allowing (fail-open)", {
1866
+ error: errorMessage,
1867
+ source,
1868
+ event
1869
+ });
1870
+ return {
1871
+ response: getOfflineFailOpen(source),
1872
+ allowed: true,
1873
+ decision: "allow",
1874
+ reason: `Pipeline error: ${errorMessage}`
1875
+ };
1876
+ }
1877
+ }
1878
+ };
1879
+
1880
+ // src/module.ts
1881
+ init_token_store();
1882
+
1883
+ // src/handlers/hook-handler.ts
1884
+ init_utils();
1885
+
1886
+ // src/data/recorder.ts
1887
+ var path6 = __toESM(require("path"));
1888
+ var os6 = __toESM(require("os"));
1889
+ var fs7 = __toESM(require("fs"));
1890
+ var import_crypto = require("crypto");
1891
+ init_utils();
1892
+ var EVENTS_FILE = path6.join(os6.homedir(), ".overwatch", "events.jsonl");
1893
+ function getHookCategory(event) {
1894
+ if (event === "stop" || event === "SessionEnd")
1895
+ return "stop";
1896
+ if (event === "SessionStart")
1897
+ return "before";
1898
+ if (event.startsWith("after"))
1899
+ return "after";
1900
+ if (event === "PostToolUse" || event === "AfterTool")
1901
+ return "after";
1902
+ if (event === "UserPromptSubmit" || event === "PreToolUse" || event === "BeforeAgent" || event === "BeforeTool") {
1903
+ return "before";
1904
+ }
1905
+ return "before";
1906
+ }
1907
+ function recordEvent(source, event, input, cerberusResponse, totalDuration) {
1908
+ const record = {
1909
+ id: cerberusResponse.event_id || (0, import_crypto.randomUUID)(),
1910
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1911
+ source,
1912
+ event,
1913
+ hook_category: getHookCategory(event),
1914
+ input,
1915
+ allowed: cerberusResponse.allowed,
1916
+ decision: cerberusResponse.decision,
1917
+ reason: cerberusResponse.reason,
1918
+ response: cerberusResponse.ide_response,
1919
+ total_duration_ms: totalDuration
1920
+ };
1921
+ try {
1922
+ const dir = path6.dirname(EVENTS_FILE);
1923
+ if (!fs7.existsSync(dir)) {
1924
+ fs7.mkdirSync(dir, { recursive: true });
1925
+ }
1926
+ fs7.appendFile(EVENTS_FILE, JSON.stringify(record) + "\n", (err) => {
1927
+ if (err) {
1928
+ logger.error("Failed to write event:", { error: String(err) });
1929
+ return;
1930
+ }
1931
+ logger.debug("Recorded:", {
1932
+ id: record.id,
1933
+ event,
1934
+ allowed: record.allowed,
1935
+ decision: record.decision
1936
+ });
1937
+ });
1938
+ } catch (error) {
1939
+ logger.error("Failed to write event:", { error: String(error) });
1940
+ }
1941
+ return record;
1942
+ }
1943
+
1944
+ // src/handlers/hook-handler.ts
1945
+ var HOOK_SOURCE_ALLOWLIST = /* @__PURE__ */ new Set([
1946
+ "cursor",
1947
+ "claudecode",
1948
+ "github_copilot",
1949
+ "gemini_cli",
1950
+ "codex",
1951
+ "langgraph",
1952
+ "pydantic_ai"
1953
+ ]);
1954
+ var HookHandler = class {
1955
+ constructor(pipeline) {
1956
+ this.pipeline = pipeline;
1957
+ this.eventCount = 0;
1958
+ }
1959
+ /**
1960
+ * Set callback for project skills scanning
1961
+ */
1962
+ setProjectSkillsScanCallback(callback) {
1963
+ this.onProjectSkillsScan = callback;
1964
+ }
1965
+ getEventCount() {
1966
+ return this.eventCount;
1967
+ }
1968
+ async handle(req, res) {
1969
+ const startTime = Date.now();
1970
+ const url = new URL(req.url || "", `http://${req.headers.host}`);
1971
+ const pathParts = url.pathname.split("/").filter(Boolean);
1972
+ const sourceRaw = pathParts[1];
1973
+ const eventRaw = pathParts[2];
1974
+ if (!sourceRaw || !eventRaw) {
1975
+ res.writeHead(400, { "Content-Type": "application/json" });
1976
+ res.end(
1977
+ JSON.stringify({
1978
+ error: "Missing source or event in path",
1979
+ expected: "/hook/<source>/<event>"
1980
+ })
1981
+ );
1982
+ return;
1983
+ }
1984
+ if (!HOOK_SOURCE_ALLOWLIST.has(sourceRaw)) {
1985
+ res.writeHead(400, { "Content-Type": "application/json" });
1986
+ res.end(
1987
+ JSON.stringify({
1988
+ error: `Unsupported source: ${sourceRaw}`,
1989
+ supported: Array.from(HOOK_SOURCE_ALLOWLIST)
1990
+ })
1991
+ );
1992
+ return;
1993
+ }
1994
+ const source = sourceRaw;
1995
+ const event = eventRaw;
1996
+ logger.info(`${source}/${event} received`);
1997
+ const body = await parseBody(req);
1998
+ const result = await this.pipeline.process(source, event, body);
1999
+ this.eventCount++;
2000
+ const duration = Date.now() - startTime;
2001
+ const cerberusResponse = {
2002
+ allowed: result.allowed,
2003
+ decision: result.decision,
2004
+ reason: result.reason,
2005
+ ide_response: result.response,
2006
+ event_id: result.eventId
2007
+ };
2008
+ logger.info(
2009
+ `${source}/${event} completed in ${duration}ms`,
2010
+ result.response
2011
+ );
2012
+ res.writeHead(200, { "Content-Type": "application/json" });
2013
+ res.end(JSON.stringify(result.response));
2014
+ recordEvent(
2015
+ source,
2016
+ event,
2017
+ body,
2018
+ cerberusResponse,
2019
+ duration
2020
+ );
2021
+ const workspace = this.extractWorkspace(body);
2022
+ if (workspace && this.onProjectSkillsScan) {
2023
+ try {
2024
+ this.onProjectSkillsScan(workspace);
2025
+ } catch (error) {
2026
+ logger.error("Error in project skills scan callback", {
2027
+ workspace,
2028
+ error: String(error)
2029
+ });
2030
+ }
2031
+ }
2032
+ }
2033
+ /**
2034
+ * Extract workspace path from event body
2035
+ */
2036
+ extractWorkspace(body) {
2037
+ if (body && typeof body === "object" && body !== null) {
2038
+ const input = body;
2039
+ if (input.workspace_roots && Array.isArray(input.workspace_roots)) {
2040
+ const roots = input.workspace_roots;
2041
+ if (roots.length > 0 && typeof roots[0] === "string" && roots[0].length > 0) {
2042
+ return roots[0];
2043
+ }
2044
+ }
2045
+ }
2046
+ const workspaceFields = ["cwd", "workingDirectory", "workspace", "directory", "path"];
2047
+ for (const field of workspaceFields) {
2048
+ const value = body[field];
2049
+ if (typeof value === "string" && value.length > 0) {
2050
+ return value;
2051
+ }
2052
+ }
2053
+ if (body.context && typeof body.context === "object" && body.context !== null) {
2054
+ const context = body.context;
2055
+ for (const field of workspaceFields) {
2056
+ const value = context[field];
2057
+ if (typeof value === "string" && value.length > 0) {
2058
+ return value;
2059
+ }
2060
+ }
2061
+ }
2062
+ return null;
2063
+ }
2064
+ };
2065
+
2066
+ // src/handlers/scan-handler.ts
2067
+ var fs8 = __toESM(require("fs"));
2068
+ var path7 = __toESM(require("path"));
2069
+ var os7 = __toESM(require("os"));
2070
+ var crypto2 = __toESM(require("crypto"));
2071
+ init_utils();
2072
+ var SCANS_FILE = path7.join(os7.homedir(), ".overwatch", "scans.jsonl");
2073
+ var ScanHandler = class {
2074
+ constructor(scanner, ingestor) {
2075
+ this.scanner = scanner;
2076
+ this.ingestor = ingestor;
2077
+ }
2078
+ async handle(req, res) {
2079
+ const body = await parseBody(req);
2080
+ if (body.action === "run") {
2081
+ logger.info("Running internal scan...");
2082
+ if (!this.scanner.isAvailable()) {
2083
+ res.writeHead(503, { "Content-Type": "application/json" });
2084
+ res.end(
2085
+ JSON.stringify({ status: "error", error: "Scanner not available" })
2086
+ );
2087
+ return;
2088
+ }
2089
+ try {
2090
+ const rulesDir = body.rulesDir;
2091
+ const results = await this.scanner.runScan(rulesDir);
2092
+ this.recordScan(results);
2093
+ res.writeHead(200, { "Content-Type": "application/json" });
2094
+ res.end(JSON.stringify({ status: "ok", ...results }));
2095
+ } catch (error) {
2096
+ const errorMsg = error instanceof Error ? error.message : String(error);
2097
+ logger.error("Internal scan failed:", { error: errorMsg });
2098
+ res.writeHead(500, { "Content-Type": "application/json" });
2099
+ res.end(JSON.stringify({ status: "error", error: errorMsg }));
2100
+ }
2101
+ return;
2102
+ }
2103
+ logger.info("Received external scan results");
2104
+ this.recordScan(body);
2105
+ res.writeHead(200, { "Content-Type": "application/json" });
2106
+ res.end(JSON.stringify({ status: "ok" }));
2107
+ }
2108
+ // Public so module.ts can use it for initial scan
2109
+ async runInitialScan() {
2110
+ if (!this.scanner.isAvailable()) {
2111
+ logger.warn("Scanner not available, skipping initial MCP scan");
2112
+ return;
2113
+ }
2114
+ try {
2115
+ logger.info("Running initial MCP scan...");
2116
+ const results = await this.scanner.runScan();
2117
+ this.recordScan(results);
2118
+ logger.info("Initial MCP scan completed");
2119
+ } catch (error) {
2120
+ logger.error("Initial MCP scan failed:", { error: String(error) });
2121
+ }
2122
+ }
2123
+ recordScan(input) {
2124
+ const results = input.results || [];
2125
+ const totalServers = input.total_servers || results.length;
2126
+ let totalIssues = 0;
2127
+ let maxSeverity;
2128
+ const severityOrder = {
2129
+ low: 1,
2130
+ medium: 2,
2131
+ high: 3,
2132
+ critical: 4
2133
+ };
2134
+ for (const result of results) {
2135
+ const yaraResults = result.yara_results || [];
2136
+ for (const yr of yaraResults) {
2137
+ if (yr.status === "warning") {
2138
+ totalIssues++;
2139
+ const metadata = yr.rule_metadata;
2140
+ const sev = (metadata?.severity || "low").toLowerCase();
2141
+ if (!maxSeverity || (severityOrder[sev] || 0) > (severityOrder[maxSeverity] || 0)) {
2142
+ maxSeverity = sev;
2143
+ }
2144
+ }
2145
+ }
2146
+ }
2147
+ const source = results[0]?.ide_source || "cursor";
2148
+ const record = {
2149
+ id: crypto2.randomUUID(),
2150
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2151
+ source,
2152
+ total_servers: totalServers,
2153
+ total_issues: totalIssues,
2154
+ max_severity: maxSeverity,
2155
+ raw: input
2156
+ };
2157
+ try {
2158
+ const dir = path7.dirname(SCANS_FILE);
2159
+ if (!fs8.existsSync(dir))
2160
+ fs8.mkdirSync(dir, { recursive: true });
2161
+ fs8.appendFileSync(SCANS_FILE, JSON.stringify(record) + "\n");
2162
+ logger.info("Recorded:", {
2163
+ id: record.id,
2164
+ servers: totalServers,
2165
+ issues: totalIssues
2166
+ });
2167
+ const serverPayload = {
2168
+ id: record.id,
2169
+ timestamp: record.timestamp,
2170
+ source: record.source,
2171
+ total_servers: record.total_servers,
2172
+ total_issues: record.total_issues,
2173
+ max_severity: record.max_severity,
2174
+ results: input.results,
2175
+ scan_type: input.scan_type,
2176
+ ...input.total_servers !== void 0 && {
2177
+ total_servers_raw: input.total_servers
2178
+ }
2179
+ };
2180
+ logger.debug("Ingesting scan with results at top level", {
2181
+ id: record.id,
2182
+ resultsCount: Array.isArray(input.results) ? input.results.length : 0
2183
+ });
2184
+ if (results.length === 0) {
2185
+ logger.info("Skipping ingestion: no scan results to send");
2186
+ } else {
2187
+ this.ingestor?.ingest(serverPayload);
2188
+ }
2189
+ } catch (error) {
2190
+ logger.error("Failed to write scan:", { error: String(error) });
2191
+ }
2192
+ }
2193
+ };
2194
+
2195
+ // src/handlers/oauth-handler.ts
2196
+ init_utils();
2197
+ var OAuthHandler = class {
2198
+ constructor(port, pendingOAuthStates, oauthStateTimeout) {
2199
+ this.port = port;
2200
+ this.pendingOAuthStates = pendingOAuthStates;
2201
+ this.oauthStateTimeout = oauthStateTimeout;
2202
+ }
2203
+ async handleStart(req, res) {
2204
+ if (req.method !== "POST") {
2205
+ res.writeHead(405, { "Content-Type": "application/json" });
2206
+ res.end(JSON.stringify({ error: "Method not allowed" }));
2207
+ return;
2208
+ }
2209
+ const oauthState = createOAuthState(this.port);
2210
+ const codeChallenge = generateCodeChallenge(oauthState.codeVerifier);
2211
+ const authUrl = buildAuthorizationUrl(
2212
+ oauthState.state,
2213
+ codeChallenge,
2214
+ oauthState.redirectUri
2215
+ );
2216
+ this.pendingOAuthStates.set(oauthState.state, oauthState);
2217
+ setTimeout(() => {
2218
+ const state = this.pendingOAuthStates.get(oauthState.state);
2219
+ if (state && state.status === "pending") {
2220
+ state.status = "expired";
2221
+ }
2222
+ setTimeout(() => {
2223
+ this.pendingOAuthStates.delete(oauthState.state);
2224
+ }, 3e4);
2225
+ }, this.oauthStateTimeout);
2226
+ logger.info("Started new auth flow", { state: oauthState.state });
2227
+ res.writeHead(200, { "Content-Type": "application/json" });
2228
+ res.end(
2229
+ JSON.stringify({
2230
+ state: oauthState.state,
2231
+ auth_url: authUrl
2232
+ })
2233
+ );
2234
+ }
2235
+ async handleCallback(req, res) {
2236
+ const url = new URL(req.url || "", `http://${req.headers.host}`);
2237
+ const code = url.searchParams.get("code");
2238
+ const state = url.searchParams.get("state");
2239
+ const error = url.searchParams.get("error");
2240
+ const errorDescription = url.searchParams.get("error_description");
2241
+ if (error) {
2242
+ logger.error("Auth error:", { error, errorDescription });
2243
+ res.writeHead(400, { "Content-Type": "text/html" });
2244
+ res.end(generateErrorHtml(errorDescription || error));
2245
+ return;
2246
+ }
2247
+ if (!state || !code) {
2248
+ logger.error("Missing state or code parameter");
2249
+ res.writeHead(400, { "Content-Type": "text/html" });
2250
+ res.end(generateErrorHtml("Missing required parameters"));
2251
+ return;
2252
+ }
2253
+ const oauthState = this.pendingOAuthStates.get(state);
2254
+ if (!oauthState) {
2255
+ logger.error("Unknown or expired state:", { state });
2256
+ res.writeHead(400, { "Content-Type": "text/html" });
2257
+ res.end(generateErrorHtml("Invalid or expired login session"));
2258
+ return;
2259
+ }
2260
+ if (oauthState.status !== "pending") {
2261
+ logger.error("State not pending:", { state, status: oauthState.status });
2262
+ res.writeHead(400, { "Content-Type": "text/html" });
2263
+ res.end(generateErrorHtml("Login session already processed"));
2264
+ return;
2265
+ }
2266
+ try {
2267
+ const { tokens, userInfo: userInfo2 } = await exchangeCodeForTokens(
2268
+ code,
2269
+ oauthState.codeVerifier,
2270
+ oauthState.redirectUri
2271
+ );
2272
+ saveTokens(tokens, userInfo2);
2273
+ oauthState.status = "completed";
2274
+ oauthState.tokens = tokens;
2275
+ oauthState.userInfo = userInfo2;
2276
+ logger.info("Login successful", { email: userInfo2?.email });
2277
+ res.writeHead(200, { "Content-Type": "text/html" });
2278
+ res.end(
2279
+ generateSuccessHtml(
2280
+ "You can close this window and return to the terminal.",
2281
+ userInfo2?.email
2282
+ )
2283
+ );
2284
+ } catch (err) {
2285
+ const errorMsg = err instanceof Error ? err.message : String(err);
2286
+ logger.error("Token exchange failed:", { error: errorMsg });
2287
+ oauthState.status = "failed";
2288
+ oauthState.error = errorMsg;
2289
+ res.writeHead(400, { "Content-Type": "text/html" });
2290
+ res.end(generateErrorHtml(errorMsg));
2291
+ }
2292
+ }
2293
+ async handleStatus(req, res) {
2294
+ const url = new URL(req.url || "", `http://${req.headers.host}`);
2295
+ const state = url.searchParams.get("state");
2296
+ if (!state) {
2297
+ res.writeHead(400, { "Content-Type": "application/json" });
2298
+ res.end(JSON.stringify({ error: "Missing state parameter" }));
2299
+ return;
2300
+ }
2301
+ const oauthState = this.pendingOAuthStates.get(state);
2302
+ if (!oauthState) {
2303
+ res.writeHead(404, { "Content-Type": "application/json" });
2304
+ res.end(JSON.stringify({ error: "Unknown state", status: "unknown" }));
2305
+ return;
2306
+ }
2307
+ res.writeHead(200, { "Content-Type": "application/json" });
2308
+ res.end(
2309
+ JSON.stringify({
2310
+ status: oauthState.status,
2311
+ user: oauthState.userInfo,
2312
+ error: oauthState.error
2313
+ })
2314
+ );
2315
+ if (oauthState.status !== "pending") {
2316
+ this.pendingOAuthStates.delete(state);
2317
+ }
2318
+ }
2319
+ };
2320
+
2321
+ // src/skills/scanner.ts
2322
+ var fs9 = __toESM(require("fs/promises"));
2323
+ var path8 = __toESM(require("path"));
2324
+ var os8 = __toESM(require("os"));
2325
+ var crypto3 = __toESM(require("crypto"));
2326
+ init_utils();
2327
+ var CLAUDE_SKILLS_DIR = path8.join(os8.homedir(), ".claude", "skills");
2328
+ var CURSOR_SKILLS_DIR = path8.join(os8.homedir(), ".cursor", "skills");
2329
+ var SkillsScanner = class {
2330
+ /**
2331
+ * Scan personal skills from both directories:
2332
+ * - ~/.claude/skills/
2333
+ * - ~/.cursor/skills/
2334
+ * Returns current state
2335
+ */
2336
+ async scan() {
2337
+ const startTime = Date.now();
2338
+ logger.info("Starting skills scan", {
2339
+ directories: [CLAUDE_SKILLS_DIR, CURSOR_SKILLS_DIR]
2340
+ });
2341
+ const [claudeSkills, cursorSkills] = await Promise.all([
2342
+ this.scanDirectory(CLAUDE_SKILLS_DIR),
2343
+ this.scanDirectory(CURSOR_SKILLS_DIR)
2344
+ ]);
2345
+ const skills = [...claudeSkills, ...cursorSkills];
2346
+ const scanDurationMs = Date.now() - startTime;
2347
+ const result = {
2348
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2349
+ totalSkills: skills.length,
2350
+ skills,
2351
+ scanDurationMs
2352
+ };
2353
+ logger.info("Skills scan completed", {
2354
+ totalSkills: skills.length,
2355
+ durationMs: scanDurationMs
2356
+ });
2357
+ return result;
2358
+ }
2359
+ /**
2360
+ * Scan skills directory
2361
+ */
2362
+ async scanDirectory(dir) {
2363
+ const skills = [];
2364
+ if (!await this.exists(dir)) {
2365
+ logger.debug("Skills directory does not exist", { dir });
2366
+ return skills;
2367
+ }
2368
+ try {
2369
+ const entries = await fs9.readdir(dir, { withFileTypes: true });
2370
+ for (const entry of entries) {
2371
+ if (!entry.isDirectory())
2372
+ continue;
2373
+ const skillDir = path8.join(dir, entry.name);
2374
+ const content = await this.readSkillContent(skillDir);
2375
+ if (content === null) {
2376
+ continue;
2377
+ }
2378
+ const files = await this.listSkillFiles(skillDir);
2379
+ const contentHash = this.hashContent(content);
2380
+ skills.push({
2381
+ name: entry.name,
2382
+ path: skillDir,
2383
+ content,
2384
+ contentHash,
2385
+ files
2386
+ });
2387
+ }
2388
+ } catch (error) {
2389
+ logger.error("Failed to scan skills directory", {
2390
+ dir,
2391
+ error: String(error)
2392
+ });
2393
+ }
2394
+ return skills;
2395
+ }
2396
+ /**
2397
+ * Check if a path exists
2398
+ */
2399
+ async exists(filePath) {
2400
+ try {
2401
+ await fs9.access(filePath);
2402
+ return true;
2403
+ } catch {
2404
+ return false;
2405
+ }
2406
+ }
2407
+ /**
2408
+ * Read full SKILL.md content
2409
+ */
2410
+ async readSkillContent(skillDir) {
2411
+ const skillFile = path8.join(skillDir, "SKILL.md");
2412
+ if (!await this.exists(skillFile)) {
2413
+ return null;
2414
+ }
2415
+ try {
2416
+ return await fs9.readFile(skillFile, "utf-8");
2417
+ } catch (error) {
2418
+ logger.error("Failed to read skill file", {
2419
+ skillFile,
2420
+ error: String(error)
2421
+ });
2422
+ return null;
2423
+ }
2424
+ }
2425
+ /**
2426
+ * List files in skill directory (recursive)
2427
+ */
2428
+ async listSkillFiles(skillDir) {
2429
+ const files = [];
2430
+ try {
2431
+ const listFiles = async (dir, prefix = "") => {
2432
+ const entries = await fs9.readdir(dir, { withFileTypes: true });
2433
+ for (const entry of entries) {
2434
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
2435
+ if (entry.isDirectory()) {
2436
+ await listFiles(path8.join(dir, entry.name), relativePath);
2437
+ } else {
2438
+ files.push(relativePath);
2439
+ }
2440
+ }
2441
+ };
2442
+ await listFiles(skillDir);
2443
+ } catch (error) {
2444
+ logger.error("Failed to list skill files", {
2445
+ skillDir,
2446
+ error: String(error)
2447
+ });
2448
+ }
2449
+ return files;
2450
+ }
2451
+ /**
2452
+ * Hash content for backend deduplication
2453
+ */
2454
+ hashContent(content) {
2455
+ const hash = crypto3.createHash("sha256");
2456
+ hash.update(content);
2457
+ return `sha256:${hash.digest("hex")}`;
2458
+ }
2459
+ /**
2460
+ * Scan project-level skills from:
2461
+ * - {workspace}/.claude/skills/
2462
+ * - {workspace}/.cursor/skills/
2463
+ */
2464
+ async scanProjectSkills(workspace) {
2465
+ const startTime = Date.now();
2466
+ const claudeSkillsDir = path8.join(workspace, ".claude", "skills");
2467
+ const cursorSkillsDir = path8.join(workspace, ".cursor", "skills");
2468
+ logger.info("Starting project skills scan", {
2469
+ workspace,
2470
+ directories: [claudeSkillsDir, cursorSkillsDir]
2471
+ });
2472
+ const [claudeSkills, cursorSkills] = await Promise.all([
2473
+ this.scanDirectory(claudeSkillsDir),
2474
+ this.scanDirectory(cursorSkillsDir)
2475
+ ]);
2476
+ const skills = [...claudeSkills, ...cursorSkills];
2477
+ const scanDurationMs = Date.now() - startTime;
2478
+ const result = {
2479
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2480
+ totalSkills: skills.length,
2481
+ skills,
2482
+ scanDurationMs,
2483
+ workspace
2484
+ };
2485
+ logger.info("Project skills scan completed", {
2486
+ workspace,
2487
+ totalSkills: skills.length,
2488
+ durationMs: scanDurationMs
2489
+ });
2490
+ return result;
2491
+ }
2492
+ };
2493
+
2494
+ // src/coverage/detector.ts
2495
+ var fs12 = __toESM(require("fs"));
2496
+ var path12 = __toESM(require("path"));
2497
+
2498
+ // src/installer.ts
2499
+ var fs11 = __toESM(require("fs"));
2500
+ var path10 = __toESM(require("path"));
2501
+ var os10 = __toESM(require("os"));
2502
+ init_version();
2503
+
2504
+ // src/utils/port-manager.ts
2505
+ var net = __toESM(require("net"));
2506
+ var fs10 = __toESM(require("fs"));
2507
+ var os9 = __toESM(require("os"));
2508
+ var path9 = __toESM(require("path"));
2509
+ var GUARDIAN_PORT = 17580;
2510
+ var PID_FILE_PATH = path9.join(
2511
+ os9.homedir(),
2512
+ ".overwatch",
2513
+ "guardian.pid"
2514
+ );
2515
+ var PortManager = class _PortManager {
2516
+ static async isPortAvailable(port) {
2517
+ return new Promise((resolve) => {
2518
+ const server = net.createServer();
2519
+ server.once("error", () => resolve(false));
2520
+ server.once("listening", () => {
2521
+ server.close(() => resolve(true));
2522
+ });
2523
+ server.listen(port, "127.0.0.1");
2524
+ });
2525
+ }
2526
+ static writePidFile(pid = process.pid) {
2527
+ const dir = path9.dirname(PID_FILE_PATH);
2528
+ if (!fs10.existsSync(dir)) {
2529
+ fs10.mkdirSync(dir, { recursive: true });
2530
+ }
2531
+ fs10.writeFileSync(PID_FILE_PATH, String(pid));
2532
+ }
2533
+ static readPidFile() {
2534
+ try {
2535
+ const raw = fs10.readFileSync(PID_FILE_PATH, "utf-8").trim();
2536
+ const pid = parseInt(raw, 10);
2537
+ if (!Number.isFinite(pid) || pid <= 0) {
2538
+ return null;
2539
+ }
2540
+ return pid;
2541
+ } catch {
2542
+ return null;
2543
+ }
2544
+ }
2545
+ static removePidFile() {
2546
+ try {
2547
+ fs10.unlinkSync(PID_FILE_PATH);
2548
+ } catch {
2549
+ }
2550
+ }
2551
+ static isProcessAlive(pid) {
2552
+ try {
2553
+ process.kill(pid, 0);
2554
+ return true;
2555
+ } catch (err) {
2556
+ return err?.code === "EPERM";
2557
+ }
2558
+ }
2559
+ /**
2560
+ * If GUARDIAN_PORT is held by a process whose PID matches our pidfile, send
2561
+ * SIGTERM (then SIGKILL) and wait for the port to free. Returns:
2562
+ * - "free": port was already available; nothing to do
2563
+ * - "killed": port was held by our pidfile owner; we killed it and the port is now free
2564
+ * - "foreign": port is held by something we don't own (or pidfile is stale and the holder is unknown)
2565
+ */
2566
+ static async reconcileStaleDaemon() {
2567
+ if (await _PortManager.isPortAvailable(GUARDIAN_PORT)) {
2568
+ const stalePid = _PortManager.readPidFile();
2569
+ if (stalePid !== null && !_PortManager.isProcessAlive(stalePid)) {
2570
+ _PortManager.removePidFile();
2571
+ }
2572
+ return { kind: "free" };
2573
+ }
2574
+ const pid = _PortManager.readPidFile();
2575
+ if (pid === null || !_PortManager.isProcessAlive(pid)) {
2576
+ return { kind: "foreign" };
2577
+ }
2578
+ try {
2579
+ process.kill(pid, "SIGTERM");
2580
+ } catch {
2581
+ }
2582
+ const sigtermDeadline = Date.now() + 5e3;
2583
+ while (Date.now() < sigtermDeadline) {
2584
+ await new Promise((resolve) => setTimeout(resolve, 100));
2585
+ if (await _PortManager.isPortAvailable(GUARDIAN_PORT)) {
2586
+ _PortManager.removePidFile();
2587
+ return { kind: "killed", pid };
2588
+ }
2589
+ }
2590
+ try {
2591
+ process.kill(pid, "SIGKILL");
2592
+ } catch {
2593
+ }
2594
+ const sigkillDeadline = Date.now() + 2e3;
2595
+ while (Date.now() < sigkillDeadline) {
2596
+ await new Promise((resolve) => setTimeout(resolve, 100));
2597
+ if (await _PortManager.isPortAvailable(GUARDIAN_PORT)) {
2598
+ _PortManager.removePidFile();
2599
+ return { kind: "killed", pid };
2600
+ }
2601
+ }
2602
+ return { kind: "foreign" };
2603
+ }
2604
+ };
2605
+
2606
+ // src/installer.ts
2607
+ var IDE_REGISTRY = {
2608
+ cursor: {
2609
+ hooksLocation: path10.join(os10.homedir(), ".cursor", "hooks.json"),
2610
+ events: [
2611
+ "beforeSubmitPrompt",
2612
+ "beforeShellExecution",
2613
+ "beforeMCPExecution",
2614
+ "beforeReadFile",
2615
+ "afterShellExecution",
2616
+ "afterMCPExecution",
2617
+ "afterFileEdit",
2618
+ "afterAgentResponse",
2619
+ "afterAgentThought",
2620
+ "stop"
2621
+ ],
2622
+ installMethod: "native"
2623
+ },
2624
+ claudecode: {
2625
+ hooksLocation: path10.join(os10.homedir(), ".claude", "settings.json"),
2626
+ events: ["UserPromptSubmit", "PreToolUse", "PostToolUse"],
2627
+ installMethod: "native"
2628
+ // Writes hooks to ~/.claude/settings.json
2629
+ },
2630
+ github_copilot: {
2631
+ hooksLocation: path10.join(os10.homedir(), ".overwatch", "github_copilot", "hooks.json"),
2632
+ events: ["sessionStart", "sessionEnd", "userPromptSubmitted", "preToolUse", "postToolUse", "errorOccurred"],
2633
+ installMethod: "native"
2634
+ },
2635
+ gemini_cli: {
2636
+ hooksLocation: path10.join(os10.homedir(), ".gemini", "settings.json"),
2637
+ events: [
2638
+ "SessionStart",
2639
+ "SessionEnd",
2640
+ "BeforeAgent",
2641
+ "AfterAgent",
2642
+ "BeforeModel",
2643
+ "AfterModel",
2644
+ "BeforeToolSelection",
2645
+ "BeforeTool",
2646
+ "AfterTool",
2647
+ "PreCompress",
2648
+ "Notification"
2649
+ ],
2650
+ installMethod: "native"
2651
+ },
2652
+ codex: {
2653
+ hooksLocation: path10.join(os10.homedir(), ".codex", "hooks.json"),
2654
+ events: [
2655
+ "SessionStart",
2656
+ "PreToolUse",
2657
+ "PermissionRequest",
2658
+ "PostToolUse",
2659
+ "UserPromptSubmit",
2660
+ "Stop"
2661
+ ],
2662
+ installMethod: "native"
2663
+ }
2664
+ };
2665
+ var SUPPORTED_IDES = [
2666
+ "cursor",
2667
+ "claudecode",
2668
+ "github_copilot",
2669
+ "gemini_cli",
2670
+ "codex"
2671
+ ];
2672
+ var OVERWATCH_META_KEY = "_overwatch";
2673
+ async function listInstalledHooks() {
2674
+ const results = [];
2675
+ for (const ide of SUPPORTED_IDES) {
2676
+ const ideConfig = IDE_REGISTRY[ide];
2677
+ const hooksLocation = ideConfig.hooksLocation;
2678
+ if (fs11.existsSync(hooksLocation)) {
2679
+ try {
2680
+ const content = fs11.readFileSync(hooksLocation, "utf-8");
2681
+ const hasOverwatchHooks = content.includes(`"${OVERWATCH_META_KEY}"`) || content.includes("universal-hook.sh") || content.includes("universal-hook.ps1");
2682
+ results.push({
2683
+ ide,
2684
+ hooksLocation,
2685
+ hooksInstalled: hasOverwatchHooks
2686
+ });
2687
+ } catch {
2688
+ results.push({
2689
+ ide,
2690
+ hooksLocation,
2691
+ hooksInstalled: false
2692
+ });
2693
+ }
2694
+ }
2695
+ }
2696
+ return results;
2697
+ }
2698
+
2699
+ // src/coverage/detector.ts
2700
+ init_utils();
2701
+
2702
+ // src/coverage/definitions.ts
2703
+ var path11 = __toESM(require("path"));
2704
+ var os11 = __toESM(require("os"));
2705
+ var HOME = os11.homedir();
2706
+ var VSCODE_EXTENSIONS = {
2707
+ darwin: path11.join(HOME, ".vscode", "extensions"),
2708
+ linux: path11.join(HOME, ".vscode", "extensions"),
2709
+ win32: path11.join(HOME, ".vscode", "extensions")
2710
+ };
2711
+ var JETBRAINS_CONFIG = {
2712
+ darwin: path11.join(HOME, "Library", "Application Support", "JetBrains"),
2713
+ linux: path11.join(HOME, ".config", "JetBrains"),
2714
+ win32: path11.join(HOME, "AppData", "Roaming", "JetBrains")
2715
+ };
2716
+ var AGENT_DEFINITIONS = [
2717
+ // ============================================
2718
+ // AI-Native IDEs
2719
+ // ============================================
2720
+ {
2721
+ id: "cursor",
2722
+ name: "Cursor",
2723
+ category: "ide",
2724
+ paths: {
2725
+ darwin: [
2726
+ path11.join(HOME, ".cursor"),
2727
+ "/Applications/Cursor.app"
2728
+ ],
2729
+ linux: [
2730
+ path11.join(HOME, ".cursor"),
2731
+ "/usr/share/applications/cursor.desktop"
2732
+ ],
2733
+ win32: [
2734
+ path11.join(HOME, ".cursor"),
2735
+ path11.join(process.env.LOCALAPPDATA || "", "Programs", "cursor")
2736
+ ]
2737
+ }
2738
+ },
2739
+ {
2740
+ id: "zed",
2741
+ name: "Zed",
2742
+ category: "ide",
2743
+ paths: {
2744
+ darwin: [
2745
+ path11.join(HOME, ".zed"),
2746
+ "/Applications/Zed.app"
2747
+ ],
2748
+ linux: [path11.join(HOME, ".config", "zed")]
2749
+ // Zed not available on Windows
2750
+ }
2751
+ },
2752
+ {
2753
+ id: "void",
2754
+ name: "Void",
2755
+ category: "ide",
2756
+ paths: {
2757
+ darwin: [
2758
+ path11.join(HOME, ".void"),
2759
+ "/Applications/Void.app"
2760
+ ],
2761
+ linux: [path11.join(HOME, ".void")],
2762
+ win32: [path11.join(HOME, ".void")]
2763
+ }
2764
+ },
2765
+ {
2766
+ id: "pearai",
2767
+ name: "PearAI",
2768
+ category: "ide",
2769
+ paths: {
2770
+ darwin: [
2771
+ path11.join(HOME, ".pearai"),
2772
+ "/Applications/PearAI.app"
2773
+ ],
2774
+ linux: [path11.join(HOME, ".pearai")],
2775
+ win32: [path11.join(HOME, ".pearai")]
2776
+ }
2777
+ },
2778
+ // ============================================
2779
+ // CLI Coding Tools
2780
+ // ============================================
2781
+ {
2782
+ id: "claudecode",
2783
+ name: "Claude Code",
2784
+ category: "cli",
2785
+ paths: {
2786
+ darwin: [path11.join(HOME, ".claude")],
2787
+ linux: [path11.join(HOME, ".claude")],
2788
+ win32: [path11.join(HOME, ".claude")]
2789
+ }
2790
+ },
2791
+ {
2792
+ id: "aider",
2793
+ name: "Aider",
2794
+ category: "cli",
2795
+ paths: {
2796
+ darwin: [path11.join(HOME, ".aider")],
2797
+ linux: [path11.join(HOME, ".aider")],
2798
+ win32: [path11.join(HOME, ".aider")]
2799
+ }
2800
+ },
2801
+ {
2802
+ id: "mentat",
2803
+ name: "Mentat",
2804
+ category: "cli",
2805
+ paths: {
2806
+ darwin: [path11.join(HOME, ".mentat")],
2807
+ linux: [path11.join(HOME, ".mentat")],
2808
+ win32: [path11.join(HOME, ".mentat")]
2809
+ }
2810
+ },
2811
+ {
2812
+ id: "gpt_pilot",
2813
+ name: "GPT-Pilot",
2814
+ category: "cli",
2815
+ paths: {
2816
+ darwin: [path11.join(HOME, ".gpt-pilot")],
2817
+ linux: [path11.join(HOME, ".gpt-pilot")],
2818
+ win32: [path11.join(HOME, ".gpt-pilot")]
2819
+ }
2820
+ },
2821
+ {
2822
+ id: "codex",
2823
+ name: "OpenAI Codex",
2824
+ category: "cli",
2825
+ paths: {
2826
+ darwin: [path11.join(HOME, ".codex")],
2827
+ linux: [path11.join(HOME, ".codex")],
2828
+ win32: [path11.join(HOME, ".codex")]
2829
+ }
2830
+ },
2831
+ {
2832
+ id: "gemini_cli",
2833
+ name: "Gemini CLI",
2834
+ category: "cli",
2835
+ paths: {
2836
+ darwin: [path11.join(HOME, ".gemini")],
2837
+ linux: [path11.join(HOME, ".gemini")],
2838
+ win32: [path11.join(HOME, ".gemini")]
2839
+ }
2840
+ },
2841
+ // ============================================
2842
+ // Traditional IDEs with AI
2843
+ // ============================================
2844
+ {
2845
+ id: "vscode",
2846
+ name: "VS Code",
2847
+ category: "ide",
2848
+ paths: {
2849
+ darwin: [path11.join(HOME, ".vscode")],
2850
+ linux: [path11.join(HOME, ".vscode")],
2851
+ win32: [path11.join(HOME, ".vscode")]
2852
+ }
2853
+ },
2854
+ {
2855
+ id: "jetbrains",
2856
+ name: "JetBrains IDEs",
2857
+ category: "ide",
2858
+ paths: {
2859
+ darwin: [path11.join(HOME, "Library", "Application Support", "JetBrains")],
2860
+ linux: [path11.join(HOME, ".config", "JetBrains")],
2861
+ win32: [path11.join(HOME, "AppData", "Roaming", "JetBrains")]
2862
+ }
2863
+ },
2864
+ // ============================================
2865
+ // VS Code Plugins
2866
+ // ============================================
2867
+ {
2868
+ id: "github_copilot",
2869
+ name: "GitHub Copilot",
2870
+ category: "plugin",
2871
+ hostIde: "vscode",
2872
+ paths: {},
2873
+ vscodeExtension: "github.copilot"
2874
+ },
2875
+ {
2876
+ id: "continue",
2877
+ name: "Continue",
2878
+ category: "plugin",
2879
+ hostIde: "vscode",
2880
+ paths: {
2881
+ darwin: [path11.join(HOME, ".continue")],
2882
+ linux: [path11.join(HOME, ".continue")],
2883
+ win32: [path11.join(HOME, ".continue")]
2884
+ },
2885
+ vscodeExtension: "continue.continue"
2886
+ },
2887
+ {
2888
+ id: "tabnine",
2889
+ name: "Tabnine",
2890
+ category: "plugin",
2891
+ hostIde: "vscode",
2892
+ paths: {
2893
+ darwin: [path11.join(HOME, ".tabnine")],
2894
+ linux: [path11.join(HOME, ".tabnine")],
2895
+ win32: [path11.join(HOME, ".tabnine")]
2896
+ },
2897
+ vscodeExtension: "tabnine.tabnine"
2898
+ },
2899
+ {
2900
+ id: "codeium",
2901
+ name: "Codeium",
2902
+ category: "plugin",
2903
+ hostIde: "vscode",
2904
+ paths: {},
2905
+ vscodeExtension: "codeium.codeium"
2906
+ },
2907
+ // ============================================
2908
+ // JetBrains Plugins
2909
+ // ============================================
2910
+ {
2911
+ id: "jetbrains_ai",
2912
+ name: "JetBrains AI Assistant",
2913
+ category: "plugin",
2914
+ hostIde: "jetbrains",
2915
+ paths: {},
2916
+ jetbrainsPlugin: "com.intellij.ai"
2917
+ }
2918
+ ];
2919
+ var MONITORABLE_AGENTS = /* @__PURE__ */ new Set([
2920
+ "cursor",
2921
+ "claudecode",
2922
+ "github_copilot",
2923
+ "gemini_cli",
2924
+ "codex"
2925
+ ]);
2926
+
2927
+ // src/coverage/detector.ts
2928
+ var PLATFORM = process.platform;
2929
+ var AgentDetector = class {
2930
+ constructor() {
2931
+ this.cachedMetrics = null;
2932
+ this.cacheTimestamp = 0;
2933
+ this.CACHE_TTL_MS = 10 * 60 * 1e3;
2934
+ }
2935
+ // 10 minute cache
2936
+ /**
2937
+ * Detect all agents and return coverage metrics
2938
+ */
2939
+ async detectAll() {
2940
+ if (this.cachedMetrics && Date.now() - this.cacheTimestamp < this.CACHE_TTL_MS) {
2941
+ return this.cachedMetrics;
2942
+ }
2943
+ const detected = await this.detectInstalledAgents();
2944
+ const monitoredAgents = await this.getMonitoredAgents();
2945
+ const monitoredIds = new Set(monitoredAgents.map((a) => a.id));
2946
+ const agents = detected.map((a) => ({
2947
+ ...a,
2948
+ monitored: monitoredIds.has(a.id)
2949
+ }));
2950
+ const installedAgents = agents.filter((a) => a.installed);
2951
+ const installedCount = installedAgents.length;
2952
+ const monitoredCount = installedAgents.filter((a) => a.monitored).length;
2953
+ const metrics = {
2954
+ installedCount,
2955
+ monitoredCount,
2956
+ percentage: installedCount > 0 ? Math.round(monitoredCount / installedCount * 100) : 0,
2957
+ agents,
2958
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2959
+ platform: PLATFORM
2960
+ };
2961
+ this.cachedMetrics = metrics;
2962
+ this.cacheTimestamp = Date.now();
2963
+ logger.debug("Agent detection completed", {
2964
+ installed: installedCount,
2965
+ monitored: monitoredCount,
2966
+ percentage: metrics.percentage
2967
+ });
2968
+ return metrics;
2969
+ }
2970
+ /**
2971
+ * Detect all installed agents on the system
2972
+ */
2973
+ async detectInstalledAgents() {
2974
+ const agents = [];
2975
+ for (const def of AGENT_DEFINITIONS) {
2976
+ const detected = this.detectAgent(def);
2977
+ agents.push(detected);
2978
+ }
2979
+ return agents;
2980
+ }
2981
+ /**
2982
+ * Get agents that are currently monitored by Overwatch
2983
+ */
2984
+ async getMonitoredAgents() {
2985
+ const installedHooks = await listInstalledHooks();
2986
+ const monitoredAgents = [];
2987
+ for (const hook of installedHooks) {
2988
+ if (hook.hooksInstalled) {
2989
+ const def = AGENT_DEFINITIONS.find((d) => d.id === hook.ide);
2990
+ if (def) {
2991
+ monitoredAgents.push({
2992
+ id: def.id,
2993
+ name: def.name,
2994
+ category: def.category,
2995
+ installed: true,
2996
+ monitored: true,
2997
+ configPath: hook.hooksLocation,
2998
+ hostIde: def.hostIde
2999
+ });
3000
+ }
3001
+ }
3002
+ }
3003
+ return monitoredAgents;
3004
+ }
3005
+ /**
3006
+ * Check if a specific agent can be monitored by Overwatch
3007
+ */
3008
+ isMonitorable(agentId) {
3009
+ return MONITORABLE_AGENTS.has(agentId);
3010
+ }
3011
+ /**
3012
+ * Clear cached detection results
3013
+ */
3014
+ clearCache() {
3015
+ this.cachedMetrics = null;
3016
+ this.cacheTimestamp = 0;
3017
+ }
3018
+ /**
3019
+ * Generate a coverage report ready for the Admin API
3020
+ * Handles the transformation from internal CoverageMetrics to API CoverageReport
3021
+ * @param machineId - Machine identifier (generated once at module init)
3022
+ */
3023
+ async generateCoverageReport(machineId) {
3024
+ const metrics = await this.detectAll();
3025
+ const installedAgents = metrics.agents.filter((a) => a.installed);
3026
+ return {
3027
+ machine_id: machineId,
3028
+ platform: metrics.platform,
3029
+ agents: installedAgents.map((agent) => ({
3030
+ id: agent.id,
3031
+ name: agent.name,
3032
+ category: agent.category,
3033
+ installed: agent.installed,
3034
+ monitored: agent.monitored,
3035
+ config_path: agent.configPath,
3036
+ host_ide: agent.hostIde
3037
+ })),
3038
+ installed_count: metrics.installedCount,
3039
+ monitored_count: metrics.monitoredCount,
3040
+ coverage_percentage: metrics.percentage,
3041
+ timestamp: metrics.timestamp
3042
+ };
3043
+ }
3044
+ /**
3045
+ * Detect a single agent based on its definition
3046
+ */
3047
+ detectAgent(def) {
3048
+ let installed = false;
3049
+ let configPath;
3050
+ const platformPaths = def.paths[PLATFORM] || [];
3051
+ for (const p of platformPaths) {
3052
+ if (this.checkPathExists(p)) {
3053
+ installed = true;
3054
+ configPath = p;
3055
+ break;
3056
+ }
3057
+ }
3058
+ if (!installed && def.vscodeExtension) {
3059
+ const extensionPath = this.findVSCodeExtension(def.vscodeExtension);
3060
+ if (extensionPath) {
3061
+ installed = true;
3062
+ configPath = extensionPath;
3063
+ }
3064
+ }
3065
+ if (!installed && def.jetbrainsPlugin) {
3066
+ const pluginPath = this.findJetBrainsPlugin(def.jetbrainsPlugin);
3067
+ if (pluginPath) {
3068
+ installed = true;
3069
+ configPath = pluginPath;
3070
+ }
3071
+ }
3072
+ return {
3073
+ id: def.id,
3074
+ name: def.name,
3075
+ category: def.category,
3076
+ installed,
3077
+ monitored: false,
3078
+ // Will be updated by detectAll()
3079
+ configPath,
3080
+ hostIde: def.hostIde
3081
+ };
3082
+ }
3083
+ /**
3084
+ * Check if a path exists (file or directory)
3085
+ */
3086
+ checkPathExists(p) {
3087
+ try {
3088
+ return fs12.existsSync(p);
3089
+ } catch {
3090
+ return false;
3091
+ }
3092
+ }
3093
+ /**
3094
+ * Find a VS Code extension by ID pattern
3095
+ */
3096
+ findVSCodeExtension(extensionId) {
3097
+ const extensionsDir = VSCODE_EXTENSIONS[PLATFORM];
3098
+ if (!extensionsDir || !this.checkPathExists(extensionsDir)) {
3099
+ return void 0;
3100
+ }
3101
+ try {
3102
+ const entries = fs12.readdirSync(extensionsDir);
3103
+ const target = extensionId.toLowerCase();
3104
+ for (const entry of entries) {
3105
+ const lower = entry.toLowerCase();
3106
+ const versionTail = lower.startsWith(target + "-") ? lower.slice(target.length + 1) : null;
3107
+ if (lower === target || versionTail !== null && /^\d/.test(versionTail)) {
3108
+ return path12.join(extensionsDir, entry);
3109
+ }
3110
+ }
3111
+ } catch {
3112
+ }
3113
+ return void 0;
3114
+ }
3115
+ /**
3116
+ * Find a JetBrains plugin by ID
3117
+ */
3118
+ findJetBrainsPlugin(pluginId) {
3119
+ const jetbrainsDir = JETBRAINS_CONFIG[PLATFORM];
3120
+ if (!jetbrainsDir || !this.checkPathExists(jetbrainsDir)) {
3121
+ return void 0;
3122
+ }
3123
+ try {
3124
+ const entries = fs12.readdirSync(jetbrainsDir);
3125
+ for (const entry of entries) {
3126
+ const pluginsDir = path12.join(jetbrainsDir, entry, "plugins");
3127
+ if (this.checkPathExists(pluginsDir)) {
3128
+ const plugins = fs12.readdirSync(pluginsDir);
3129
+ for (const plugin of plugins) {
3130
+ if (plugin.toLowerCase().includes(pluginId.toLowerCase())) {
3131
+ return path12.join(pluginsDir, plugin);
3132
+ }
3133
+ }
3134
+ }
3135
+ }
3136
+ } catch {
3137
+ }
3138
+ return void 0;
3139
+ }
3140
+ };
3141
+ var agentDetector = new AgentDetector();
3142
+
3143
+ // src/coverage/machine-id.ts
3144
+ var fs13 = __toESM(require("fs"));
3145
+ var os12 = __toESM(require("os"));
3146
+ var crypto4 = __toESM(require("crypto"));
3147
+ function generateMachineId() {
3148
+ const platformId = getPlatformMachineId();
3149
+ if (platformId) {
3150
+ return crypto4.createHash("sha256").update(platformId).digest("hex").substring(0, 32);
3151
+ }
3152
+ const components = [
3153
+ os12.hostname(),
3154
+ os12.platform(),
3155
+ os12.arch(),
3156
+ os12.userInfo().username || "unknown",
3157
+ os12.homedir()
3158
+ ];
3159
+ const combined = components.join("|");
3160
+ return crypto4.createHash("sha256").update(combined).digest("hex").substring(0, 32);
3161
+ }
3162
+ function getPlatformMachineId() {
3163
+ try {
3164
+ switch (os12.platform()) {
3165
+ case "darwin":
3166
+ return getMacOSMachineId();
3167
+ case "linux":
3168
+ return getLinuxMachineId();
3169
+ case "win32":
3170
+ return getWindowsMachineId();
3171
+ default:
3172
+ return null;
3173
+ }
3174
+ } catch {
3175
+ return null;
3176
+ }
3177
+ }
3178
+ function getMacOSMachineId() {
3179
+ try {
3180
+ const { execSync } = require("child_process");
3181
+ const output = execSync(
3182
+ "ioreg -rd1 -c IOPlatformExpertDevice | grep IOPlatformUUID",
3183
+ { encoding: "utf-8", timeout: 5e3 }
3184
+ );
3185
+ const match = output.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
3186
+ return match ? match[1] : null;
3187
+ } catch {
3188
+ return null;
3189
+ }
3190
+ }
3191
+ function getLinuxMachineId() {
3192
+ try {
3193
+ const machineIdPath = "/etc/machine-id";
3194
+ if (fs13.existsSync(machineIdPath)) {
3195
+ return fs13.readFileSync(machineIdPath, "utf-8").trim();
3196
+ }
3197
+ const dbusPath = "/var/lib/dbus/machine-id";
3198
+ if (fs13.existsSync(dbusPath)) {
3199
+ return fs13.readFileSync(dbusPath, "utf-8").trim();
3200
+ }
3201
+ return null;
3202
+ } catch {
3203
+ return null;
3204
+ }
3205
+ }
3206
+ function getWindowsMachineId() {
3207
+ try {
3208
+ const { execSync } = require("child_process");
3209
+ const output = execSync(
3210
+ 'reg query "HKLM\\SOFTWARE\\Microsoft\\Cryptography" /v MachineGuid',
3211
+ { encoding: "utf-8", timeout: 5e3 }
3212
+ );
3213
+ const match = output.match(/MachineGuid\s+REG_SZ\s+([^\s\r\n]+)/);
3214
+ return match ? match[1] : null;
3215
+ } catch {
3216
+ return null;
3217
+ }
3218
+ }
3219
+
3220
+ // src/module.ts
3221
+ var GuardianModule = class _GuardianModule {
3222
+ constructor(config = {}) {
3223
+ this.projectSkillsCache = /* @__PURE__ */ new Map();
3224
+ this.machineId = "";
3225
+ this.coverageInterval = null;
3226
+ // 10 minutes
3227
+ // Ingestors (scan results + skill scan results to admin API)
3228
+ this.scanIngestor = null;
3229
+ this.skillsIngestor = null;
3230
+ // Admin client (for installation tracking + coverage reporting)
3231
+ this.adminClient = null;
3232
+ // Tracks (user, IDE) installation records that have already been
3233
+ // sent to the admin API, with success-only caching and in-flight
3234
+ // dedup. See InstallationRecorder for the rationale.
3235
+ this.installationRecorder = new InstallationRecorder(
3236
+ (source, email) => this.recordInstallation(source, email)
3237
+ );
3238
+ this.pendingOAuthStates = /* @__PURE__ */ new Map();
3239
+ this.oauthStateTimeout = 5 * 60 * 1e3;
3240
+ this.initialized = false;
3241
+ this.debug = config.debug || false;
3242
+ this.configReader = new ConfigReader();
3243
+ this.scanner = new MCPScanner();
3244
+ this.skillsScanner = new SkillsScanner();
3245
+ this.agentDetector = new AgentDetector();
3246
+ if (this.debug)
3247
+ logger.setLevel(0 /* DEBUG */);
3248
+ this.httpServer = new GuardianHttpServer(config.httpPort || 0);
3249
+ }
3250
+ static {
3251
+ this.PROJECT_SKILLS_CACHE_TTL = 5 * 60 * 1e3;
3252
+ }
3253
+ static {
3254
+ // Cadence at which the daemon reports coverage metrics to the admin
3255
+ // API. 10 minutes = 144 calls/day/machine; if backend cost becomes a
3256
+ // concern, raise this to 1h or 24h. Keep the comments at module.ts
3257
+ // line 140 and javelin/admin-client.ts (reportCoverageMetrics) in
3258
+ // sync with this value.
3259
+ this.COVERAGE_INTERVAL_MS = 10 * 60 * 1e3;
3260
+ }
3261
+ async init() {
3262
+ if (this.initialized)
3263
+ return;
3264
+ logger.info("Initializing...");
3265
+ this.machineId = generateMachineId();
3266
+ logger.debug("Generated machine ID", { machineId: this.machineId });
3267
+ await this.initAdminClient();
3268
+ const highflameConfig = this.configReader.getHighflameConfig();
3269
+ if (highflameConfig.enabled) {
3270
+ const ingestorTokenGetter = () => this.configReader.getJwtToken();
3271
+ this.scanIngestor = new AdminIngestor(
3272
+ highflameConfig.baseUrl,
3273
+ ingestorTokenGetter,
3274
+ "mcp-scans"
3275
+ );
3276
+ this.scanIngestor.start();
3277
+ this.skillsIngestor = new AdminIngestor(
3278
+ highflameConfig.baseUrl,
3279
+ ingestorTokenGetter,
3280
+ "skills"
3281
+ );
3282
+ this.skillsIngestor.start();
3283
+ }
3284
+ const tokenGetter = () => this.configReader.getJwtToken();
3285
+ this.cerberusClient = new CerberusClient(
3286
+ getCerberusUrl(),
3287
+ tokenGetter,
3288
+ this.machineId
3289
+ );
3290
+ this.hookPipeline = new HookPipeline(
3291
+ this.cerberusClient,
3292
+ (source) => this.checkInstallation(source)
3293
+ );
3294
+ this.hookHandler = new HookHandler(this.hookPipeline);
3295
+ this.hookHandler.setProjectSkillsScanCallback((workspace) => {
3296
+ this.scanProjectSkillsIfNeeded(workspace).catch(() => {
3297
+ });
3298
+ });
3299
+ this.scanHandler = new ScanHandler(this.scanner, this.scanIngestor);
3300
+ this.oauthHandler = new OAuthHandler(
3301
+ this.httpServer.getPort(),
3302
+ this.pendingOAuthStates,
3303
+ this.oauthStateTimeout
3304
+ );
3305
+ this.initialized = true;
3306
+ logger.info("Initialized");
3307
+ this.startCoverageReporting();
3308
+ }
3309
+ /**
3310
+ * Start coverage reporting - runs on startup and every 10 minutes
3311
+ */
3312
+ startCoverageReporting() {
3313
+ this.reportCoverage().catch((error) => {
3314
+ logger.error("Failed to report initial coverage", { error: String(error) });
3315
+ });
3316
+ this.coverageInterval = setInterval(
3317
+ () => this.reportCoverage().catch((error) => {
3318
+ logger.error("Failed to report periodic coverage", { error: String(error) });
3319
+ }),
3320
+ _GuardianModule.COVERAGE_INTERVAL_MS
3321
+ );
3322
+ }
3323
+ /**
3324
+ * Report coverage metrics to admin API
3325
+ */
3326
+ async reportCoverage() {
3327
+ if (!this.adminClient) {
3328
+ logger.debug("Coverage reporting skipped - no admin client");
3329
+ return;
3330
+ }
3331
+ const report = await this.agentDetector.generateCoverageReport(this.machineId);
3332
+ const success = await this.adminClient.reportCoverageMetrics(report);
3333
+ if (success) {
3334
+ logger.info("Coverage report sent successfully", {
3335
+ installed: report.installed_count,
3336
+ monitored: report.monitored_count,
3337
+ percentage: report.coverage_percentage
3338
+ });
3339
+ }
3340
+ }
3341
+ /**
3342
+ * Initialize admin client for installation tracking and coverage reporting.
3343
+ * Extracted from the removed initPolicyManager().
3344
+ */
3345
+ async initAdminClient() {
3346
+ const adminConfig = this.configReader.getAdminConfig();
3347
+ const tokenGetter = () => this.configReader.getAdminToken();
3348
+ if (adminConfig.enabled && adminConfig.baseUrl) {
3349
+ this.adminClient = new AdminClient(adminConfig.baseUrl, tokenGetter);
3350
+ logger.info("Admin client initialized", { baseUrl: adminConfig.baseUrl });
3351
+ }
3352
+ }
3353
+ checkInstallation(source) {
3354
+ const authStatus = getAuthStatus();
3355
+ if (!authStatus.authenticated || !authStatus.email) {
3356
+ return;
3357
+ }
3358
+ this.installationRecorder.tryRecord(source, authStatus.email).catch(() => {
3359
+ });
3360
+ }
3361
+ async recordInstallation(source, userEmail) {
3362
+ if (!this.adminClient)
3363
+ return false;
3364
+ try {
3365
+ return await this.adminClient.recordInstallation({
3366
+ user_id: userEmail,
3367
+ user_email: userEmail,
3368
+ ide_type: source,
3369
+ version: VERSION,
3370
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3371
+ });
3372
+ } catch (error) {
3373
+ logger.error("Failed to record installation", { error: String(error) });
3374
+ return false;
3375
+ }
3376
+ }
3377
+ async startServer() {
3378
+ if (!this.initialized)
3379
+ await this.init();
3380
+ this.httpServer.on(
3381
+ "/hook",
3382
+ (req, res) => this.hookHandler?.handle(req, res)
3383
+ );
3384
+ this.httpServer.on("/health", async (_req, res) => {
3385
+ const authStatus = getAuthStatus();
3386
+ res.writeHead(200, { "Content-Type": "application/json" });
3387
+ res.end(
3388
+ JSON.stringify({
3389
+ status: "ok",
3390
+ version: VERSION,
3391
+ initialized: this.initialized,
3392
+ eventCount: this.hookHandler?.getEventCount() || 0,
3393
+ auth: {
3394
+ authenticated: authStatus.authenticated,
3395
+ expired: authStatus.expired || false,
3396
+ expiringSoon: authStatus.expiringSoon || false,
3397
+ email: authStatus.email
3398
+ }
3399
+ })
3400
+ );
3401
+ });
3402
+ this.httpServer.on("/coverage", async (_req, res) => {
3403
+ try {
3404
+ const metrics = await this.agentDetector.detectAll();
3405
+ res.writeHead(200, { "Content-Type": "application/json" });
3406
+ res.end(JSON.stringify(metrics));
3407
+ } catch (error) {
3408
+ res.writeHead(500, { "Content-Type": "application/json" });
3409
+ res.end(JSON.stringify({ error: "Failed to detect agents" }));
3410
+ }
3411
+ });
3412
+ this.httpServer.on(
3413
+ "/scan",
3414
+ (req, res) => this.scanHandler?.handle(req, res)
3415
+ );
3416
+ this.httpServer.on(
3417
+ "/oauth/start",
3418
+ (req, res) => this.oauthHandler?.handleStart(req, res)
3419
+ );
3420
+ this.httpServer.on(
3421
+ "/oauth/callback",
3422
+ (req, res) => this.oauthHandler?.handleCallback(req, res)
3423
+ );
3424
+ this.httpServer.on(
3425
+ "/oauth/status",
3426
+ (req, res) => this.oauthHandler?.handleStatus(req, res)
3427
+ );
3428
+ this.httpServer.on(
3429
+ "/shutdown",
3430
+ (req, res) => this.handleShutdown(req, res)
3431
+ );
3432
+ await this.httpServer.start();
3433
+ logger.info(`Server started on port ${this.httpServer.getPort()}`);
3434
+ if (this.scanHandler)
3435
+ this.scanHandler.runInitialScan();
3436
+ this.runStartupSkillsScan();
3437
+ }
3438
+ async runStartupSkillsScan() {
3439
+ if (!this.skillsIngestor) {
3440
+ logger.debug("Skipping startup skills scan: no ingestor configured");
3441
+ return;
3442
+ }
3443
+ try {
3444
+ logger.info("Running startup skills scan...");
3445
+ const result = await this.skillsScanner.scan();
3446
+ if (this.skillsIngestor) {
3447
+ this.skillsIngestor.ingest(result);
3448
+ }
3449
+ logger.info("Startup skills scan completed", {
3450
+ totalSkills: result.totalSkills
3451
+ });
3452
+ } catch (error) {
3453
+ logger.error("Startup skills scan failed", { error: String(error) });
3454
+ }
3455
+ }
3456
+ handleShutdown(req, res) {
3457
+ if (req.method !== "POST") {
3458
+ res.writeHead(405, { "Content-Type": "application/json" });
3459
+ res.end(JSON.stringify({ error: "Method not allowed" }));
3460
+ return;
3461
+ }
3462
+ res.writeHead(200, { "Content-Type": "application/json" });
3463
+ res.end(JSON.stringify({ status: "shutting_down" }), () => {
3464
+ logger.info("Shutdown requested via API, sending SIGTERM...");
3465
+ process.kill(process.pid, "SIGTERM");
3466
+ });
3467
+ }
3468
+ async stopServer() {
3469
+ if (this.coverageInterval) {
3470
+ clearInterval(this.coverageInterval);
3471
+ this.coverageInterval = null;
3472
+ }
3473
+ this.scanIngestor?.stop();
3474
+ this.skillsIngestor?.stop();
3475
+ await this.httpServer.stop();
3476
+ }
3477
+ getPort() {
3478
+ return this.httpServer.getPort();
3479
+ }
3480
+ /**
3481
+ * Scan project-level skills if not recently cached
3482
+ * Called from hook handler when workspace is present in event
3483
+ */
3484
+ async scanProjectSkillsIfNeeded(workspace) {
3485
+ const cached = this.projectSkillsCache.get(workspace);
3486
+ const now = Date.now();
3487
+ if (cached && now - cached.timestamp < _GuardianModule.PROJECT_SKILLS_CACHE_TTL) {
3488
+ logger.debug("Project skills cache hit", { workspace });
3489
+ return;
3490
+ }
3491
+ try {
3492
+ const result = await this.skillsScanner.scanProjectSkills(workspace);
3493
+ this.projectSkillsCache.set(workspace, { timestamp: now });
3494
+ if (result.totalSkills > 0 && this.skillsIngestor) {
3495
+ this.skillsIngestor.ingest(result);
3496
+ }
3497
+ } catch (error) {
3498
+ logger.error("Failed to scan project skills", {
3499
+ workspace,
3500
+ error: String(error)
3501
+ });
3502
+ }
3503
+ }
3504
+ };
3505
+
3506
+ // src/daemon.ts
3507
+ function parseArgs() {
3508
+ const args = process.argv.slice(2);
3509
+ let debug = false;
3510
+ for (const arg of args) {
3511
+ if (arg === "--debug") {
3512
+ debug = true;
3513
+ }
3514
+ }
3515
+ return { debug };
3516
+ }
3517
+ async function main() {
3518
+ const { debug } = parseArgs();
3519
+ const portFile = path13.join(os13.homedir(), ".overwatch", "guardian_port");
3520
+ console.log("[Guardian] Starting daemon...");
3521
+ console.log(`[Guardian] Port: ${GUARDIAN_PORT}`);
3522
+ console.log(`[Guardian] Debug: ${debug}`);
3523
+ const portDir = path13.dirname(portFile);
3524
+ if (!fs14.existsSync(portDir)) {
3525
+ fs14.mkdirSync(portDir, { recursive: true });
3526
+ }
3527
+ const port = GUARDIAN_PORT;
3528
+ const guardian = new GuardianModule({
3529
+ httpPort: port,
3530
+ debug
3531
+ });
3532
+ try {
3533
+ await guardian.startServer();
3534
+ } catch (err) {
3535
+ const code = err?.code;
3536
+ if (code === "EADDRINUSE") {
3537
+ console.error(
3538
+ `[Guardian] Port ${port} is already in use. Another instance is likely running; exiting cleanly.`
3539
+ );
3540
+ process.exit(0);
3541
+ }
3542
+ throw err;
3543
+ }
3544
+ fs14.writeFileSync(portFile, String(port));
3545
+ PortManager.writePidFile();
3546
+ console.log(`[Guardian] Server started on port ${port}`);
3547
+ console.log(`[Guardian] Port written to ${portFile}`);
3548
+ const shutdown = async (signal) => {
3549
+ console.log(`[Guardian] Received ${signal}, shutting down...`);
3550
+ try {
3551
+ await guardian.stopServer();
3552
+ if (fs14.existsSync(portFile)) {
3553
+ fs14.unlinkSync(portFile);
3554
+ }
3555
+ PortManager.removePidFile();
3556
+ console.log("[Guardian] Shutdown complete");
3557
+ process.exit(0);
3558
+ } catch (error) {
3559
+ console.error("[Guardian] Error during shutdown:", error);
3560
+ process.exit(1);
3561
+ }
3562
+ };
3563
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
3564
+ process.on("SIGINT", () => shutdown("SIGINT"));
3565
+ process.on("uncaughtException", (error) => {
3566
+ console.error("[Guardian] Uncaught exception:", error);
3567
+ process.exit(1);
3568
+ });
3569
+ process.on("unhandledRejection", (reason) => {
3570
+ console.error("[Guardian] Unhandled rejection:", reason);
3571
+ process.exit(1);
3572
+ });
3573
+ console.log("[Guardian] Daemon running. Press Ctrl+C to stop.");
3574
+ }
3575
+ main().catch((error) => {
3576
+ console.error("[Guardian] Failed to start:", error);
3577
+ process.exit(1);
3578
+ });