@node9/proxy 1.10.1 → 1.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -147,8 +147,8 @@ function sanitizeConfig(raw) {
147
147
  }
148
148
  }
149
149
  const lines = result.error.issues.map((issue) => {
150
- const path35 = issue.path.length > 0 ? issue.path.join(".") : "root";
151
- return ` \u2022 ${path35}: ${issue.message}`;
150
+ const path39 = issue.path.length > 0 ? issue.path.join(".") : "root";
151
+ return ` \u2022 ${path39}: ${issue.message}`;
152
152
  });
153
153
  return {
154
154
  sanitized,
@@ -232,7 +232,8 @@ var init_config_schema = __esm({
232
232
  enableTrustSessions: z.boolean().optional(),
233
233
  allowGlobalPause: z.boolean().optional(),
234
234
  auditHashArgs: z.boolean().optional(),
235
- agentPolicy: z.enum(["require_approval", "block_on_rules"]).optional()
235
+ agentPolicy: z.enum(["require_approval", "block_on_rules"]).optional(),
236
+ cloudSyncIntervalHours: z.number().positive().optional()
236
237
  }).optional(),
237
238
  policy: z.object({
238
239
  sandboxPaths: z.array(z.string()).optional(),
@@ -495,11 +496,11 @@ function getGlobalSettings() {
495
496
  };
496
497
  }
497
498
  function getCredentials() {
498
- const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
499
+ const DEFAULT_API_URL2 = "https://api.node9.ai/api/v1/intercept";
499
500
  if (process.env.NODE9_API_KEY) {
500
501
  return {
501
502
  apiKey: process.env.NODE9_API_KEY,
502
- apiUrl: process.env.NODE9_API_URL || DEFAULT_API_URL
503
+ apiUrl: process.env.NODE9_API_URL || DEFAULT_API_URL2
503
504
  };
504
505
  }
505
506
  try {
@@ -511,13 +512,13 @@ function getCredentials() {
511
512
  if (profile?.apiKey) {
512
513
  return {
513
514
  apiKey: profile.apiKey,
514
- apiUrl: profile.apiUrl || DEFAULT_API_URL
515
+ apiUrl: profile.apiUrl || DEFAULT_API_URL2
515
516
  };
516
517
  }
517
518
  if (creds.apiKey) {
518
519
  return {
519
520
  apiKey: creds.apiKey,
520
- apiUrl: creds.apiUrl || DEFAULT_API_URL
521
+ apiUrl: creds.apiUrl || DEFAULT_API_URL2
521
522
  };
522
523
  }
523
524
  }
@@ -568,6 +569,8 @@ function getConfig(cwd) {
568
569
  if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
569
570
  mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
570
571
  if (s.environment !== void 0) mergedSettings.environment = s.environment;
572
+ if (s.cloudSyncIntervalHours !== void 0)
573
+ mergedSettings.cloudSyncIntervalHours = s.cloudSyncIntervalHours;
571
574
  if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
572
575
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
573
576
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
@@ -577,7 +580,12 @@ function getConfig(cwd) {
577
580
  if (p.smartRules) {
578
581
  const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
579
582
  const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
580
- mergedPolicy.smartRules = [...defaultBlocks, ...p.smartRules, ...defaultNonBlocks];
583
+ const userRuleNames = new Set(p.smartRules.filter((r) => r.name).map((r) => r.name));
584
+ const filteredBlocks = defaultBlocks.filter((r) => !r.name || !userRuleNames.has(r.name));
585
+ const filteredNonBlocks = defaultNonBlocks.filter(
586
+ (r) => !r.name || !userRuleNames.has(r.name)
587
+ );
588
+ mergedPolicy.smartRules = [...filteredBlocks, ...p.smartRules, ...filteredNonBlocks];
581
589
  }
582
590
  if (p.snapshot) {
583
591
  const s2 = p.snapshot;
@@ -611,6 +619,16 @@ function getConfig(cwd) {
611
619
  };
612
620
  applyLayer(globalConfig);
613
621
  applyLayer(projectConfig);
622
+ {
623
+ const cacheFile = path3.join(os3.homedir(), ".node9", "rules-cache.json");
624
+ try {
625
+ const raw = JSON.parse(fs3.readFileSync(cacheFile, "utf-8"));
626
+ if (Array.isArray(raw.rules) && raw.rules.length > 0) {
627
+ applyLayer({ policy: { smartRules: raw.rules } });
628
+ }
629
+ } catch {
630
+ }
631
+ }
614
632
  const shieldOverrides = readShieldOverrides();
615
633
  for (const shieldName of readActiveShields()) {
616
634
  const shield = getShield(shieldName);
@@ -726,7 +744,8 @@ var init_config = __esm({
726
744
  // 120-second auto-deny timeout
727
745
  flightRecorder: true,
728
746
  auditHashArgs: true,
729
- approvers: { native: true, browser: true, cloud: false, terminal: true }
747
+ approvers: { native: true, browser: true, cloud: false, terminal: true },
748
+ cloudSyncIntervalHours: 5
730
749
  },
731
750
  policy: {
732
751
  sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
@@ -1710,9 +1729,9 @@ function matchesPattern(text, patterns) {
1710
1729
  const withoutDotSlash = text.replace(/^\.\//, "");
1711
1730
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1712
1731
  }
1713
- function getNestedValue(obj, path35) {
1732
+ function getNestedValue(obj, path39) {
1714
1733
  if (!obj || typeof obj !== "object") return null;
1715
- return path35.split(".").reduce((prev, curr) => prev?.[curr], obj);
1734
+ return path39.split(".").reduce((prev, curr) => prev?.[curr], obj);
1716
1735
  }
1717
1736
  function shouldSnapshot(toolName, args, config) {
1718
1737
  if (!config.settings.enableUndo) return false;
@@ -2187,8 +2206,8 @@ async function explainPolicy(toolName, args) {
2187
2206
  const flattenedArgs = JSON.stringify(args).toLowerCase();
2188
2207
  const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
2189
2208
  allTokens.push(...extraTokens);
2190
- const preview = extraTokens.slice(0, 8).join(", ") + (extraTokens.length > 8 ? "\u2026" : "");
2191
- detail += ` + deep scan of args: [${preview}]`;
2209
+ const preview2 = extraTokens.slice(0, 8).join(", ") + (extraTokens.length > 8 ? "\u2026" : "");
2210
+ detail += ` + deep scan of args: [${preview2}]`;
2192
2211
  }
2193
2212
  steps.push({ name: "Input parsing", outcome: "checked", detail });
2194
2213
  }
@@ -2440,19 +2459,44 @@ function getInternalToken() {
2440
2459
  function isDaemonRunning() {
2441
2460
  const pidFile = path10.join(os8.homedir(), ".node9", "daemon.pid");
2442
2461
  if (fs9.existsSync(pidFile)) {
2462
+ let pid;
2463
+ let port;
2443
2464
  try {
2444
- const { pid, port } = JSON.parse(fs9.readFileSync(pidFile, "utf-8"));
2445
- if (port !== DAEMON_PORT) return false;
2446
- process.kill(pid, 0);
2447
- return true;
2465
+ const data = JSON.parse(fs9.readFileSync(pidFile, "utf-8"));
2466
+ pid = data.pid;
2467
+ port = data.port;
2448
2468
  } catch {
2449
2469
  return false;
2450
2470
  }
2471
+ if (port !== DAEMON_PORT) {
2472
+ return false;
2473
+ }
2474
+ const MAX_PID2 = 4194304;
2475
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || pid > MAX_PID2) {
2476
+ return false;
2477
+ }
2478
+ try {
2479
+ process.kill(pid, 0);
2480
+ } catch (err2) {
2481
+ if (err2 instanceof Error && "code" in err2 && err2.code === "ESRCH") {
2482
+ try {
2483
+ fs9.unlinkSync(pidFile);
2484
+ } catch {
2485
+ }
2486
+ }
2487
+ return false;
2488
+ }
2489
+ const r = spawnSync("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
2490
+ encoding: "utf8",
2491
+ timeout: 300
2492
+ });
2493
+ if (r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`)) return true;
2494
+ return false;
2451
2495
  }
2452
2496
  try {
2453
2497
  const r = spawnSync("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
2454
2498
  encoding: "utf8",
2455
- timeout: 500
2499
+ timeout: 300
2456
2500
  });
2457
2501
  return r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`);
2458
2502
  } catch {
@@ -6218,15 +6262,152 @@ var init_costSync = __esm({
6218
6262
  }
6219
6263
  });
6220
6264
 
6221
- // src/daemon/server.ts
6222
- import http from "http";
6265
+ // src/daemon/sync.ts
6223
6266
  import fs17 from "fs";
6267
+ import https from "https";
6268
+ import os15 from "os";
6224
6269
  import path20 from "path";
6270
+ function readCredentials() {
6271
+ if (process.env.NODE9_API_KEY) {
6272
+ return {
6273
+ apiKey: process.env.NODE9_API_KEY,
6274
+ apiUrl: process.env.NODE9_API_URL ?? DEFAULT_API_URL
6275
+ };
6276
+ }
6277
+ try {
6278
+ const credPath = path20.join(os15.homedir(), ".node9", "credentials.json");
6279
+ const creds = JSON.parse(fs17.readFileSync(credPath, "utf-8"));
6280
+ const profileName = process.env.NODE9_PROFILE ?? "default";
6281
+ const profile = creds[profileName];
6282
+ if (typeof profile?.apiKey === "string" && profile.apiKey.length > 0) {
6283
+ return {
6284
+ apiKey: profile.apiKey,
6285
+ apiUrl: typeof profile.apiUrl === "string" ? profile.apiUrl.replace(/\/intercept$/, "/policy") : DEFAULT_API_URL
6286
+ };
6287
+ }
6288
+ if (typeof creds.apiKey === "string" && creds.apiKey.length > 0) {
6289
+ return { apiKey: creds.apiKey, apiUrl: DEFAULT_API_URL };
6290
+ }
6291
+ } catch {
6292
+ }
6293
+ return null;
6294
+ }
6295
+ function fetchCloudRules(apiKey, apiUrl) {
6296
+ const parsed = new URL(apiUrl);
6297
+ return new Promise((resolve, reject) => {
6298
+ const req = https.request(
6299
+ {
6300
+ hostname: parsed.hostname,
6301
+ port: parsed.port ? parseInt(parsed.port, 10) : void 0,
6302
+ path: parsed.pathname + parsed.search,
6303
+ method: "GET",
6304
+ headers: {
6305
+ Authorization: `Bearer ${apiKey}`,
6306
+ "Content-Type": "application/json"
6307
+ },
6308
+ timeout: 1e4
6309
+ },
6310
+ (res) => {
6311
+ const chunks = [];
6312
+ res.on("data", (chunk) => chunks.push(chunk));
6313
+ res.on("end", () => {
6314
+ if (res.statusCode !== 200) {
6315
+ reject(new Error(`API returned ${res.statusCode ?? "unknown"}`));
6316
+ return;
6317
+ }
6318
+ try {
6319
+ const body = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
6320
+ const rules = Array.isArray(body) ? body : Array.isArray(body.rules) ? body.rules : [];
6321
+ resolve(rules);
6322
+ } catch (e) {
6323
+ reject(e);
6324
+ }
6325
+ });
6326
+ }
6327
+ );
6328
+ req.on("error", reject);
6329
+ req.on("timeout", () => {
6330
+ req.destroy(new Error("Cloud policy fetch timed out"));
6331
+ });
6332
+ req.end();
6333
+ });
6334
+ }
6335
+ async function syncOnce() {
6336
+ const creds = readCredentials();
6337
+ if (!creds) return;
6338
+ try {
6339
+ const rules = await fetchCloudRules(creds.apiKey, creds.apiUrl);
6340
+ const cache = { fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), rules };
6341
+ const dir = path20.dirname(rulesCacheFile());
6342
+ if (!fs17.existsSync(dir)) fs17.mkdirSync(dir, { recursive: true });
6343
+ fs17.writeFileSync(rulesCacheFile(), JSON.stringify(cache, null, 2) + "\n", "utf-8");
6344
+ } catch {
6345
+ }
6346
+ }
6347
+ async function runCloudSync() {
6348
+ const creds = readCredentials();
6349
+ if (!creds) {
6350
+ return { ok: false, reason: "No API key configured. Add credentials with: node9 login" };
6351
+ }
6352
+ try {
6353
+ const rules = await fetchCloudRules(creds.apiKey, creds.apiUrl);
6354
+ const cache = { fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), rules };
6355
+ const dir = path20.dirname(rulesCacheFile());
6356
+ if (!fs17.existsSync(dir)) fs17.mkdirSync(dir, { recursive: true });
6357
+ fs17.writeFileSync(rulesCacheFile(), JSON.stringify(cache, null, 2) + "\n", "utf-8");
6358
+ return { ok: true, rules: rules.length, fetchedAt: cache.fetchedAt };
6359
+ } catch (err2) {
6360
+ return { ok: false, reason: err2 instanceof Error ? err2.message : String(err2) };
6361
+ }
6362
+ }
6363
+ function getCloudSyncStatus() {
6364
+ try {
6365
+ const raw = JSON.parse(fs17.readFileSync(rulesCacheFile(), "utf-8"));
6366
+ if (!Array.isArray(raw.rules) || typeof raw.fetchedAt !== "string") return { cached: false };
6367
+ return { cached: true, rules: raw.rules.length, fetchedAt: raw.fetchedAt };
6368
+ } catch {
6369
+ return { cached: false };
6370
+ }
6371
+ }
6372
+ function getCloudRules() {
6373
+ try {
6374
+ const raw = JSON.parse(fs17.readFileSync(rulesCacheFile(), "utf-8"));
6375
+ return Array.isArray(raw.rules) ? raw.rules : null;
6376
+ } catch {
6377
+ return null;
6378
+ }
6379
+ }
6380
+ function startCloudSync() {
6381
+ const rawHours = getConfig().settings.cloudSyncIntervalHours ?? DEFAULT_INTERVAL_HOURS;
6382
+ const intervalHours = Math.max(rawHours, MIN_INTERVAL_HOURS);
6383
+ const intervalMs = intervalHours * 60 * 60 * 1e3;
6384
+ const initial = setTimeout(() => void syncOnce(), 3e4);
6385
+ initial.unref();
6386
+ const recurring = setInterval(() => void syncOnce(), intervalMs);
6387
+ recurring.unref();
6388
+ }
6389
+ var rulesCacheFile, DEFAULT_API_URL, DEFAULT_INTERVAL_HOURS, MIN_INTERVAL_HOURS;
6390
+ var init_sync = __esm({
6391
+ "src/daemon/sync.ts"() {
6392
+ "use strict";
6393
+ init_config();
6394
+ rulesCacheFile = () => path20.join(os15.homedir(), ".node9", "rules-cache.json");
6395
+ DEFAULT_API_URL = "https://api.node9.ai/api/v1/policy";
6396
+ DEFAULT_INTERVAL_HOURS = 5;
6397
+ MIN_INTERVAL_HOURS = 1;
6398
+ }
6399
+ });
6400
+
6401
+ // src/daemon/server.ts
6402
+ import http from "http";
6403
+ import fs18 from "fs";
6404
+ import path21 from "path";
6225
6405
  import { randomUUID as randomUUID4 } from "crypto";
6226
6406
  import { spawnSync as spawnSync2 } from "child_process";
6227
6407
  import chalk2 from "chalk";
6228
6408
  function startDaemon() {
6229
6409
  startCostSync();
6410
+ startCloudSync();
6230
6411
  loadInsightCounts();
6231
6412
  const csrfToken = randomUUID4();
6232
6413
  const internalToken = randomUUID4();
@@ -6242,7 +6423,7 @@ function startDaemon() {
6242
6423
  idleTimer = setTimeout(() => {
6243
6424
  if (autoStarted) {
6244
6425
  try {
6245
- fs17.unlinkSync(DAEMON_PID_FILE);
6426
+ fs18.unlinkSync(DAEMON_PID_FILE);
6246
6427
  } catch {
6247
6428
  }
6248
6429
  }
@@ -6405,7 +6586,7 @@ data: ${JSON.stringify(item.data)}
6405
6586
  status: "pending"
6406
6587
  });
6407
6588
  }
6408
- const projectCwd = typeof cwd === "string" && path20.isAbsolute(cwd) ? cwd : void 0;
6589
+ const projectCwd = typeof cwd === "string" && path21.isAbsolute(cwd) ? cwd : void 0;
6409
6590
  const projectConfig = getConfig(projectCwd);
6410
6591
  const browserEnabled = projectConfig.settings.approvers?.browser !== false;
6411
6592
  const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
@@ -6795,8 +6976,8 @@ data: ${JSON.stringify(item.data)}
6795
6976
  const body = await readBody(req);
6796
6977
  const data = body ? JSON.parse(body) : {};
6797
6978
  const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
6798
- const node9Dir = path20.dirname(GLOBAL_CONFIG_PATH);
6799
- if (!path20.resolve(configPath).startsWith(node9Dir + path20.sep)) {
6979
+ const node9Dir = path21.dirname(GLOBAL_CONFIG_PATH);
6980
+ if (!path21.resolve(configPath).startsWith(node9Dir + path21.sep)) {
6800
6981
  res.writeHead(400, { "Content-Type": "application/json" });
6801
6982
  return res.end(
6802
6983
  JSON.stringify({ error: "configPath must be within the node9 config directory" })
@@ -6907,14 +7088,14 @@ data: ${JSON.stringify(item.data)}
6907
7088
  server.on("error", (e) => {
6908
7089
  if (e.code === "EADDRINUSE") {
6909
7090
  try {
6910
- if (fs17.existsSync(DAEMON_PID_FILE)) {
6911
- const { pid } = JSON.parse(fs17.readFileSync(DAEMON_PID_FILE, "utf-8"));
7091
+ if (fs18.existsSync(DAEMON_PID_FILE)) {
7092
+ const { pid } = JSON.parse(fs18.readFileSync(DAEMON_PID_FILE, "utf-8"));
6912
7093
  process.kill(pid, 0);
6913
7094
  return process.exit(0);
6914
7095
  }
6915
7096
  } catch {
6916
7097
  try {
6917
- fs17.unlinkSync(DAEMON_PID_FILE);
7098
+ fs18.unlinkSync(DAEMON_PID_FILE);
6918
7099
  } catch {
6919
7100
  }
6920
7101
  server.listen(DAEMON_PORT, DAEMON_HOST);
@@ -6983,56 +7164,327 @@ var init_server = __esm({
6983
7164
  init_patch();
6984
7165
  init_config_schema();
6985
7166
  init_costSync();
7167
+ init_sync();
7168
+ }
7169
+ });
7170
+
7171
+ // src/daemon/service.ts
7172
+ import fs19 from "fs";
7173
+ import path22 from "path";
7174
+ import os16 from "os";
7175
+ import { spawnSync as spawnSync3, execFileSync } from "child_process";
7176
+ function resolveNode9Binary() {
7177
+ try {
7178
+ const script = process.argv[1];
7179
+ if (typeof script === "string" && path22.isAbsolute(script) && fs19.existsSync(script)) {
7180
+ return fs19.realpathSync(script);
7181
+ }
7182
+ } catch {
7183
+ }
7184
+ try {
7185
+ const cmd = process.platform === "win32" ? "where" : "which";
7186
+ const r = spawnSync3(cmd, ["node9"], { encoding: "utf8", timeout: 3e3 });
7187
+ if (r.status === 0 && r.stdout.trim()) {
7188
+ return r.stdout.trim().split("\n")[0].trim();
7189
+ }
7190
+ } catch {
7191
+ }
7192
+ return null;
7193
+ }
7194
+ function xmlEscape(s) {
7195
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
7196
+ }
7197
+ function launchdPlist(binaryPath) {
7198
+ const logDir = path22.join(os16.homedir(), ".node9");
7199
+ const nodePath = xmlEscape(process.execPath);
7200
+ const scriptPath = xmlEscape(binaryPath);
7201
+ const outLog = xmlEscape(path22.join(logDir, "daemon.log"));
7202
+ const errLog = xmlEscape(path22.join(logDir, "daemon-error.log"));
7203
+ return `<?xml version="1.0" encoding="UTF-8"?>
7204
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
7205
+ <plist version="1.0">
7206
+ <dict>
7207
+ <key>Label</key>
7208
+ <string>${LAUNCHD_LABEL}</string>
7209
+ <key>ProgramArguments</key>
7210
+ <array>
7211
+ <string>${nodePath}</string>
7212
+ <string>${scriptPath}</string>
7213
+ <string>daemon</string>
7214
+ </array>
7215
+ <key>RunAtLoad</key>
7216
+ <true/>
7217
+ <key>KeepAlive</key>
7218
+ <true/>
7219
+ <key>ThrottleInterval</key>
7220
+ <integer>10</integer>
7221
+ <key>StandardOutPath</key>
7222
+ <string>${outLog}</string>
7223
+ <key>StandardErrorPath</key>
7224
+ <string>${errLog}</string>
7225
+ <key>EnvironmentVariables</key>
7226
+ <dict>
7227
+ <key>NODE9_AUTO_STARTED</key>
7228
+ <string>1</string>
7229
+ <key>NODE9_BROWSER_OPENED</key>
7230
+ <string>1</string>
7231
+ </dict>
7232
+ </dict>
7233
+ </plist>
7234
+ `;
7235
+ }
7236
+ function installLaunchd(binaryPath) {
7237
+ const dir = path22.dirname(LAUNCHD_PLIST);
7238
+ if (!fs19.existsSync(dir)) fs19.mkdirSync(dir, { recursive: true });
7239
+ fs19.writeFileSync(LAUNCHD_PLIST, launchdPlist(binaryPath), "utf-8");
7240
+ spawnSync3("launchctl", ["unload", LAUNCHD_PLIST], { encoding: "utf8" });
7241
+ const r = spawnSync3("launchctl", ["load", "-w", LAUNCHD_PLIST], {
7242
+ encoding: "utf8",
7243
+ timeout: 5e3
7244
+ });
7245
+ if (r.status !== 0) {
7246
+ throw new Error(`launchctl load failed: ${r.stderr || r.stdout || "unknown error"}`);
7247
+ }
7248
+ }
7249
+ function uninstallLaunchd() {
7250
+ if (fs19.existsSync(LAUNCHD_PLIST)) {
7251
+ spawnSync3("launchctl", ["unload", "-w", LAUNCHD_PLIST], { encoding: "utf8", timeout: 5e3 });
7252
+ fs19.unlinkSync(LAUNCHD_PLIST);
7253
+ }
7254
+ }
7255
+ function isLaunchdInstalled() {
7256
+ return fs19.existsSync(LAUNCHD_PLIST);
7257
+ }
7258
+ function systemdUnit(binaryPath) {
7259
+ return `[Unit]
7260
+ Description=node9 approval daemon
7261
+ After=network.target
7262
+
7263
+ [Service]
7264
+ Type=simple
7265
+ ExecStart=${process.execPath} ${binaryPath} daemon
7266
+ Restart=on-failure
7267
+ RestartSec=10s
7268
+ Environment=NODE9_AUTO_STARTED=1
7269
+ Environment=NODE9_BROWSER_OPENED=1
7270
+
7271
+ [Install]
7272
+ WantedBy=default.target
7273
+ `;
7274
+ }
7275
+ function installSystemd(binaryPath) {
7276
+ if (!fs19.existsSync(SYSTEMD_UNIT_DIR)) {
7277
+ fs19.mkdirSync(SYSTEMD_UNIT_DIR, { recursive: true });
7278
+ }
7279
+ fs19.writeFileSync(SYSTEMD_UNIT, systemdUnit(binaryPath), "utf-8");
7280
+ try {
7281
+ execFileSync("loginctl", ["enable-linger", os16.userInfo().username], { timeout: 3e3 });
7282
+ } catch {
7283
+ }
7284
+ const reload = spawnSync3("systemctl", ["--user", "daemon-reload"], {
7285
+ encoding: "utf8",
7286
+ timeout: 5e3
7287
+ });
7288
+ if (reload.status !== 0) {
7289
+ throw new Error(`systemctl daemon-reload failed: ${reload.stderr}`);
7290
+ }
7291
+ spawnSync3("systemctl", ["--user", "stop", "node9-daemon"], { encoding: "utf8", timeout: 3e3 });
7292
+ const enable = spawnSync3("systemctl", ["--user", "enable", "--now", "node9-daemon"], {
7293
+ encoding: "utf8",
7294
+ timeout: 5e3
7295
+ });
7296
+ if (enable.status !== 0) {
7297
+ throw new Error(`systemctl enable failed: ${enable.stderr}`);
7298
+ }
7299
+ }
7300
+ function uninstallSystemd() {
7301
+ if (fs19.existsSync(SYSTEMD_UNIT)) {
7302
+ spawnSync3("systemctl", ["--user", "disable", "--now", "node9-daemon"], {
7303
+ encoding: "utf8",
7304
+ timeout: 5e3
7305
+ });
7306
+ spawnSync3("systemctl", ["--user", "daemon-reload"], { encoding: "utf8", timeout: 5e3 });
7307
+ fs19.unlinkSync(SYSTEMD_UNIT);
7308
+ }
7309
+ }
7310
+ function isSystemdInstalled() {
7311
+ return fs19.existsSync(SYSTEMD_UNIT);
7312
+ }
7313
+ function stopRunningDaemon() {
7314
+ const pidFile = path22.join(os16.homedir(), ".node9", "daemon.pid");
7315
+ if (!fs19.existsSync(pidFile)) return;
7316
+ try {
7317
+ const data = JSON.parse(fs19.readFileSync(pidFile, "utf-8"));
7318
+ const pid = data.pid;
7319
+ const MAX_PID2 = 4194304;
7320
+ if (typeof pid === "number" && Number.isInteger(pid) && pid > 0 && pid <= MAX_PID2) {
7321
+ try {
7322
+ process.kill(pid, "SIGTERM");
7323
+ const deadline = Date.now() + 3e3;
7324
+ const pollStop = spawnSync3(
7325
+ "sh",
7326
+ ["-c", `while kill -0 ${pid} 2>/dev/null; do sleep 0.1; done`],
7327
+ {
7328
+ timeout: 3100
7329
+ }
7330
+ );
7331
+ void pollStop;
7332
+ void deadline;
7333
+ } catch {
7334
+ }
7335
+ }
7336
+ try {
7337
+ fs19.unlinkSync(pidFile);
7338
+ } catch {
7339
+ }
7340
+ } catch {
7341
+ }
7342
+ }
7343
+ function installDaemonService() {
7344
+ const binary = resolveNode9Binary();
7345
+ if (!binary) {
7346
+ return { ok: false, reason: "Could not locate the node9 binary. Is it in your PATH?" };
7347
+ }
7348
+ stopRunningDaemon();
7349
+ try {
7350
+ if (process.platform === "darwin") {
7351
+ const alreadyInstalled = isLaunchdInstalled();
7352
+ installLaunchd(binary);
7353
+ return { ok: true, platform: "launchd", alreadyInstalled };
7354
+ }
7355
+ if (process.platform === "linux") {
7356
+ const check = spawnSync3("systemctl", ["--user", "--version"], {
7357
+ encoding: "utf8",
7358
+ timeout: 2e3
7359
+ });
7360
+ if (check.status !== 0) {
7361
+ return {
7362
+ ok: false,
7363
+ reason: "systemd not available. Start the daemon manually with: node9 daemon start"
7364
+ };
7365
+ }
7366
+ const alreadyInstalled = isSystemdInstalled();
7367
+ installSystemd(binary);
7368
+ return { ok: true, platform: "systemd", alreadyInstalled };
7369
+ }
7370
+ return {
7371
+ ok: false,
7372
+ reason: `Automatic service install is not supported on ${process.platform}. Start the daemon manually with: node9 daemon start`
7373
+ };
7374
+ } catch (err2) {
7375
+ return {
7376
+ ok: false,
7377
+ reason: err2 instanceof Error ? err2.message : String(err2)
7378
+ };
7379
+ }
7380
+ }
7381
+ function uninstallDaemonService() {
7382
+ try {
7383
+ if (process.platform === "darwin") {
7384
+ uninstallLaunchd();
7385
+ return { ok: true, platform: "launchd", alreadyInstalled: false };
7386
+ }
7387
+ if (process.platform === "linux") {
7388
+ uninstallSystemd();
7389
+ return { ok: true, platform: "systemd", alreadyInstalled: false };
7390
+ }
7391
+ return {
7392
+ ok: false,
7393
+ reason: `Service management not supported on ${process.platform}.`
7394
+ };
7395
+ } catch (err2) {
7396
+ return {
7397
+ ok: false,
7398
+ reason: err2 instanceof Error ? err2.message : String(err2)
7399
+ };
7400
+ }
7401
+ }
7402
+ function isDaemonServiceInstalled() {
7403
+ if (process.platform === "darwin") return isLaunchdInstalled();
7404
+ if (process.platform === "linux") return isSystemdInstalled();
7405
+ return false;
7406
+ }
7407
+ var LAUNCHD_LABEL, LAUNCHD_PLIST, SYSTEMD_UNIT_DIR, SYSTEMD_UNIT;
7408
+ var init_service = __esm({
7409
+ "src/daemon/service.ts"() {
7410
+ "use strict";
7411
+ LAUNCHD_LABEL = "ai.node9.daemon";
7412
+ LAUNCHD_PLIST = path22.join(os16.homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
7413
+ SYSTEMD_UNIT_DIR = path22.join(os16.homedir(), ".config", "systemd", "user");
7414
+ SYSTEMD_UNIT = path22.join(SYSTEMD_UNIT_DIR, "node9-daemon.service");
6986
7415
  }
6987
7416
  });
6988
7417
 
6989
7418
  // src/daemon/index.ts
6990
- import fs18 from "fs";
7419
+ import fs20 from "fs";
6991
7420
  import chalk3 from "chalk";
6992
- import { spawnSync as spawnSync3 } from "child_process";
7421
+ import { spawnSync as spawnSync4 } from "child_process";
6993
7422
  function stopDaemon() {
6994
- if (!fs18.existsSync(DAEMON_PID_FILE)) return console.log(chalk3.yellow("Not running."));
7423
+ if (!fs20.existsSync(DAEMON_PID_FILE)) return console.log(chalk3.yellow("Not running."));
6995
7424
  try {
6996
- const { pid } = JSON.parse(fs18.readFileSync(DAEMON_PID_FILE, "utf-8"));
7425
+ const data = JSON.parse(fs20.readFileSync(DAEMON_PID_FILE, "utf-8"));
7426
+ const pid = data.pid;
7427
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || pid > MAX_PID) {
7428
+ console.log(chalk3.gray("Cleaned up invalid PID file."));
7429
+ return;
7430
+ }
6997
7431
  process.kill(pid, "SIGTERM");
6998
7432
  console.log(chalk3.green("\u2705 Stopped."));
6999
7433
  } catch {
7000
7434
  console.log(chalk3.gray("Cleaned up stale PID file."));
7001
7435
  } finally {
7002
7436
  try {
7003
- fs18.unlinkSync(DAEMON_PID_FILE);
7437
+ fs20.unlinkSync(DAEMON_PID_FILE);
7004
7438
  } catch {
7005
7439
  }
7006
7440
  }
7007
7441
  }
7008
7442
  function daemonStatus() {
7009
- if (fs18.existsSync(DAEMON_PID_FILE)) {
7443
+ const serviceInstalled = isDaemonServiceInstalled();
7444
+ const serviceLabel = serviceInstalled ? chalk3.green("installed (starts on login)") : chalk3.yellow("not installed \u2014 run: node9 daemon install");
7445
+ let processStatus;
7446
+ if (fs20.existsSync(DAEMON_PID_FILE)) {
7010
7447
  try {
7011
- const { pid } = JSON.parse(fs18.readFileSync(DAEMON_PID_FILE, "utf-8"));
7012
- process.kill(pid, 0);
7013
- console.log(chalk3.green("Node9 daemon: running"));
7014
- return;
7448
+ const data = JSON.parse(fs20.readFileSync(DAEMON_PID_FILE, "utf-8"));
7449
+ const pid = data.pid;
7450
+ const port = data.port;
7451
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || pid > MAX_PID) {
7452
+ processStatus = chalk3.yellow("not running (invalid PID file)");
7453
+ } else {
7454
+ process.kill(pid, 0);
7455
+ processStatus = chalk3.green(
7456
+ `running (PID ${pid}, port ${typeof port === "number" ? port : DAEMON_PORT})`
7457
+ );
7458
+ }
7015
7459
  } catch {
7016
- console.log(chalk3.yellow("Node9 daemon: not running (stale PID)"));
7017
- return;
7460
+ processStatus = chalk3.yellow("not running (stale PID file)");
7018
7461
  }
7019
- }
7020
- const r = spawnSync3("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
7021
- encoding: "utf8",
7022
- timeout: 500
7023
- });
7024
- if (r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`)) {
7025
- console.log(chalk3.yellow("Node9 daemon: running (no PID file \u2014 orphaned)"));
7026
7462
  } else {
7027
- console.log(chalk3.yellow("Node9 daemon: not running"));
7463
+ const r = spawnSync4("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
7464
+ encoding: "utf8",
7465
+ timeout: 500
7466
+ });
7467
+ if (r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`)) {
7468
+ processStatus = chalk3.yellow(`running (orphaned \u2014 no PID file)`);
7469
+ } else {
7470
+ processStatus = chalk3.yellow("not running");
7471
+ }
7028
7472
  }
7473
+ console.log(`
7474
+ Process : ${processStatus}`);
7475
+ console.log(` Service : ${serviceLabel}
7476
+ `);
7029
7477
  }
7478
+ var MAX_PID;
7030
7479
  var init_daemon2 = __esm({
7031
7480
  "src/daemon/index.ts"() {
7032
7481
  "use strict";
7033
7482
  init_server();
7034
7483
  init_state2();
7484
+ init_service();
7035
7485
  init_state2();
7486
+ init_service();
7487
+ MAX_PID = 4194304;
7036
7488
  }
7037
7489
  });
7038
7490
 
@@ -7042,10 +7494,10 @@ __export(tail_exports, {
7042
7494
  startTail: () => startTail
7043
7495
  });
7044
7496
  import http2 from "http";
7045
- import chalk19 from "chalk";
7046
- import fs29 from "fs";
7047
- import os25 from "os";
7048
- import path32 from "path";
7497
+ import chalk23 from "chalk";
7498
+ import fs33 from "fs";
7499
+ import os29 from "os";
7500
+ import path36 from "path";
7049
7501
  import readline5 from "readline";
7050
7502
  import { spawn as spawn10, execSync as execSync3 } from "child_process";
7051
7503
  function getIcon(tool) {
@@ -7068,22 +7520,22 @@ function formatBase(activity) {
7068
7520
  const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
7069
7521
  const icon = getIcon(activity.tool);
7070
7522
  const toolName = activity.tool.slice(0, 16).padEnd(16);
7071
- const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os25.homedir(), "~");
7523
+ const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os29.homedir(), "~");
7072
7524
  const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
7073
- return `${chalk19.gray(time)} ${icon} ${chalk19.white.bold(toolName)} ${chalk19.dim(argsPreview)}`;
7525
+ return `${chalk23.gray(time)} ${icon} ${chalk23.white.bold(toolName)} ${chalk23.dim(argsPreview)}`;
7074
7526
  }
7075
7527
  function renderResult(activity, result) {
7076
7528
  const base = formatBase(activity);
7077
7529
  let status;
7078
7530
  if (result.status === "allow") {
7079
- status = chalk19.green("\u2713 ALLOW");
7531
+ status = chalk23.green("\u2713 ALLOW");
7080
7532
  } else if (result.status === "dlp") {
7081
- status = chalk19.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
7533
+ status = chalk23.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
7082
7534
  } else {
7083
- status = chalk19.red("\u2717 BLOCK");
7535
+ status = chalk23.red("\u2717 BLOCK");
7084
7536
  }
7085
7537
  const cost = result.costEstimate ?? activity.costEstimate;
7086
- const costSuffix = cost == null ? "" : chalk19.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
7538
+ const costSuffix = cost == null ? "" : chalk23.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
7087
7539
  if (process.stdout.isTTY) {
7088
7540
  if (pendingShownForId === activity.id && pendingWrappedLines > 1) {
7089
7541
  readline5.moveCursor(process.stdout, 0, -(pendingWrappedLines - 1));
@@ -7100,19 +7552,19 @@ function renderResult(activity, result) {
7100
7552
  }
7101
7553
  function renderPending(activity) {
7102
7554
  if (!process.stdout.isTTY) return;
7103
- const line = `${formatBase(activity)} ${chalk19.yellow("\u25CF \u2026")}`;
7555
+ const line = `${formatBase(activity)} ${chalk23.yellow("\u25CF \u2026")}`;
7104
7556
  pendingShownForId = activity.id;
7105
7557
  pendingWrappedLines = wrappedLineCount(line);
7106
7558
  process.stdout.write(`${line}\r`);
7107
7559
  }
7108
7560
  async function ensureDaemon() {
7109
7561
  let pidPort = null;
7110
- if (fs29.existsSync(PID_FILE)) {
7562
+ if (fs33.existsSync(PID_FILE)) {
7111
7563
  try {
7112
- const { port } = JSON.parse(fs29.readFileSync(PID_FILE, "utf-8"));
7564
+ const { port } = JSON.parse(fs33.readFileSync(PID_FILE, "utf-8"));
7113
7565
  pidPort = port;
7114
7566
  } catch {
7115
- console.error(chalk19.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
7567
+ console.error(chalk23.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
7116
7568
  }
7117
7569
  }
7118
7570
  const checkPort = pidPort ?? DAEMON_PORT;
@@ -7123,7 +7575,7 @@ async function ensureDaemon() {
7123
7575
  if (res.ok) return checkPort;
7124
7576
  } catch {
7125
7577
  }
7126
- console.log(chalk19.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
7578
+ console.log(chalk23.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
7127
7579
  const child = spawn10(process.execPath, [process.argv[1], "daemon"], {
7128
7580
  detached: true,
7129
7581
  stdio: "ignore",
@@ -7140,7 +7592,7 @@ async function ensureDaemon() {
7140
7592
  } catch {
7141
7593
  }
7142
7594
  }
7143
- console.error(chalk19.red("\u274C Daemon failed to start. Try: node9 daemon start"));
7595
+ console.error(chalk23.red("\u274C Daemon failed to start. Try: node9 daemon start"));
7144
7596
  process.exit(1);
7145
7597
  }
7146
7598
  function postDecisionHttp(id, decision, csrfToken, port, opts) {
@@ -7172,27 +7624,56 @@ function postDecisionHttp(id, decision, csrfToken, port, opts) {
7172
7624
  req.end(body);
7173
7625
  });
7174
7626
  }
7627
+ function extractArgsSummary(toolName, args) {
7628
+ const a = args;
7629
+ if (!a) return "";
7630
+ const cmd = a["command"] ?? a["cmd"];
7631
+ if (typeof cmd === "string") return cmd.replace(/\s+/g, " ").trim();
7632
+ const fp = a["file_path"] ?? a["path"] ?? a["filepath"];
7633
+ if (typeof fp === "string") return fp;
7634
+ const q = a["query"] ?? a["sql"];
7635
+ if (typeof q === "string") return q.replace(/\s+/g, " ").trim();
7636
+ const s = JSON.stringify(a).replace(/\s+/g, " ");
7637
+ return s.length > 80 ? s.slice(0, 80) + "\u2026" : s;
7638
+ }
7639
+ function cleanReason(raw) {
7640
+ return raw.replace(/\s*[—–-]+\s*(blocked by|requires human approval)[^)]*(\([^)]*\))?\.?\s*$/i, "").trim();
7641
+ }
7642
+ function cleanBlockedBy(raw) {
7643
+ const shieldMatch = raw.match(/shield:([^:]+):/i);
7644
+ if (shieldMatch) return shieldMatch[1];
7645
+ const smartMatch = raw.match(/^Smart Rule:\s*(.+)$/i);
7646
+ if (smartMatch) return smartMatch[1];
7647
+ return raw;
7648
+ }
7175
7649
  function buildCardLines(req, localCount = 0) {
7176
7650
  if (req.recoveryCommand) {
7177
7651
  return buildRecoveryCardLines(req);
7178
7652
  }
7179
- const argsStr = JSON.stringify(req.args ?? {}).replace(/\s+/g, " ");
7180
- const argsPreview = argsStr.length > 60 ? argsStr.slice(0, 60) + "\u2026" : argsStr;
7181
- const tierLabel = req.riskMetadata?.tier != null ? req.riskMetadata.tier <= 2 ? `${YELLOW}\u26A0 Tier ${req.riskMetadata.tier}` : `${RED}\u{1F6D1} Tier ${req.riskMetadata.tier}` : `${YELLOW}\u26A0 Review`;
7182
- const blockedBy = req.riskMetadata?.blockedByLabel ?? "Policy rule";
7653
+ const argsSummary = extractArgsSummary(req.toolName, req.args);
7654
+ const argsPreview = argsSummary.length > 72 ? argsSummary.slice(0, 72) + "\u2026" : argsSummary;
7655
+ const rawBlockedBy = req.riskMetadata?.blockedByLabel ?? "Policy rule";
7656
+ const blockedBy = cleanBlockedBy(rawBlockedBy);
7657
+ const isBlock = req.riskMetadata?.tier != null && req.riskMetadata.tier <= 2;
7658
+ const severityIcon = isBlock ? `${RED}\u{1F6D1}` : `${YELLOW}\u26A0 `;
7659
+ const rawDesc = req.riskMetadata?.ruleDescription ?? "";
7660
+ const description = rawDesc ? cleanReason(rawDesc) : "";
7183
7661
  const lines = [
7184
7662
  ``,
7185
7663
  `${BOLD2}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET2}`,
7186
7664
  `${CYAN}\u2551${RESET2} Tool: ${BOLD2}${req.toolName}${RESET2}`,
7187
- `${CYAN}\u2551${RESET2} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET2}`
7665
+ `${CYAN}\u2551${RESET2} Policy: ${severityIcon} ${blockedBy}${RESET2}`
7188
7666
  ];
7189
- if (req.riskMetadata?.ruleDescription) {
7190
- lines.push(`${CYAN}\u2551${RESET2} ${YELLOW}\u2139 ${req.riskMetadata.ruleDescription}${RESET2}`);
7667
+ if (description) {
7668
+ lines.push(`${CYAN}\u2551${RESET2} Why: ${YELLOW}${description}${RESET2}`);
7191
7669
  }
7192
- if (req.riskMetadata?.ruleName && blockedBy.includes("Taint")) {
7670
+ if (req.riskMetadata?.ruleName && rawBlockedBy.includes("Taint")) {
7193
7671
  lines.push(`${CYAN}\u2551${RESET2} ${YELLOW}\u26A0 ${req.riskMetadata.ruleName}${RESET2}`);
7194
7672
  }
7195
- lines.push(`${CYAN}\u2551${RESET2} Args: ${GRAY}${argsPreview}${RESET2}`);
7673
+ if (argsPreview) {
7674
+ const argLabel = req.toolName.toLowerCase().includes("bash") ? "Command" : "Args ";
7675
+ lines.push(`${CYAN}\u2551${RESET2} ${argLabel}: ${GRAY}${argsPreview}${RESET2}`);
7676
+ }
7196
7677
  if (localCount >= 2) {
7197
7678
  lines.push(
7198
7679
  `${CYAN}\u2551${RESET2} ${YELLOW}\u{1F4A1}${RESET2} Approved ${localCount}\xD7 before \u2014 ${BOLD2}[a]${RESET2}${YELLOW} creates a permanent rule${RESET2}`
@@ -7232,9 +7713,9 @@ function buildRecoveryCardLines(req) {
7232
7713
  ];
7233
7714
  }
7234
7715
  function readApproversFromDisk() {
7235
- const configPath = path32.join(os25.homedir(), ".node9", "config.json");
7716
+ const configPath = path36.join(os29.homedir(), ".node9", "config.json");
7236
7717
  try {
7237
- const raw = JSON.parse(fs29.readFileSync(configPath, "utf-8"));
7718
+ const raw = JSON.parse(fs33.readFileSync(configPath, "utf-8"));
7238
7719
  const settings = raw.settings ?? {};
7239
7720
  return settings.approvers ?? {};
7240
7721
  } catch {
@@ -7245,20 +7726,20 @@ function approverStatusLine() {
7245
7726
  const a = readApproversFromDisk();
7246
7727
  const fmt = (label, key) => {
7247
7728
  const on = a[key] !== false;
7248
- return `[${key[0]}]${label.slice(1)} ${on ? chalk19.green("\u2713") : chalk19.dim("\u2717")}`;
7729
+ return `[${key[0]}]${label.slice(1)} ${on ? chalk23.green("\u2713") : chalk23.dim("\u2717")}`;
7249
7730
  };
7250
7731
  return `${fmt("native", "native")} ${fmt("browser", "browser")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
7251
7732
  }
7252
7733
  function toggleApprover(channel) {
7253
- const configPath = path32.join(os25.homedir(), ".node9", "config.json");
7734
+ const configPath = path36.join(os29.homedir(), ".node9", "config.json");
7254
7735
  try {
7255
- const raw = JSON.parse(fs29.readFileSync(configPath, "utf-8"));
7736
+ const raw = JSON.parse(fs33.readFileSync(configPath, "utf-8"));
7256
7737
  const settings = raw.settings ?? {};
7257
7738
  const approvers = settings.approvers ?? {};
7258
7739
  approvers[channel] = approvers[channel] === false;
7259
7740
  settings.approvers = approvers;
7260
7741
  raw.settings = settings;
7261
- fs29.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
7742
+ fs33.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
7262
7743
  } catch (err2) {
7263
7744
  process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
7264
7745
  `);
@@ -7290,7 +7771,7 @@ async function startTail(options = {}) {
7290
7771
  req2.end();
7291
7772
  });
7292
7773
  if (result.ok) {
7293
- console.log(chalk19.green("\u2713 Flight Recorder buffer cleared."));
7774
+ console.log(chalk23.green("\u2713 Flight Recorder buffer cleared."));
7294
7775
  } else if (result.code === "ECONNREFUSED") {
7295
7776
  throw new Error("Daemon is not running. Start it with: node9 daemon start");
7296
7777
  } else if (result.code === "ETIMEDOUT") {
@@ -7334,7 +7815,7 @@ async function startTail(options = {}) {
7334
7815
  const channel = name === "n" ? "native" : name === "b" ? "browser" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
7335
7816
  if (channel) {
7336
7817
  toggleApprover(channel);
7337
- console.log(chalk19.dim(` Approvers: ${approverStatusLine()}`));
7818
+ console.log(chalk23.dim(` Approvers: ${approverStatusLine()}`));
7338
7819
  }
7339
7820
  };
7340
7821
  process.stdin.on("keypress", idleKeypressHandler);
@@ -7400,7 +7881,7 @@ async function startTail(options = {}) {
7400
7881
  localAllowCounts.get(req2.toolName) ?? 0
7401
7882
  )
7402
7883
  );
7403
- const decisionStamp = action === "always-allow" ? chalk19.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk19.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk19.green("\u2713 ALLOWED") : action === "redirect" ? chalk19.yellow("\u21A9 REDIRECT AI") : chalk19.red("\u2717 DENIED");
7884
+ const decisionStamp = action === "always-allow" ? chalk23.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk23.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk23.green("\u2713 ALLOWED") : action === "redirect" ? chalk23.yellow("\u21A9 REDIRECT AI") : chalk23.red("\u2717 DENIED");
7404
7885
  stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
7405
7886
  for (const line of stampedLines) process.stdout.write(line + "\n");
7406
7887
  process.stdout.write(SHOW_CURSOR);
@@ -7428,8 +7909,8 @@ async function startTail(options = {}) {
7428
7909
  }
7429
7910
  postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err2) => {
7430
7911
  try {
7431
- fs29.appendFileSync(
7432
- path32.join(os25.homedir(), ".node9", "hook-debug.log"),
7912
+ fs33.appendFileSync(
7913
+ path36.join(os29.homedir(), ".node9", "hook-debug.log"),
7433
7914
  `[tail] POST /decision failed: ${String(err2)}
7434
7915
  `
7435
7916
  );
@@ -7451,7 +7932,7 @@ async function startTail(options = {}) {
7451
7932
  );
7452
7933
  const stampedLines = buildCardLines(req2, priorCount);
7453
7934
  if (externalDecision) {
7454
- const source = externalDecision === "allow" ? chalk19.green("\u2713 ALLOWED") : chalk19.red("\u2717 DENIED");
7935
+ const source = externalDecision === "allow" ? chalk23.green("\u2713 ALLOWED") : chalk23.red("\u2717 DENIED");
7455
7936
  stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
7456
7937
  }
7457
7938
  for (const line of stampedLines) process.stdout.write(line + "\n");
@@ -7510,16 +7991,16 @@ async function startTail(options = {}) {
7510
7991
  }
7511
7992
  } catch {
7512
7993
  }
7513
- console.log(chalk19.cyan.bold(`
7514
- \u{1F6F0}\uFE0F Node9 tail `) + chalk19.dim(`\u2192 ${dashboardUrl}`));
7994
+ console.log(chalk23.cyan.bold(`
7995
+ \u{1F6F0}\uFE0F Node9 tail `) + chalk23.dim(`\u2192 ${dashboardUrl}`));
7515
7996
  if (canApprove) {
7516
- console.log(chalk19.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
7517
- console.log(chalk19.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
7997
+ console.log(chalk23.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
7998
+ console.log(chalk23.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
7518
7999
  }
7519
8000
  if (options.history) {
7520
- console.log(chalk19.dim("Showing history + live events.\n"));
8001
+ console.log(chalk23.dim("Showing history + live events.\n"));
7521
8002
  } else {
7522
- console.log(chalk19.dim("Showing live events only. Use --history to include past.\n"));
8003
+ console.log(chalk23.dim("Showing live events only. Use --history to include past.\n"));
7523
8004
  }
7524
8005
  process.on("SIGINT", () => {
7525
8006
  exitIdleMode();
@@ -7529,13 +8010,13 @@ async function startTail(options = {}) {
7529
8010
  readline5.clearLine(process.stdout, 0);
7530
8011
  readline5.cursorTo(process.stdout, 0);
7531
8012
  }
7532
- console.log(chalk19.dim("\n\u{1F6F0}\uFE0F Disconnected."));
8013
+ console.log(chalk23.dim("\n\u{1F6F0}\uFE0F Disconnected."));
7533
8014
  process.exit(0);
7534
8015
  });
7535
8016
  const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
7536
8017
  const req = http2.get(sseUrl, (res) => {
7537
8018
  if (res.statusCode !== 200) {
7538
- console.error(chalk19.red(`Failed to connect: HTTP ${res.statusCode}`));
8019
+ console.error(chalk23.red(`Failed to connect: HTTP ${res.statusCode}`));
7539
8020
  process.exit(1);
7540
8021
  }
7541
8022
  if (canApprove) enterIdleMode();
@@ -7566,7 +8047,7 @@ async function startTail(options = {}) {
7566
8047
  readline5.clearLine(process.stdout, 0);
7567
8048
  readline5.cursorTo(process.stdout, 0);
7568
8049
  }
7569
- console.log(chalk19.red("\n\u274C Daemon disconnected."));
8050
+ console.log(chalk23.red("\n\u274C Daemon disconnected."));
7570
8051
  process.exit(1);
7571
8052
  });
7572
8053
  });
@@ -7658,9 +8139,9 @@ async function startTail(options = {}) {
7658
8139
  const hash = data.hash ?? "";
7659
8140
  const summary = data.argsSummary ?? data.tool;
7660
8141
  const fileCount = data.fileCount ?? 0;
7661
- const files = fileCount > 0 ? chalk19.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
8142
+ const files = fileCount > 0 ? chalk23.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
7662
8143
  process.stdout.write(
7663
- `${chalk19.dim(time)} ${chalk19.cyan("\u{1F4F8} snapshot")} ${chalk19.dim(hash)} ${summary}${files}
8144
+ `${chalk23.dim(time)} ${chalk23.cyan("\u{1F4F8} snapshot")} ${chalk23.dim(hash)} ${summary}${files}
7664
8145
  `
7665
8146
  );
7666
8147
  return;
@@ -7677,7 +8158,7 @@ async function startTail(options = {}) {
7677
8158
  }
7678
8159
  req.on("error", (err2) => {
7679
8160
  const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
7680
- console.error(chalk19.red(`
8161
+ console.error(chalk23.red(`
7681
8162
  \u274C ${msg}`));
7682
8163
  process.exit(1);
7683
8164
  });
@@ -7689,7 +8170,7 @@ var init_tail = __esm({
7689
8170
  init_daemon2();
7690
8171
  init_daemon();
7691
8172
  init_core();
7692
- PID_FILE = path32.join(os25.homedir(), ".node9", "daemon.pid");
8173
+ PID_FILE = path36.join(os29.homedir(), ".node9", "daemon.pid");
7693
8174
  ICONS = {
7694
8175
  bash: "\u{1F4BB}",
7695
8176
  shell: "\u{1F4BB}",
@@ -7730,9 +8211,9 @@ __export(hud_exports, {
7730
8211
  main: () => main,
7731
8212
  renderEnvironmentLine: () => renderEnvironmentLine
7732
8213
  });
7733
- import fs30 from "fs";
7734
- import path33 from "path";
7735
- import os26 from "os";
8214
+ import fs34 from "fs";
8215
+ import path37 from "path";
8216
+ import os30 from "os";
7736
8217
  import http3 from "http";
7737
8218
  async function readStdin() {
7738
8219
  const chunks = [];
@@ -7808,9 +8289,9 @@ function formatTimeLeft(resetsAt) {
7808
8289
  return ` (${m}m left)`;
7809
8290
  }
7810
8291
  function safeReadJson(filePath) {
7811
- if (!fs30.existsSync(filePath)) return null;
8292
+ if (!fs34.existsSync(filePath)) return null;
7812
8293
  try {
7813
- return JSON.parse(fs30.readFileSync(filePath, "utf-8"));
8294
+ return JSON.parse(fs34.readFileSync(filePath, "utf-8"));
7814
8295
  } catch {
7815
8296
  return null;
7816
8297
  }
@@ -7831,12 +8312,12 @@ function countHooksInFile(filePath) {
7831
8312
  return Object.keys(cfg.hooks).length;
7832
8313
  }
7833
8314
  function countRulesInDir(rulesDir) {
7834
- if (!fs30.existsSync(rulesDir)) return 0;
8315
+ if (!fs34.existsSync(rulesDir)) return 0;
7835
8316
  let count = 0;
7836
8317
  try {
7837
- for (const entry of fs30.readdirSync(rulesDir, { withFileTypes: true })) {
8318
+ for (const entry of fs34.readdirSync(rulesDir, { withFileTypes: true })) {
7838
8319
  if (entry.isDirectory()) {
7839
- count += countRulesInDir(path33.join(rulesDir, entry.name));
8320
+ count += countRulesInDir(path37.join(rulesDir, entry.name));
7840
8321
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
7841
8322
  count++;
7842
8323
  }
@@ -7847,46 +8328,46 @@ function countRulesInDir(rulesDir) {
7847
8328
  }
7848
8329
  function isSamePath(a, b) {
7849
8330
  try {
7850
- return path33.resolve(a) === path33.resolve(b);
8331
+ return path37.resolve(a) === path37.resolve(b);
7851
8332
  } catch {
7852
8333
  return false;
7853
8334
  }
7854
8335
  }
7855
8336
  function countConfigs(cwd) {
7856
- const homeDir2 = os26.homedir();
7857
- const claudeDir = path33.join(homeDir2, ".claude");
8337
+ const homeDir2 = os30.homedir();
8338
+ const claudeDir = path37.join(homeDir2, ".claude");
7858
8339
  let claudeMdCount = 0;
7859
8340
  let rulesCount = 0;
7860
8341
  let hooksCount = 0;
7861
8342
  const userMcpServers = /* @__PURE__ */ new Set();
7862
8343
  const projectMcpServers = /* @__PURE__ */ new Set();
7863
- if (fs30.existsSync(path33.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
7864
- rulesCount += countRulesInDir(path33.join(claudeDir, "rules"));
7865
- const userSettings = path33.join(claudeDir, "settings.json");
8344
+ if (fs34.existsSync(path37.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
8345
+ rulesCount += countRulesInDir(path37.join(claudeDir, "rules"));
8346
+ const userSettings = path37.join(claudeDir, "settings.json");
7866
8347
  for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
7867
8348
  hooksCount += countHooksInFile(userSettings);
7868
- const userClaudeJson = path33.join(homeDir2, ".claude.json");
8349
+ const userClaudeJson = path37.join(homeDir2, ".claude.json");
7869
8350
  for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
7870
8351
  for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
7871
8352
  userMcpServers.delete(name);
7872
8353
  }
7873
8354
  if (cwd) {
7874
- if (fs30.existsSync(path33.join(cwd, "CLAUDE.md"))) claudeMdCount++;
7875
- if (fs30.existsSync(path33.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
7876
- const projectClaudeDir = path33.join(cwd, ".claude");
8355
+ if (fs34.existsSync(path37.join(cwd, "CLAUDE.md"))) claudeMdCount++;
8356
+ if (fs34.existsSync(path37.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
8357
+ const projectClaudeDir = path37.join(cwd, ".claude");
7877
8358
  const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
7878
8359
  if (!overlapsUserScope) {
7879
- if (fs30.existsSync(path33.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
7880
- rulesCount += countRulesInDir(path33.join(projectClaudeDir, "rules"));
7881
- const projSettings = path33.join(projectClaudeDir, "settings.json");
8360
+ if (fs34.existsSync(path37.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
8361
+ rulesCount += countRulesInDir(path37.join(projectClaudeDir, "rules"));
8362
+ const projSettings = path37.join(projectClaudeDir, "settings.json");
7882
8363
  for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
7883
8364
  hooksCount += countHooksInFile(projSettings);
7884
8365
  }
7885
- if (fs30.existsSync(path33.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
7886
- const localSettings = path33.join(projectClaudeDir, "settings.local.json");
8366
+ if (fs34.existsSync(path37.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
8367
+ const localSettings = path37.join(projectClaudeDir, "settings.local.json");
7887
8368
  for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
7888
8369
  hooksCount += countHooksInFile(localSettings);
7889
- const mcpJsonServers = getMcpServerNames(path33.join(cwd, ".mcp.json"));
8370
+ const mcpJsonServers = getMcpServerNames(path37.join(cwd, ".mcp.json"));
7890
8371
  const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
7891
8372
  for (const name of disabledMcpJson) mcpJsonServers.delete(name);
7892
8373
  for (const name of mcpJsonServers) projectMcpServers.add(name);
@@ -7919,12 +8400,12 @@ function readActiveShieldsHud() {
7919
8400
  return shieldsCache.value;
7920
8401
  }
7921
8402
  try {
7922
- const shieldsPath = path33.join(os26.homedir(), ".node9", "shields.json");
7923
- if (!fs30.existsSync(shieldsPath)) {
8403
+ const shieldsPath = path37.join(os30.homedir(), ".node9", "shields.json");
8404
+ if (!fs34.existsSync(shieldsPath)) {
7924
8405
  shieldsCache = { value: [], ts: now };
7925
8406
  return [];
7926
8407
  }
7927
- const parsed = JSON.parse(fs30.readFileSync(shieldsPath, "utf-8"));
8408
+ const parsed = JSON.parse(fs34.readFileSync(shieldsPath, "utf-8"));
7928
8409
  if (!Array.isArray(parsed.active)) {
7929
8410
  shieldsCache = { value: [], ts: now };
7930
8411
  return [];
@@ -8026,17 +8507,17 @@ function renderContextLine(stdin) {
8026
8507
  async function main() {
8027
8508
  try {
8028
8509
  const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
8029
- if (fs30.existsSync(path33.join(os26.homedir(), ".node9", "hud-debug"))) {
8510
+ if (fs34.existsSync(path37.join(os30.homedir(), ".node9", "hud-debug"))) {
8030
8511
  try {
8031
- const logPath = path33.join(os26.homedir(), ".node9", "hud-debug.log");
8512
+ const logPath = path37.join(os30.homedir(), ".node9", "hud-debug.log");
8032
8513
  const MAX_LOG_SIZE = 10 * 1024 * 1024;
8033
8514
  let size = 0;
8034
8515
  try {
8035
- size = fs30.statSync(logPath).size;
8516
+ size = fs34.statSync(logPath).size;
8036
8517
  } catch {
8037
8518
  }
8038
8519
  if (size < MAX_LOG_SIZE) {
8039
- fs30.appendFileSync(
8520
+ fs34.appendFileSync(
8040
8521
  logPath,
8041
8522
  JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
8042
8523
  );
@@ -8057,11 +8538,11 @@ async function main() {
8057
8538
  try {
8058
8539
  const cwd = stdin.cwd ?? process.cwd();
8059
8540
  for (const configPath of [
8060
- path33.join(cwd, "node9.config.json"),
8061
- path33.join(os26.homedir(), ".node9", "config.json")
8541
+ path37.join(cwd, "node9.config.json"),
8542
+ path37.join(os30.homedir(), ".node9", "config.json")
8062
8543
  ]) {
8063
- if (!fs30.existsSync(configPath)) continue;
8064
- const cfg = JSON.parse(fs30.readFileSync(configPath, "utf-8"));
8544
+ if (!fs34.existsSync(configPath)) continue;
8545
+ const cfg = JSON.parse(fs34.readFileSync(configPath, "utf-8"));
8065
8546
  const hud = cfg.settings?.hud;
8066
8547
  if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
8067
8548
  }
@@ -8492,7 +8973,9 @@ function detectAgents(homeDir2 = os11.homedir()) {
8492
8973
  claude: exists(path15.join(homeDir2, ".claude")) || exists(path15.join(homeDir2, ".claude.json")),
8493
8974
  gemini: exists(path15.join(homeDir2, ".gemini")),
8494
8975
  cursor: exists(path15.join(homeDir2, ".cursor")),
8495
- codex: exists(path15.join(homeDir2, ".codex"))
8976
+ codex: exists(path15.join(homeDir2, ".codex")),
8977
+ windsurf: exists(path15.join(homeDir2, ".codeium", "windsurf")),
8978
+ vscode: exists(path15.join(homeDir2, ".vscode"))
8496
8979
  };
8497
8980
  }
8498
8981
  async function setupCursor() {
@@ -8641,6 +9124,38 @@ async function setupCodex() {
8641
9124
  printDaemonTip();
8642
9125
  }
8643
9126
  }
9127
+ function teardownCodex() {
9128
+ const homeDir2 = os11.homedir();
9129
+ const configPath = path15.join(homeDir2, ".codex", "config.toml");
9130
+ const config = readToml(configPath);
9131
+ if (!config?.mcp_servers) {
9132
+ console.log(chalk.blue(" \u2139\uFE0F ~/.codex/config.toml not found \u2014 nothing to remove"));
9133
+ return;
9134
+ }
9135
+ let changed = false;
9136
+ if (removeNode9McpServer(config.mcp_servers)) {
9137
+ changed = true;
9138
+ console.log(chalk.green(" \u2705 Removed node9 MCP server entry from ~/.codex/config.toml"));
9139
+ }
9140
+ for (const [name, server] of Object.entries(config.mcp_servers)) {
9141
+ const args = server.args;
9142
+ if (server.command === "node9" && Array.isArray(args) && args[0] === "mcp" && args[1] === "--upstream" && typeof args[2] === "string") {
9143
+ const [originalCmd, ...originalArgs] = args[2].split(" ");
9144
+ config.mcp_servers[name] = {
9145
+ ...server,
9146
+ command: originalCmd,
9147
+ args: originalArgs.length ? originalArgs : void 0
9148
+ };
9149
+ changed = true;
9150
+ }
9151
+ }
9152
+ if (changed) {
9153
+ writeToml(configPath, config);
9154
+ console.log(chalk.green(" \u2705 Unwrapped MCP servers in ~/.codex/config.toml"));
9155
+ } else {
9156
+ console.log(chalk.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in ~/.codex/config.toml"));
9157
+ }
9158
+ }
8644
9159
  function setupHud() {
8645
9160
  const homeDir2 = os11.homedir();
8646
9161
  const hooksPath = path15.join(homeDir2, ".claude", "settings.json");
@@ -8687,33 +9202,305 @@ function teardownHud() {
8687
9202
  console.log(chalk.green(" \u2705 node9 HUD removed from ~/.claude/settings.json"));
8688
9203
  console.log(chalk.gray(" Restart Claude Code for changes to take effect."));
8689
9204
  }
8690
-
8691
- // src/cli.ts
8692
- init_daemon2();
8693
- import chalk20 from "chalk";
8694
- import fs31 from "fs";
8695
- import path34 from "path";
8696
- import os27 from "os";
8697
- import { confirm as confirm2 } from "@inquirer/prompts";
8698
-
8699
- // src/utils/duration.ts
8700
- function parseDuration(str) {
8701
- const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
8702
- if (!m) return null;
8703
- const n = parseFloat(m[1]);
8704
- switch ((m[2] ?? "m").toLowerCase()) {
8705
- case "s":
8706
- return Math.round(n * 1e3);
8707
- case "m":
8708
- return Math.round(n * 6e4);
8709
- case "h":
8710
- return Math.round(n * 36e5);
8711
- case "d":
8712
- return Math.round(n * 864e5);
8713
- default:
8714
- return null;
8715
- }
8716
- }
9205
+ async function setupWindsurf() {
9206
+ const homeDir2 = os11.homedir();
9207
+ const mcpPath = path15.join(homeDir2, ".codeium", "windsurf", "mcp_config.json");
9208
+ const mcpConfig = readJson(mcpPath) ?? {};
9209
+ const servers = mcpConfig.mcpServers ?? {};
9210
+ let anythingChanged = false;
9211
+ if (!hasNode9McpServer(servers)) {
9212
+ servers["node9"] = NODE9_MCP_SERVER_ENTRY;
9213
+ mcpConfig.mcpServers = servers;
9214
+ writeJson(mcpPath, mcpConfig);
9215
+ console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
9216
+ anythingChanged = true;
9217
+ }
9218
+ const serversToWrap = [];
9219
+ for (const [name, server] of Object.entries(servers)) {
9220
+ if (!server.command || server.command === "node9") continue;
9221
+ serversToWrap.push({ name, upstream: [server.command, ...server.args ?? []].join(" ") });
9222
+ }
9223
+ if (serversToWrap.length > 0) {
9224
+ console.log(chalk.bold("The following existing entries will be modified:\n"));
9225
+ console.log(chalk.white(` ${mcpPath}`));
9226
+ for (const { name, upstream } of serversToWrap) {
9227
+ console.log(chalk.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
9228
+ }
9229
+ console.log("");
9230
+ const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
9231
+ if (proceed) {
9232
+ for (const { name, upstream } of serversToWrap) {
9233
+ servers[name] = {
9234
+ ...servers[name],
9235
+ command: "node9",
9236
+ args: ["mcp", "--upstream", upstream]
9237
+ };
9238
+ }
9239
+ mcpConfig.mcpServers = servers;
9240
+ writeJson(mcpPath, mcpConfig);
9241
+ console.log(chalk.green(`
9242
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
9243
+ anythingChanged = true;
9244
+ } else {
9245
+ console.log(chalk.yellow(" Skipped MCP server wrapping."));
9246
+ }
9247
+ console.log("");
9248
+ }
9249
+ console.log(
9250
+ chalk.yellow(
9251
+ " \u26A0\uFE0F Note: Windsurf does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Windsurf."
9252
+ )
9253
+ );
9254
+ console.log("");
9255
+ if (!anythingChanged && serversToWrap.length === 0) {
9256
+ console.log(chalk.blue("\u2139\uFE0F Node9 is already fully configured for Windsurf."));
9257
+ printDaemonTip();
9258
+ return;
9259
+ }
9260
+ if (anythingChanged) {
9261
+ console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Windsurf via MCP proxy!"));
9262
+ console.log(chalk.gray(" Restart Windsurf for changes to take effect."));
9263
+ printDaemonTip();
9264
+ }
9265
+ }
9266
+ function teardownWindsurf() {
9267
+ const homeDir2 = os11.homedir();
9268
+ const mcpPath = path15.join(homeDir2, ".codeium", "windsurf", "mcp_config.json");
9269
+ const mcpConfig = readJson(mcpPath);
9270
+ if (!mcpConfig?.mcpServers) {
9271
+ console.log(
9272
+ chalk.blue(" \u2139\uFE0F ~/.codeium/windsurf/mcp_config.json not found \u2014 nothing to remove")
9273
+ );
9274
+ return;
9275
+ }
9276
+ let changed = false;
9277
+ if (removeNode9McpServer(mcpConfig.mcpServers)) {
9278
+ changed = true;
9279
+ console.log(
9280
+ chalk.green(" \u2705 Removed node9 MCP server entry from ~/.codeium/windsurf/mcp_config.json")
9281
+ );
9282
+ }
9283
+ for (const [name, server] of Object.entries(mcpConfig.mcpServers)) {
9284
+ const args = server.args;
9285
+ if (server.command === "node9" && Array.isArray(args) && args[0] === "mcp" && args[1] === "--upstream" && typeof args[2] === "string") {
9286
+ const [originalCmd, ...originalArgs] = args[2].split(" ");
9287
+ mcpConfig.mcpServers[name] = {
9288
+ ...server,
9289
+ command: originalCmd,
9290
+ args: originalArgs.length ? originalArgs : void 0
9291
+ };
9292
+ changed = true;
9293
+ }
9294
+ }
9295
+ if (changed) {
9296
+ writeJson(mcpPath, mcpConfig);
9297
+ console.log(chalk.green(" \u2705 Unwrapped MCP servers in ~/.codeium/windsurf/mcp_config.json"));
9298
+ } else {
9299
+ console.log(
9300
+ chalk.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in ~/.codeium/windsurf/mcp_config.json")
9301
+ );
9302
+ }
9303
+ }
9304
+ function hasNode9McpServerVSCode(servers) {
9305
+ const entry = servers["node9"];
9306
+ return !!entry && entry.command === "node9" && Array.isArray(entry.args) && entry.args[0] === "mcp-server";
9307
+ }
9308
+ async function setupVSCode() {
9309
+ const homeDir2 = os11.homedir();
9310
+ const mcpPath = path15.join(homeDir2, ".vscode", "mcp.json");
9311
+ const mcpConfig = readJson(mcpPath) ?? {};
9312
+ const servers = mcpConfig.servers ?? {};
9313
+ let anythingChanged = false;
9314
+ if (!hasNode9McpServerVSCode(servers)) {
9315
+ servers["node9"] = { type: "stdio", command: "node9", args: ["mcp-server"] };
9316
+ mcpConfig.servers = servers;
9317
+ writeJson(mcpPath, mcpConfig);
9318
+ console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
9319
+ anythingChanged = true;
9320
+ }
9321
+ const serversToWrap = [];
9322
+ for (const [name, server] of Object.entries(servers)) {
9323
+ if (!server.command || server.command === "node9") continue;
9324
+ serversToWrap.push({ name, upstream: [server.command, ...server.args ?? []].join(" ") });
9325
+ }
9326
+ if (serversToWrap.length > 0) {
9327
+ console.log(chalk.bold("The following existing entries will be modified:\n"));
9328
+ console.log(chalk.white(` ${mcpPath}`));
9329
+ for (const { name, upstream } of serversToWrap) {
9330
+ console.log(chalk.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
9331
+ }
9332
+ console.log("");
9333
+ const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
9334
+ if (proceed) {
9335
+ for (const { name, upstream } of serversToWrap) {
9336
+ servers[name] = {
9337
+ ...servers[name],
9338
+ type: "stdio",
9339
+ command: "node9",
9340
+ args: ["mcp", "--upstream", upstream]
9341
+ };
9342
+ }
9343
+ mcpConfig.servers = servers;
9344
+ writeJson(mcpPath, mcpConfig);
9345
+ console.log(chalk.green(`
9346
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
9347
+ anythingChanged = true;
9348
+ } else {
9349
+ console.log(chalk.yellow(" Skipped MCP server wrapping."));
9350
+ }
9351
+ console.log("");
9352
+ }
9353
+ console.log(
9354
+ chalk.yellow(
9355
+ " \u26A0\uFE0F Note: VSCode MCP support requires the GitHub Copilot extension (v1.99+).\n Pre-execution hooks are not supported \u2014 MCP proxy wrapping only."
9356
+ )
9357
+ );
9358
+ console.log("");
9359
+ if (!anythingChanged && serversToWrap.length === 0) {
9360
+ console.log(chalk.blue("\u2139\uFE0F Node9 is already fully configured for VSCode."));
9361
+ printDaemonTip();
9362
+ return;
9363
+ }
9364
+ if (anythingChanged) {
9365
+ console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting VSCode via MCP proxy!"));
9366
+ console.log(chalk.gray(" Restart VSCode for changes to take effect."));
9367
+ printDaemonTip();
9368
+ }
9369
+ }
9370
+ function teardownVSCode() {
9371
+ const homeDir2 = os11.homedir();
9372
+ const mcpPath = path15.join(homeDir2, ".vscode", "mcp.json");
9373
+ const mcpConfig = readJson(mcpPath);
9374
+ if (!mcpConfig?.servers) {
9375
+ console.log(chalk.blue(" \u2139\uFE0F ~/.vscode/mcp.json not found \u2014 nothing to remove"));
9376
+ return;
9377
+ }
9378
+ let changed = false;
9379
+ if (hasNode9McpServerVSCode(mcpConfig.servers)) {
9380
+ delete mcpConfig.servers["node9"];
9381
+ changed = true;
9382
+ console.log(chalk.green(" \u2705 Removed node9 MCP server entry from ~/.vscode/mcp.json"));
9383
+ }
9384
+ for (const [name, server] of Object.entries(mcpConfig.servers)) {
9385
+ const args = server.args;
9386
+ if (server.command === "node9" && Array.isArray(args) && args[0] === "mcp" && args[1] === "--upstream" && typeof args[2] === "string") {
9387
+ const [originalCmd, ...originalArgs] = args[2].split(" ");
9388
+ mcpConfig.servers[name] = {
9389
+ ...server,
9390
+ type: "stdio",
9391
+ command: originalCmd,
9392
+ args: originalArgs.length ? originalArgs : void 0
9393
+ };
9394
+ changed = true;
9395
+ }
9396
+ }
9397
+ if (changed) {
9398
+ writeJson(mcpPath, mcpConfig);
9399
+ console.log(chalk.green(" \u2705 Unwrapped MCP servers in ~/.vscode/mcp.json"));
9400
+ } else {
9401
+ console.log(chalk.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in ~/.vscode/mcp.json"));
9402
+ }
9403
+ }
9404
+ function getAgentsStatus(homeDir2 = os11.homedir()) {
9405
+ const detected = detectAgents(homeDir2);
9406
+ const claudeWired = (() => {
9407
+ const settings = readJson(path15.join(homeDir2, ".claude", "settings.json"));
9408
+ return !!settings?.hooks?.PreToolUse?.some((m) => m.hooks.some((h) => isNode9Hook(h.command)));
9409
+ })();
9410
+ const geminiWired = (() => {
9411
+ const settings = readJson(path15.join(homeDir2, ".gemini", "settings.json"));
9412
+ return !!settings?.hooks?.BeforeTool?.some((m) => m.hooks.some((h) => isNode9Hook(h.command)));
9413
+ })();
9414
+ const cursorWired = (() => {
9415
+ const cfg = readJson(path15.join(homeDir2, ".cursor", "mcp.json"));
9416
+ return !!(cfg?.mcpServers && hasNode9McpServer(cfg.mcpServers));
9417
+ })();
9418
+ const codexWired = (() => {
9419
+ const cfg = readToml(path15.join(homeDir2, ".codex", "config.toml"));
9420
+ return !!(cfg?.mcp_servers && hasNode9McpServer(cfg.mcp_servers));
9421
+ })();
9422
+ const windsurfWired = (() => {
9423
+ const cfg = readJson(
9424
+ path15.join(homeDir2, ".codeium", "windsurf", "mcp_config.json")
9425
+ );
9426
+ return !!(cfg?.mcpServers && hasNode9McpServer(cfg.mcpServers));
9427
+ })();
9428
+ const vscodeWired = (() => {
9429
+ const cfg = readJson(path15.join(homeDir2, ".vscode", "mcp.json"));
9430
+ return !!(cfg?.servers && hasNode9McpServerVSCode(cfg.servers));
9431
+ })();
9432
+ return [
9433
+ {
9434
+ name: "claude",
9435
+ label: "Claude Code",
9436
+ installed: detected.claude,
9437
+ wired: claudeWired,
9438
+ mode: detected.claude ? "hooks" : null
9439
+ },
9440
+ {
9441
+ name: "gemini",
9442
+ label: "Gemini CLI",
9443
+ installed: detected.gemini,
9444
+ wired: geminiWired,
9445
+ mode: detected.gemini ? "hooks" : null
9446
+ },
9447
+ {
9448
+ name: "cursor",
9449
+ label: "Cursor",
9450
+ installed: detected.cursor,
9451
+ wired: cursorWired,
9452
+ mode: detected.cursor ? "mcp" : null
9453
+ },
9454
+ {
9455
+ name: "windsurf",
9456
+ label: "Windsurf",
9457
+ installed: detected.windsurf,
9458
+ wired: windsurfWired,
9459
+ mode: detected.windsurf ? "mcp" : null
9460
+ },
9461
+ {
9462
+ name: "vscode",
9463
+ label: "VSCode",
9464
+ installed: detected.vscode,
9465
+ wired: vscodeWired,
9466
+ mode: detected.vscode ? "mcp" : null
9467
+ },
9468
+ {
9469
+ name: "codex",
9470
+ label: "Codex",
9471
+ installed: detected.codex,
9472
+ wired: codexWired,
9473
+ mode: detected.codex ? "mcp" : null
9474
+ }
9475
+ ];
9476
+ }
9477
+
9478
+ // src/cli.ts
9479
+ init_daemon2();
9480
+ import chalk24 from "chalk";
9481
+ import fs35 from "fs";
9482
+ import path38 from "path";
9483
+ import os31 from "os";
9484
+ import { confirm as confirm2 } from "@inquirer/prompts";
9485
+
9486
+ // src/utils/duration.ts
9487
+ function parseDuration(str) {
9488
+ const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
9489
+ if (!m) return null;
9490
+ const n = parseFloat(m[1]);
9491
+ switch ((m[2] ?? "m").toLowerCase()) {
9492
+ case "s":
9493
+ return Math.round(n * 1e3);
9494
+ case "m":
9495
+ return Math.round(n * 6e4);
9496
+ case "h":
9497
+ return Math.round(n * 36e5);
9498
+ case "d":
9499
+ return Math.round(n * 864e5);
9500
+ default:
9501
+ return null;
9502
+ }
9503
+ }
8717
9504
 
8718
9505
  // src/proxy/index.ts
8719
9506
  init_orchestrator();
@@ -8922,19 +9709,19 @@ init_daemon();
8922
9709
  init_config();
8923
9710
  init_policy();
8924
9711
  import chalk5 from "chalk";
8925
- import fs20 from "fs";
9712
+ import fs22 from "fs";
8926
9713
  import { spawn as spawn6 } from "child_process";
8927
- import path22 from "path";
8928
- import os16 from "os";
9714
+ import path24 from "path";
9715
+ import os18 from "os";
8929
9716
 
8930
9717
  // src/undo.ts
8931
- import { spawnSync as spawnSync4, spawn as spawn5 } from "child_process";
9718
+ import { spawnSync as spawnSync5, spawn as spawn5 } from "child_process";
8932
9719
  import crypto3 from "crypto";
8933
- import fs19 from "fs";
9720
+ import fs21 from "fs";
8934
9721
  import net3 from "net";
8935
- import path21 from "path";
8936
- import os15 from "os";
8937
- var ACTIVITY_SOCKET_PATH3 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path21.join(os15.tmpdir(), "node9-activity.sock");
9722
+ import path23 from "path";
9723
+ import os17 from "os";
9724
+ var ACTIVITY_SOCKET_PATH3 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path23.join(os17.tmpdir(), "node9-activity.sock");
8938
9725
  function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
8939
9726
  try {
8940
9727
  const payload = JSON.stringify({
@@ -8954,22 +9741,22 @@ function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
8954
9741
  } catch {
8955
9742
  }
8956
9743
  }
8957
- var SNAPSHOT_STACK_PATH = path21.join(os15.homedir(), ".node9", "snapshots.json");
8958
- var UNDO_LATEST_PATH = path21.join(os15.homedir(), ".node9", "undo_latest.txt");
9744
+ var SNAPSHOT_STACK_PATH = path23.join(os17.homedir(), ".node9", "snapshots.json");
9745
+ var UNDO_LATEST_PATH = path23.join(os17.homedir(), ".node9", "undo_latest.txt");
8959
9746
  var MAX_SNAPSHOTS = 10;
8960
9747
  var GIT_TIMEOUT = 15e3;
8961
9748
  function readStack() {
8962
9749
  try {
8963
- if (fs19.existsSync(SNAPSHOT_STACK_PATH))
8964
- return JSON.parse(fs19.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
9750
+ if (fs21.existsSync(SNAPSHOT_STACK_PATH))
9751
+ return JSON.parse(fs21.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
8965
9752
  } catch {
8966
9753
  }
8967
9754
  return [];
8968
9755
  }
8969
9756
  function writeStack(stack) {
8970
- const dir = path21.dirname(SNAPSHOT_STACK_PATH);
8971
- if (!fs19.existsSync(dir)) fs19.mkdirSync(dir, { recursive: true });
8972
- fs19.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
9757
+ const dir = path23.dirname(SNAPSHOT_STACK_PATH);
9758
+ if (!fs21.existsSync(dir)) fs21.mkdirSync(dir, { recursive: true });
9759
+ fs21.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
8973
9760
  }
8974
9761
  function extractFilePath(args) {
8975
9762
  if (!args || typeof args !== "object") return null;
@@ -8989,12 +9776,12 @@ function buildArgsSummary(tool, args) {
8989
9776
  return "";
8990
9777
  }
8991
9778
  function findProjectRoot(filePath) {
8992
- let dir = path21.dirname(filePath);
9779
+ let dir = path23.dirname(filePath);
8993
9780
  while (true) {
8994
- if (fs19.existsSync(path21.join(dir, ".git")) || fs19.existsSync(path21.join(dir, "package.json"))) {
9781
+ if (fs21.existsSync(path23.join(dir, ".git")) || fs21.existsSync(path23.join(dir, "package.json"))) {
8995
9782
  return dir;
8996
9783
  }
8997
- const parent = path21.dirname(dir);
9784
+ const parent = path23.dirname(dir);
8998
9785
  if (parent === dir) return process.cwd();
8999
9786
  dir = parent;
9000
9787
  }
@@ -9002,7 +9789,7 @@ function findProjectRoot(filePath) {
9002
9789
  function normalizeCwdForHash(cwd) {
9003
9790
  let normalized;
9004
9791
  try {
9005
- normalized = fs19.realpathSync(cwd);
9792
+ normalized = fs21.realpathSync(cwd);
9006
9793
  } catch {
9007
9794
  normalized = cwd;
9008
9795
  }
@@ -9012,16 +9799,16 @@ function normalizeCwdForHash(cwd) {
9012
9799
  }
9013
9800
  function getShadowRepoDir(cwd) {
9014
9801
  const hash = crypto3.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
9015
- return path21.join(os15.homedir(), ".node9", "snapshots", hash);
9802
+ return path23.join(os17.homedir(), ".node9", "snapshots", hash);
9016
9803
  }
9017
9804
  function cleanOrphanedIndexFiles(shadowDir) {
9018
9805
  try {
9019
9806
  const cutoff = Date.now() - 6e4;
9020
- for (const f of fs19.readdirSync(shadowDir)) {
9807
+ for (const f of fs21.readdirSync(shadowDir)) {
9021
9808
  if (f.startsWith("index_")) {
9022
- const fp = path21.join(shadowDir, f);
9809
+ const fp = path23.join(shadowDir, f);
9023
9810
  try {
9024
- if (fs19.statSync(fp).mtimeMs < cutoff) fs19.unlinkSync(fp);
9811
+ if (fs21.statSync(fp).mtimeMs < cutoff) fs21.unlinkSync(fp);
9025
9812
  } catch {
9026
9813
  }
9027
9814
  }
@@ -9033,7 +9820,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
9033
9820
  const hardcoded = [".git", ".node9"];
9034
9821
  const lines = [...hardcoded, ...ignorePaths].join("\n");
9035
9822
  try {
9036
- fs19.writeFileSync(path21.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
9823
+ fs21.writeFileSync(path23.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
9037
9824
  } catch {
9038
9825
  }
9039
9826
  }
@@ -9041,54 +9828,54 @@ function ensureShadowRepo(shadowDir, cwd) {
9041
9828
  cleanOrphanedIndexFiles(shadowDir);
9042
9829
  const normalizedCwd = normalizeCwdForHash(cwd);
9043
9830
  const shadowEnvBase = { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd };
9044
- const check = spawnSync4("git", ["rev-parse", "--git-dir"], {
9831
+ const check = spawnSync5("git", ["rev-parse", "--git-dir"], {
9045
9832
  env: shadowEnvBase,
9046
9833
  timeout: 3e3
9047
9834
  });
9048
9835
  if (check.status === 0) {
9049
- const ptPath = path21.join(shadowDir, "project-path.txt");
9836
+ const ptPath = path23.join(shadowDir, "project-path.txt");
9050
9837
  try {
9051
- const stored = fs19.readFileSync(ptPath, "utf8").trim();
9838
+ const stored = fs21.readFileSync(ptPath, "utf8").trim();
9052
9839
  if (stored === normalizedCwd) return true;
9053
9840
  if (process.env.NODE9_DEBUG === "1")
9054
9841
  console.error(
9055
9842
  `[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
9056
9843
  );
9057
- fs19.rmSync(shadowDir, { recursive: true, force: true });
9844
+ fs21.rmSync(shadowDir, { recursive: true, force: true });
9058
9845
  } catch {
9059
9846
  try {
9060
- fs19.writeFileSync(ptPath, normalizedCwd, "utf8");
9847
+ fs21.writeFileSync(ptPath, normalizedCwd, "utf8");
9061
9848
  } catch {
9062
9849
  }
9063
9850
  return true;
9064
9851
  }
9065
9852
  }
9066
9853
  try {
9067
- fs19.mkdirSync(shadowDir, { recursive: true });
9854
+ fs21.mkdirSync(shadowDir, { recursive: true });
9068
9855
  } catch {
9069
9856
  }
9070
- const init = spawnSync4("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
9857
+ const init = spawnSync5("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
9071
9858
  if (init.status !== 0 || init.error) {
9072
9859
  const reason = init.error ? init.error.message : init.stderr?.toString();
9073
9860
  if (process.env.NODE9_DEBUG === "1") console.error("[Node9] git init --bare failed:", reason);
9074
9861
  return false;
9075
9862
  }
9076
- const configFile = path21.join(shadowDir, "config");
9077
- spawnSync4("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
9863
+ const configFile = path23.join(shadowDir, "config");
9864
+ spawnSync5("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
9078
9865
  timeout: 3e3
9079
9866
  });
9080
- spawnSync4("git", ["config", "--file", configFile, "core.fsmonitor", "true"], {
9867
+ spawnSync5("git", ["config", "--file", configFile, "core.fsmonitor", "true"], {
9081
9868
  timeout: 3e3
9082
9869
  });
9083
9870
  try {
9084
- fs19.writeFileSync(path21.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
9871
+ fs21.writeFileSync(path23.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
9085
9872
  } catch {
9086
9873
  }
9087
9874
  return true;
9088
9875
  }
9089
9876
  function buildGitEnv(cwd) {
9090
9877
  const shadowDir = getShadowRepoDir(cwd);
9091
- const check = spawnSync4("git", ["rev-parse", "--git-dir"], {
9878
+ const check = spawnSync5("git", ["rev-parse", "--git-dir"], {
9092
9879
  env: { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd },
9093
9880
  timeout: 2e3
9094
9881
  });
@@ -9101,23 +9888,23 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
9101
9888
  let indexFile = null;
9102
9889
  try {
9103
9890
  const rawFilePath = extractFilePath(args);
9104
- const absFilePath = rawFilePath && path21.isAbsolute(rawFilePath) ? rawFilePath : null;
9891
+ const absFilePath = rawFilePath && path23.isAbsolute(rawFilePath) ? rawFilePath : null;
9105
9892
  const cwd = absFilePath ? findProjectRoot(absFilePath) : process.cwd();
9106
9893
  const shadowDir = getShadowRepoDir(cwd);
9107
9894
  if (!ensureShadowRepo(shadowDir, cwd)) return null;
9108
9895
  writeShadowExcludes(shadowDir, ignorePaths);
9109
- indexFile = path21.join(shadowDir, `index_${process.pid}_${Date.now()}`);
9896
+ indexFile = path23.join(shadowDir, `index_${process.pid}_${Date.now()}`);
9110
9897
  const shadowEnv = {
9111
9898
  ...process.env,
9112
9899
  GIT_DIR: shadowDir,
9113
9900
  GIT_WORK_TREE: cwd,
9114
9901
  GIT_INDEX_FILE: indexFile
9115
9902
  };
9116
- spawnSync4("git", ["add", "-A"], { env: shadowEnv, timeout: GIT_TIMEOUT });
9117
- const treeRes = spawnSync4("git", ["write-tree"], { env: shadowEnv, timeout: GIT_TIMEOUT });
9903
+ spawnSync5("git", ["add", "-A"], { env: shadowEnv, timeout: GIT_TIMEOUT });
9904
+ const treeRes = spawnSync5("git", ["write-tree"], { env: shadowEnv, timeout: GIT_TIMEOUT });
9118
9905
  const treeHash = treeRes.stdout?.toString().trim();
9119
9906
  if (!treeHash || treeRes.status !== 0) return null;
9120
- const commitRes = spawnSync4(
9907
+ const commitRes = spawnSync5(
9121
9908
  "git",
9122
9909
  ["commit-tree", treeHash, "-m", `Node9 AI Snapshot: ${(/* @__PURE__ */ new Date()).toISOString()}`],
9123
9910
  { env: shadowEnv, timeout: GIT_TIMEOUT }
@@ -9129,7 +9916,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
9129
9916
  let capturedFiles = [];
9130
9917
  let capturedDiff = null;
9131
9918
  if (prevEntry) {
9132
- const filesRes = spawnSync4("git", ["diff", "--name-only", prevEntry.hash, commitHash], {
9919
+ const filesRes = spawnSync5("git", ["diff", "--name-only", prevEntry.hash, commitHash], {
9133
9920
  env: shadowEnv,
9134
9921
  timeout: GIT_TIMEOUT
9135
9922
  });
@@ -9139,7 +9926,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
9139
9926
  if (capturedFiles.length === 0) {
9140
9927
  return prevEntry.hash;
9141
9928
  }
9142
- const diffRes = spawnSync4("git", ["diff", prevEntry.hash, commitHash], {
9929
+ const diffRes = spawnSync5("git", ["diff", prevEntry.hash, commitHash], {
9143
9930
  env: shadowEnv,
9144
9931
  timeout: GIT_TIMEOUT
9145
9932
  });
@@ -9147,7 +9934,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
9147
9934
  capturedDiff = diffRes.stdout?.toString() || null;
9148
9935
  }
9149
9936
  } else {
9150
- const filesRes = spawnSync4("git", ["ls-tree", "-r", "--name-only", commitHash], {
9937
+ const filesRes = spawnSync5("git", ["ls-tree", "-r", "--name-only", commitHash], {
9151
9938
  env: shadowEnv,
9152
9939
  timeout: GIT_TIMEOUT
9153
9940
  });
@@ -9178,7 +9965,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
9178
9965
  writeStack(stack);
9179
9966
  const entry = stack[stack.length - 1];
9180
9967
  notifySnapshotTaken(commitHash.slice(0, 7), tool, entry.argsSummary, capturedFiles.length);
9181
- fs19.writeFileSync(UNDO_LATEST_PATH, commitHash);
9968
+ fs21.writeFileSync(UNDO_LATEST_PATH, commitHash);
9182
9969
  if (shouldGc) {
9183
9970
  spawn5("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
9184
9971
  }
@@ -9189,7 +9976,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
9189
9976
  } finally {
9190
9977
  if (indexFile) {
9191
9978
  try {
9192
- fs19.unlinkSync(indexFile);
9979
+ fs21.unlinkSync(indexFile);
9193
9980
  } catch {
9194
9981
  }
9195
9982
  }
@@ -9201,14 +9988,14 @@ function getSnapshotHistory() {
9201
9988
  function computeUndoDiff(hash, cwd) {
9202
9989
  try {
9203
9990
  const env = buildGitEnv(cwd);
9204
- const statRes = spawnSync4("git", ["diff", hash, "--stat", "--", "."], {
9991
+ const statRes = spawnSync5("git", ["diff", hash, "--stat", "--", "."], {
9205
9992
  cwd,
9206
9993
  env,
9207
9994
  timeout: GIT_TIMEOUT
9208
9995
  });
9209
9996
  const stat = statRes.stdout?.toString().trim();
9210
9997
  if (!stat || statRes.status !== 0) return null;
9211
- const diffRes = spawnSync4("git", ["diff", hash, "--", "."], {
9998
+ const diffRes = spawnSync5("git", ["diff", hash, "--", "."], {
9212
9999
  cwd,
9213
10000
  env,
9214
10001
  timeout: GIT_TIMEOUT
@@ -9227,7 +10014,7 @@ function applyUndo(hash, cwd) {
9227
10014
  try {
9228
10015
  const dir = cwd ?? process.cwd();
9229
10016
  const env = buildGitEnv(dir);
9230
- const restore = spawnSync4("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
10017
+ const restore = spawnSync5("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
9231
10018
  cwd: dir,
9232
10019
  env,
9233
10020
  timeout: GIT_TIMEOUT
@@ -9239,7 +10026,7 @@ function applyUndo(hash, cwd) {
9239
10026
  }
9240
10027
  return false;
9241
10028
  }
9242
- const lsTree = spawnSync4("git", ["ls-tree", "-r", "--name-only", hash], {
10029
+ const lsTree = spawnSync5("git", ["ls-tree", "-r", "--name-only", hash], {
9243
10030
  cwd: dir,
9244
10031
  env,
9245
10032
  timeout: GIT_TIMEOUT
@@ -9258,16 +10045,16 @@ function applyUndo(hash, cwd) {
9258
10045
  `);
9259
10046
  return false;
9260
10047
  }
9261
- const tracked = spawnSync4("git", ["ls-files"], { cwd: dir, env, timeout: GIT_TIMEOUT }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
9262
- const untracked = spawnSync4("git", ["ls-files", "--others", "--exclude-standard"], {
10048
+ const tracked = spawnSync5("git", ["ls-files"], { cwd: dir, env, timeout: GIT_TIMEOUT }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
10049
+ const untracked = spawnSync5("git", ["ls-files", "--others", "--exclude-standard"], {
9263
10050
  cwd: dir,
9264
10051
  env,
9265
10052
  timeout: GIT_TIMEOUT
9266
10053
  }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
9267
10054
  for (const file of [...tracked, ...untracked]) {
9268
- const fullPath = path21.join(dir, file);
9269
- if (!snapshotFiles.has(file) && fs19.existsSync(fullPath)) {
9270
- fs19.unlinkSync(fullPath);
10055
+ const fullPath = path23.join(dir, file);
10056
+ if (!snapshotFiles.has(file) && fs21.existsSync(fullPath)) {
10057
+ fs21.unlinkSync(fullPath);
9271
10058
  }
9272
10059
  }
9273
10060
  return true;
@@ -9291,9 +10078,9 @@ function registerCheckCommand(program2) {
9291
10078
  } catch (err2) {
9292
10079
  const tempConfig = getConfig();
9293
10080
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
9294
- const logPath = path22.join(os16.homedir(), ".node9", "hook-debug.log");
10081
+ const logPath = path24.join(os18.homedir(), ".node9", "hook-debug.log");
9295
10082
  const errMsg = err2 instanceof Error ? err2.message : String(err2);
9296
- fs20.appendFileSync(
10083
+ fs22.appendFileSync(
9297
10084
  logPath,
9298
10085
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
9299
10086
  RAW: ${raw}
@@ -9306,13 +10093,13 @@ RAW: ${raw}
9306
10093
  if (config.settings.autoStartDaemon && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON) {
9307
10094
  try {
9308
10095
  const scriptPath = process.argv[1];
9309
- if (typeof scriptPath !== "string" || !path22.isAbsolute(scriptPath))
10096
+ if (typeof scriptPath !== "string" || !path24.isAbsolute(scriptPath))
9310
10097
  throw new Error("node9: argv[1] is not an absolute path");
9311
- const resolvedScript = fs20.realpathSync(scriptPath);
9312
- const expectedCli = fs20.realpathSync(path22.resolve(__dirname, "../../cli.js"));
9313
- if (resolvedScript !== expectedCli)
10098
+ const resolvedScript = fs22.realpathSync(scriptPath);
10099
+ const packageDist = fs22.realpathSync(path24.resolve(__dirname, "../.."));
10100
+ if (!resolvedScript.startsWith(packageDist + path24.sep) && resolvedScript !== packageDist)
9314
10101
  throw new Error(
9315
- "node9: daemon spawn aborted \u2014 argv[1] does not resolve to the node9 CLI"
10102
+ `node9: daemon spawn aborted \u2014 argv[1] (${resolvedScript}) is outside package dist (${packageDist})`
9316
10103
  );
9317
10104
  const safeEnv = { ...process.env };
9318
10105
  for (const key of [
@@ -9331,14 +10118,24 @@ RAW: ${raw}
9331
10118
  env: { ...safeEnv, NODE9_AUTO_STARTED: "1", NODE9_BROWSER_OPENED: "1" }
9332
10119
  });
9333
10120
  d.unref();
9334
- } catch {
10121
+ } catch (spawnErr) {
10122
+ const logPath = path24.join(os18.homedir(), ".node9", "hook-debug.log");
10123
+ const msg = spawnErr instanceof Error ? spawnErr.message : String(spawnErr);
10124
+ try {
10125
+ fs22.appendFileSync(
10126
+ logPath,
10127
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] daemon-autostart-failed: ${msg}
10128
+ `
10129
+ );
10130
+ } catch {
10131
+ }
9335
10132
  }
9336
10133
  }
9337
10134
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
9338
- const logPath = path22.join(os16.homedir(), ".node9", "hook-debug.log");
9339
- if (!fs20.existsSync(path22.dirname(logPath)))
9340
- fs20.mkdirSync(path22.dirname(logPath), { recursive: true });
9341
- fs20.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
10135
+ const logPath = path24.join(os18.homedir(), ".node9", "hook-debug.log");
10136
+ if (!fs22.existsSync(path24.dirname(logPath)))
10137
+ fs22.mkdirSync(path24.dirname(logPath), { recursive: true });
10138
+ fs22.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
9342
10139
  `);
9343
10140
  }
9344
10141
  const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
@@ -9351,8 +10148,8 @@ RAW: ${raw}
9351
10148
  const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
9352
10149
  let ttyFd = null;
9353
10150
  try {
9354
- ttyFd = fs20.openSync("/dev/tty", "w");
9355
- const writeTty = (line) => fs20.writeSync(ttyFd, line + "\n");
10151
+ ttyFd = fs22.openSync("/dev/tty", "w");
10152
+ const writeTty = (line) => fs22.writeSync(ttyFd, line + "\n");
9356
10153
  if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
9357
10154
  writeTty(chalk5.bgRed.white.bold(`
9358
10155
  \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
@@ -9371,7 +10168,7 @@ RAW: ${raw}
9371
10168
  } finally {
9372
10169
  if (ttyFd !== null)
9373
10170
  try {
9374
- fs20.closeSync(ttyFd);
10171
+ fs22.closeSync(ttyFd);
9375
10172
  } catch {
9376
10173
  }
9377
10174
  }
@@ -9403,7 +10200,7 @@ RAW: ${raw}
9403
10200
  if (shouldSnapshot(toolName, toolInput, config)) {
9404
10201
  await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
9405
10202
  }
9406
- const safeCwdForAuth = typeof payload.cwd === "string" && path22.isAbsolute(payload.cwd) ? payload.cwd : void 0;
10203
+ const safeCwdForAuth = typeof payload.cwd === "string" && path24.isAbsolute(payload.cwd) ? payload.cwd : void 0;
9407
10204
  const result = await authorizeHeadless(toolName, toolInput, meta, {
9408
10205
  cwd: safeCwdForAuth
9409
10206
  });
@@ -9415,12 +10212,12 @@ RAW: ${raw}
9415
10212
  }
9416
10213
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
9417
10214
  try {
9418
- const tty = fs20.openSync("/dev/tty", "w");
9419
- fs20.writeSync(
10215
+ const tty = fs22.openSync("/dev/tty", "w");
10216
+ fs22.writeSync(
9420
10217
  tty,
9421
10218
  chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
9422
10219
  );
9423
- fs20.closeSync(tty);
10220
+ fs22.closeSync(tty);
9424
10221
  } catch {
9425
10222
  }
9426
10223
  const daemonReady = await autoStartDaemonAndWait();
@@ -9447,9 +10244,9 @@ RAW: ${raw}
9447
10244
  });
9448
10245
  } catch (err2) {
9449
10246
  if (process.env.NODE9_DEBUG === "1") {
9450
- const logPath = path22.join(os16.homedir(), ".node9", "hook-debug.log");
10247
+ const logPath = path24.join(os18.homedir(), ".node9", "hook-debug.log");
9451
10248
  const errMsg = err2 instanceof Error ? err2.message : String(err2);
9452
- fs20.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
10249
+ fs22.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
9453
10250
  `);
9454
10251
  }
9455
10252
  process.exit(0);
@@ -9486,9 +10283,9 @@ RAW: ${raw}
9486
10283
  init_audit();
9487
10284
  init_config();
9488
10285
  init_policy();
9489
- import fs21 from "fs";
9490
- import path23 from "path";
9491
- import os17 from "os";
10286
+ import fs23 from "fs";
10287
+ import path25 from "path";
10288
+ import os19 from "os";
9492
10289
  init_daemon();
9493
10290
 
9494
10291
  // src/utils/cp-mv-parser.ts
@@ -9561,10 +10358,10 @@ function registerLogCommand(program2) {
9561
10358
  decision: "allowed",
9562
10359
  source: "post-hook"
9563
10360
  };
9564
- const logPath = path23.join(os17.homedir(), ".node9", "audit.log");
9565
- if (!fs21.existsSync(path23.dirname(logPath)))
9566
- fs21.mkdirSync(path23.dirname(logPath), { recursive: true });
9567
- fs21.appendFileSync(logPath, JSON.stringify(entry) + "\n");
10361
+ const logPath = path25.join(os19.homedir(), ".node9", "audit.log");
10362
+ if (!fs23.existsSync(path25.dirname(logPath)))
10363
+ fs23.mkdirSync(path25.dirname(logPath), { recursive: true });
10364
+ fs23.appendFileSync(logPath, JSON.stringify(entry) + "\n");
9568
10365
  if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
9569
10366
  const command = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
9570
10367
  if (command) {
@@ -9597,7 +10394,7 @@ function registerLogCommand(program2) {
9597
10394
  }
9598
10395
  }
9599
10396
  }
9600
- const safeCwd = typeof payload.cwd === "string" && path23.isAbsolute(payload.cwd) ? payload.cwd : void 0;
10397
+ const safeCwd = typeof payload.cwd === "string" && path25.isAbsolute(payload.cwd) ? payload.cwd : void 0;
9601
10398
  const config = getConfig(safeCwd);
9602
10399
  if (shouldSnapshot(tool, {}, config)) {
9603
10400
  await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
@@ -9606,9 +10403,9 @@ function registerLogCommand(program2) {
9606
10403
  const msg = err2 instanceof Error ? err2.message : String(err2);
9607
10404
  process.stderr.write(`[Node9] audit log error: ${msg}
9608
10405
  `);
9609
- const debugPath = path23.join(os17.homedir(), ".node9", "hook-debug.log");
10406
+ const debugPath = path25.join(os19.homedir(), ".node9", "hook-debug.log");
9610
10407
  try {
9611
- fs21.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
10408
+ fs23.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
9612
10409
  `);
9613
10410
  } catch {
9614
10411
  }
@@ -9638,10 +10435,10 @@ init_config();
9638
10435
  import chalk6 from "chalk";
9639
10436
 
9640
10437
  // src/utils/https-fetch.ts
9641
- import https from "https";
10438
+ import https2 from "https";
9642
10439
  function httpsFetch(url) {
9643
10440
  return new Promise((resolve, reject) => {
9644
- https.get(url, (res) => {
10441
+ https2.get(url, (res) => {
9645
10442
  if (res.statusCode !== 200) {
9646
10443
  reject(new Error(`HTTP ${String(res.statusCode)} for ${url}`));
9647
10444
  res.resume();
@@ -10009,13 +10806,13 @@ function registerConfigShowCommand(program2) {
10009
10806
  // src/cli/commands/doctor.ts
10010
10807
  init_daemon();
10011
10808
  import chalk7 from "chalk";
10012
- import fs22 from "fs";
10013
- import path24 from "path";
10014
- import os18 from "os";
10809
+ import fs24 from "fs";
10810
+ import path26 from "path";
10811
+ import os20 from "os";
10015
10812
  import { execSync as execSync2 } from "child_process";
10016
10813
  function registerDoctorCommand(program2, version2) {
10017
10814
  program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
10018
- const homeDir2 = os18.homedir();
10815
+ const homeDir2 = os20.homedir();
10019
10816
  let failures = 0;
10020
10817
  function pass(msg) {
10021
10818
  console.log(chalk7.green(" \u2705 ") + msg);
@@ -10064,10 +10861,10 @@ function registerDoctorCommand(program2, version2) {
10064
10861
  );
10065
10862
  }
10066
10863
  section("Configuration");
10067
- const globalConfigPath = path24.join(homeDir2, ".node9", "config.json");
10068
- if (fs22.existsSync(globalConfigPath)) {
10864
+ const globalConfigPath = path26.join(homeDir2, ".node9", "config.json");
10865
+ if (fs24.existsSync(globalConfigPath)) {
10069
10866
  try {
10070
- JSON.parse(fs22.readFileSync(globalConfigPath, "utf-8"));
10867
+ JSON.parse(fs24.readFileSync(globalConfigPath, "utf-8"));
10071
10868
  pass("~/.node9/config.json found and valid");
10072
10869
  } catch {
10073
10870
  fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
@@ -10075,10 +10872,10 @@ function registerDoctorCommand(program2, version2) {
10075
10872
  } else {
10076
10873
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
10077
10874
  }
10078
- const projectConfigPath = path24.join(process.cwd(), "node9.config.json");
10079
- if (fs22.existsSync(projectConfigPath)) {
10875
+ const projectConfigPath = path26.join(process.cwd(), "node9.config.json");
10876
+ if (fs24.existsSync(projectConfigPath)) {
10080
10877
  try {
10081
- JSON.parse(fs22.readFileSync(projectConfigPath, "utf-8"));
10878
+ JSON.parse(fs24.readFileSync(projectConfigPath, "utf-8"));
10082
10879
  pass("node9.config.json found and valid (project)");
10083
10880
  } catch {
10084
10881
  fail(
@@ -10087,8 +10884,8 @@ function registerDoctorCommand(program2, version2) {
10087
10884
  );
10088
10885
  }
10089
10886
  }
10090
- const credsPath = path24.join(homeDir2, ".node9", "credentials.json");
10091
- if (fs22.existsSync(credsPath)) {
10887
+ const credsPath = path26.join(homeDir2, ".node9", "credentials.json");
10888
+ if (fs24.existsSync(credsPath)) {
10092
10889
  pass("Cloud credentials found (~/.node9/credentials.json)");
10093
10890
  } else {
10094
10891
  warn(
@@ -10097,10 +10894,10 @@ function registerDoctorCommand(program2, version2) {
10097
10894
  );
10098
10895
  }
10099
10896
  section("Agent Hooks");
10100
- const claudeSettingsPath = path24.join(homeDir2, ".claude", "settings.json");
10101
- if (fs22.existsSync(claudeSettingsPath)) {
10897
+ const claudeSettingsPath = path26.join(homeDir2, ".claude", "settings.json");
10898
+ if (fs24.existsSync(claudeSettingsPath)) {
10102
10899
  try {
10103
- const cs = JSON.parse(fs22.readFileSync(claudeSettingsPath, "utf-8"));
10900
+ const cs = JSON.parse(fs24.readFileSync(claudeSettingsPath, "utf-8"));
10104
10901
  const hasHook = cs.hooks?.PreToolUse?.some(
10105
10902
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
10106
10903
  );
@@ -10116,10 +10913,10 @@ function registerDoctorCommand(program2, version2) {
10116
10913
  } else {
10117
10914
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
10118
10915
  }
10119
- const geminiSettingsPath = path24.join(homeDir2, ".gemini", "settings.json");
10120
- if (fs22.existsSync(geminiSettingsPath)) {
10916
+ const geminiSettingsPath = path26.join(homeDir2, ".gemini", "settings.json");
10917
+ if (fs24.existsSync(geminiSettingsPath)) {
10121
10918
  try {
10122
- const gs = JSON.parse(fs22.readFileSync(geminiSettingsPath, "utf-8"));
10919
+ const gs = JSON.parse(fs24.readFileSync(geminiSettingsPath, "utf-8"));
10123
10920
  const hasHook = gs.hooks?.BeforeTool?.some(
10124
10921
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
10125
10922
  );
@@ -10135,10 +10932,10 @@ function registerDoctorCommand(program2, version2) {
10135
10932
  } else {
10136
10933
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
10137
10934
  }
10138
- const cursorHooksPath = path24.join(homeDir2, ".cursor", "hooks.json");
10139
- if (fs22.existsSync(cursorHooksPath)) {
10935
+ const cursorHooksPath = path26.join(homeDir2, ".cursor", "hooks.json");
10936
+ if (fs24.existsSync(cursorHooksPath)) {
10140
10937
  try {
10141
- const cur = JSON.parse(fs22.readFileSync(cursorHooksPath, "utf-8"));
10938
+ const cur = JSON.parse(fs24.readFileSync(cursorHooksPath, "utf-8"));
10142
10939
  const hasHook = cur.hooks?.preToolUse?.some(
10143
10940
  (h) => h.command?.includes("node9") || h.command?.includes("cli.js")
10144
10941
  );
@@ -10176,9 +10973,9 @@ function registerDoctorCommand(program2, version2) {
10176
10973
 
10177
10974
  // src/cli/commands/audit.ts
10178
10975
  import chalk8 from "chalk";
10179
- import fs23 from "fs";
10180
- import path25 from "path";
10181
- import os19 from "os";
10976
+ import fs25 from "fs";
10977
+ import path27 from "path";
10978
+ import os21 from "os";
10182
10979
  function formatRelativeTime(timestamp) {
10183
10980
  const diff = Date.now() - new Date(timestamp).getTime();
10184
10981
  const sec = Math.floor(diff / 1e3);
@@ -10191,14 +10988,14 @@ function formatRelativeTime(timestamp) {
10191
10988
  }
10192
10989
  function registerAuditCommand(program2) {
10193
10990
  program2.command("audit").description("View local execution audit log").option("--tail <n>", "Number of entries to show", "20").option("--tool <pattern>", "Filter by tool name (substring match)").option("--deny", "Show only denied actions").option("--json", "Output raw JSON").action((options) => {
10194
- const logPath = path25.join(os19.homedir(), ".node9", "audit.log");
10195
- if (!fs23.existsSync(logPath)) {
10991
+ const logPath = path27.join(os21.homedir(), ".node9", "audit.log");
10992
+ if (!fs25.existsSync(logPath)) {
10196
10993
  console.log(
10197
10994
  chalk8.yellow("No audit logs found. Run node9 with an agent to generate entries.")
10198
10995
  );
10199
10996
  return;
10200
10997
  }
10201
- const raw = fs23.readFileSync(logPath, "utf-8");
10998
+ const raw = fs25.readFileSync(logPath, "utf-8");
10202
10999
  const lines = raw.split("\n").filter((l) => l.trim() !== "");
10203
11000
  let entries = lines.flatMap((line) => {
10204
11001
  try {
@@ -10252,9 +11049,9 @@ function registerAuditCommand(program2) {
10252
11049
 
10253
11050
  // src/cli/commands/report.ts
10254
11051
  import chalk9 from "chalk";
10255
- import fs24 from "fs";
10256
- import path26 from "path";
10257
- import os20 from "os";
11052
+ import fs26 from "fs";
11053
+ import path28 from "path";
11054
+ import os22 from "os";
10258
11055
  var TEST_COMMAND_RE3 = /(?:^|\s)(npm\s+(?:run\s+)?test|npx\s+(?:vitest|jest|mocha)|yarn\s+(?:run\s+)?test|pnpm\s+(?:run\s+)?test|vitest|jest|mocha|pytest|py\.test|cargo\s+test|go\s+test|bundle\s+exec\s+rspec|rspec|phpunit|dotnet\s+test)\b/i;
10259
11056
  function buildTestTimestamps(allEntries) {
10260
11057
  const testTs = /* @__PURE__ */ new Set();
@@ -10301,8 +11098,8 @@ function getDateRange(period) {
10301
11098
  }
10302
11099
  }
10303
11100
  function parseAuditLog(logPath) {
10304
- if (!fs24.existsSync(logPath)) return [];
10305
- const raw = fs24.readFileSync(logPath, "utf-8");
11101
+ if (!fs26.existsSync(logPath)) return [];
11102
+ const raw = fs26.readFileSync(logPath, "utf-8");
10306
11103
  return raw.split("\n").flatMap((line) => {
10307
11104
  if (!line.trim()) return [];
10308
11105
  try {
@@ -10328,9 +11125,9 @@ function colorBar(value, max, width) {
10328
11125
  const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
10329
11126
  return chalk9.cyan(s.slice(0, filled)) + chalk9.dim(s.slice(filled));
10330
11127
  }
10331
- function pct(num2, total) {
11128
+ function pct(num3, total) {
10332
11129
  if (total === 0) return "\u2013";
10333
- return Math.round(num2 / total * 100) + "%";
11130
+ return Math.round(num3 / total * 100) + "%";
10334
11131
  }
10335
11132
  function fmtDate(d) {
10336
11133
  const date = typeof d === "string" ? /* @__PURE__ */ new Date(d + "T12:00:00") : d;
@@ -10371,11 +11168,11 @@ function loadClaudeCost(start, end) {
10371
11168
  inputTokens: 0,
10372
11169
  cacheReadTokens: 0
10373
11170
  };
10374
- const projectsDir = path26.join(os20.homedir(), ".claude", "projects");
10375
- if (!fs24.existsSync(projectsDir)) return empty;
11171
+ const projectsDir = path28.join(os22.homedir(), ".claude", "projects");
11172
+ if (!fs26.existsSync(projectsDir)) return empty;
10376
11173
  let dirs;
10377
11174
  try {
10378
- dirs = fs24.readdirSync(projectsDir);
11175
+ dirs = fs26.readdirSync(projectsDir);
10379
11176
  } catch {
10380
11177
  return empty;
10381
11178
  }
@@ -10385,18 +11182,18 @@ function loadClaudeCost(start, end) {
10385
11182
  const byDay = /* @__PURE__ */ new Map();
10386
11183
  const byModel = /* @__PURE__ */ new Map();
10387
11184
  for (const proj of dirs) {
10388
- const projPath = path26.join(projectsDir, proj);
11185
+ const projPath = path28.join(projectsDir, proj);
10389
11186
  let files;
10390
11187
  try {
10391
- const stat = fs24.statSync(projPath);
11188
+ const stat = fs26.statSync(projPath);
10392
11189
  if (!stat.isDirectory()) continue;
10393
- files = fs24.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
11190
+ files = fs26.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
10394
11191
  } catch {
10395
11192
  continue;
10396
11193
  }
10397
11194
  for (const file of files) {
10398
11195
  try {
10399
- const raw = fs24.readFileSync(path26.join(projPath, file), "utf-8");
11196
+ const raw = fs26.readFileSync(path28.join(projPath, file), "utf-8");
10400
11197
  for (const line of raw.split("\n")) {
10401
11198
  if (!line.trim()) continue;
10402
11199
  let entry;
@@ -10439,7 +11236,7 @@ function registerReportCommand(program2) {
10439
11236
  const period = ["today", "7d", "30d", "month"].includes(
10440
11237
  options.period
10441
11238
  ) ? options.period : "7d";
10442
- const logPath = path26.join(os20.homedir(), ".node9", "audit.log");
11239
+ const logPath = path28.join(os22.homedir(), ".node9", "audit.log");
10443
11240
  const allEntries = parseAuditLog(logPath);
10444
11241
  if (allEntries.length === 0) {
10445
11242
  console.log(
@@ -10690,26 +11487,67 @@ init_daemon2();
10690
11487
  init_daemon();
10691
11488
  import chalk10 from "chalk";
10692
11489
  import { spawn as spawn7 } from "child_process";
11490
+ var VALID_ACTIONS = "start | stop | restart | status | install | uninstall";
10693
11491
  function registerDaemonCommand(program2) {
10694
- program2.command("daemon").description("Run the local approval server").argument("[action]", "start | stop | status (default: start)").option("-b, --background", "Start the daemon in the background (detached)").option("-o, --openui", "Start in background and open browser").option(
11492
+ program2.command("daemon").description("Manage the local approval daemon").argument("[action]", `${VALID_ACTIONS} (default: start)`).option("-b, --background", "Start the daemon in the background (detached)").option("-o, --openui", "Start in background and open browser").option(
10695
11493
  "-w, --watch",
10696
11494
  "Start daemon + open browser, stay alive permanently (Flight Recorder mode)"
10697
11495
  ).action(
10698
11496
  async (action, options) => {
10699
11497
  const cmd = (action ?? "start").toLowerCase();
10700
- if (cmd === "stop") return stopDaemon();
10701
- if (cmd === "status") return daemonStatus();
10702
- if (cmd !== "start" && action !== void 0) {
10703
- console.error(
10704
- chalk10.red(`Unknown daemon action: "${action}". Use: start | stop | status`)
10705
- );
10706
- process.exit(1);
11498
+ if (cmd === "install") {
11499
+ const result = installDaemonService();
11500
+ if (!result.ok) {
11501
+ console.error(chalk10.red(`\u2717 ${result.reason}`));
11502
+ process.exit(1);
11503
+ }
11504
+ if (result.alreadyInstalled) {
11505
+ console.log(chalk10.green(`\u2713 Daemon service reinstalled (${result.platform})`));
11506
+ } else {
11507
+ console.log(chalk10.green(`\u2713 Daemon installed as login service (${result.platform})`));
11508
+ console.log(chalk10.gray(" The daemon will now start automatically on login."));
11509
+ }
11510
+ process.exit(0);
10707
11511
  }
10708
- if (options.watch) {
10709
- process.env.NODE9_WATCH_MODE = "1";
10710
- setTimeout(() => {
10711
- openBrowserLocal();
10712
- console.log(chalk10.cyan(`\u{1F6F0}\uFE0F Flight Recorder: http://${DAEMON_HOST}:${DAEMON_PORT}/`));
11512
+ if (cmd === "uninstall") {
11513
+ const result = uninstallDaemonService();
11514
+ if (!result.ok) {
11515
+ console.error(chalk10.red(`\u2717 ${result.reason}`));
11516
+ process.exit(1);
11517
+ }
11518
+ console.log(chalk10.green(`\u2713 Daemon service removed (${result.platform})`));
11519
+ console.log(chalk10.gray(" The daemon will no longer start automatically on login."));
11520
+ console.log(chalk10.gray(" To stop the running daemon: node9 daemon stop"));
11521
+ process.exit(0);
11522
+ }
11523
+ if (cmd === "stop") return stopDaemon();
11524
+ if (cmd === "restart") {
11525
+ stopDaemon();
11526
+ await new Promise((r) => setTimeout(r, 500));
11527
+ const child = spawn7(process.execPath, [process.argv[1], "daemon"], {
11528
+ detached: true,
11529
+ stdio: "ignore",
11530
+ env: { ...process.env, NODE9_AUTO_STARTED: "1", NODE9_BROWSER_OPENED: "1" }
11531
+ });
11532
+ child.unref();
11533
+ if (child.pid) {
11534
+ console.log(chalk10.green(`\u2713 Daemon restarted (PID ${child.pid})`));
11535
+ } else {
11536
+ console.error(chalk10.red("\u2717 Failed to restart daemon \u2014 spawn returned no PID"));
11537
+ process.exit(1);
11538
+ }
11539
+ process.exit(0);
11540
+ }
11541
+ if (cmd === "status") return daemonStatus();
11542
+ if (cmd !== "start" && action !== void 0) {
11543
+ console.error(chalk10.red(`Unknown daemon action: "${action}". Use: ${VALID_ACTIONS}`));
11544
+ process.exit(1);
11545
+ }
11546
+ if (options.watch) {
11547
+ process.env.NODE9_WATCH_MODE = "1";
11548
+ setTimeout(() => {
11549
+ openBrowserLocal();
11550
+ console.log(chalk10.cyan(`\u{1F6F0}\uFE0F Flight Recorder: http://${DAEMON_HOST}:${DAEMON_PORT}/`));
10713
11551
  }, 600);
10714
11552
  startDaemon();
10715
11553
  return;
@@ -10753,12 +11591,12 @@ function registerDaemonCommand(program2) {
10753
11591
  init_core();
10754
11592
  init_daemon();
10755
11593
  import chalk11 from "chalk";
10756
- import fs25 from "fs";
10757
- import path27 from "path";
10758
- import os21 from "os";
11594
+ import fs27 from "fs";
11595
+ import path29 from "path";
11596
+ import os23 from "os";
10759
11597
  function readJson2(filePath) {
10760
11598
  try {
10761
- if (fs25.existsSync(filePath)) return JSON.parse(fs25.readFileSync(filePath, "utf-8"));
11599
+ if (fs27.existsSync(filePath)) return JSON.parse(fs27.readFileSync(filePath, "utf-8"));
10762
11600
  } catch {
10763
11601
  }
10764
11602
  return null;
@@ -10823,28 +11661,28 @@ function registerStatusCommand(program2) {
10823
11661
  console.log("");
10824
11662
  const modeLabel = settings.mode === "audit" ? chalk11.blue("audit") : settings.mode === "strict" ? chalk11.red("strict") : chalk11.white("standard");
10825
11663
  console.log(` Mode: ${modeLabel}`);
10826
- const projectConfig = path27.join(process.cwd(), "node9.config.json");
10827
- const globalConfig = path27.join(os21.homedir(), ".node9", "config.json");
11664
+ const projectConfig = path29.join(process.cwd(), "node9.config.json");
11665
+ const globalConfig = path29.join(os23.homedir(), ".node9", "config.json");
10828
11666
  console.log(
10829
- ` Local: ${fs25.existsSync(projectConfig) ? chalk11.green("Active (node9.config.json)") : chalk11.gray("Not present")}`
11667
+ ` Local: ${fs27.existsSync(projectConfig) ? chalk11.green("Active (node9.config.json)") : chalk11.gray("Not present")}`
10830
11668
  );
10831
11669
  console.log(
10832
- ` Global: ${fs25.existsSync(globalConfig) ? chalk11.green("Active (~/.node9/config.json)") : chalk11.gray("Not present")}`
11670
+ ` Global: ${fs27.existsSync(globalConfig) ? chalk11.green("Active (~/.node9/config.json)") : chalk11.gray("Not present")}`
10833
11671
  );
10834
11672
  if (mergedConfig.policy.sandboxPaths.length > 0) {
10835
11673
  console.log(
10836
11674
  ` Sandbox: ${chalk11.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
10837
11675
  );
10838
11676
  }
10839
- const homeDir2 = os21.homedir();
11677
+ const homeDir2 = os23.homedir();
10840
11678
  const claudeSettings = readJson2(
10841
- path27.join(homeDir2, ".claude", "settings.json")
11679
+ path29.join(homeDir2, ".claude", "settings.json")
10842
11680
  );
10843
- const claudeConfig = readJson2(path27.join(homeDir2, ".claude.json"));
11681
+ const claudeConfig = readJson2(path29.join(homeDir2, ".claude.json"));
10844
11682
  const geminiSettings = readJson2(
10845
- path27.join(homeDir2, ".gemini", "settings.json")
11683
+ path29.join(homeDir2, ".gemini", "settings.json")
10846
11684
  );
10847
- const cursorConfig = readJson2(path27.join(homeDir2, ".cursor", "mcp.json"));
11685
+ const cursorConfig = readJson2(path29.join(homeDir2, ".cursor", "mcp.json"));
10848
11686
  const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
10849
11687
  if (agentFound) {
10850
11688
  console.log("");
@@ -10904,11 +11742,12 @@ function registerStatusCommand(program2) {
10904
11742
  // src/cli/commands/init.ts
10905
11743
  init_core();
10906
11744
  import chalk12 from "chalk";
10907
- import fs26 from "fs";
10908
- import path28 from "path";
10909
- import os22 from "os";
10910
- import https2 from "https";
11745
+ import fs28 from "fs";
11746
+ import path30 from "path";
11747
+ import os24 from "os";
11748
+ import https3 from "https";
10911
11749
  init_shields();
11750
+ init_service();
10912
11751
  var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
10913
11752
  function fireTelemetryPing(agents) {
10914
11753
  try {
@@ -10918,7 +11757,7 @@ function fireTelemetryPing(agents) {
10918
11757
  os: process.platform,
10919
11758
  node9_version: process.env.npm_package_version ?? "unknown"
10920
11759
  });
10921
- const req = https2.request(
11760
+ const req = https3.request(
10922
11761
  {
10923
11762
  hostname: "api.node9.ai",
10924
11763
  path: "/api/v1/telemetry",
@@ -10965,15 +11804,15 @@ function registerInitCommand(program2) {
10965
11804
  }
10966
11805
  console.log("");
10967
11806
  }
10968
- const configPath = path28.join(os22.homedir(), ".node9", "config.json");
10969
- if (fs26.existsSync(configPath) && !options.force) {
11807
+ const configPath = path30.join(os24.homedir(), ".node9", "config.json");
11808
+ if (fs28.existsSync(configPath) && !options.force) {
10970
11809
  try {
10971
- const existing = JSON.parse(fs26.readFileSync(configPath, "utf-8"));
11810
+ const existing = JSON.parse(fs28.readFileSync(configPath, "utf-8"));
10972
11811
  const settings = existing.settings ?? {};
10973
11812
  if (settings.mode !== chosenMode) {
10974
11813
  settings.mode = chosenMode;
10975
11814
  existing.settings = settings;
10976
- fs26.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
11815
+ fs28.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
10977
11816
  console.log(chalk12.green(`\u2705 Mode updated: ${chosenMode}`));
10978
11817
  } else {
10979
11818
  console.log(chalk12.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
@@ -10986,9 +11825,9 @@ function registerInitCommand(program2) {
10986
11825
  ...DEFAULT_CONFIG,
10987
11826
  settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
10988
11827
  };
10989
- const dir = path28.dirname(configPath);
10990
- if (!fs26.existsSync(dir)) fs26.mkdirSync(dir, { recursive: true });
10991
- fs26.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
11828
+ const dir = path30.dirname(configPath);
11829
+ if (!fs28.existsSync(dir)) fs28.mkdirSync(dir, { recursive: true });
11830
+ fs28.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
10992
11831
  console.log(chalk12.green(`\u2705 Config created: ${configPath}`));
10993
11832
  console.log(chalk12.gray(` Mode: ${chosenMode}`));
10994
11833
  }
@@ -11000,9 +11839,13 @@ function registerInitCommand(program2) {
11000
11839
  );
11001
11840
  if (found.length === 0) {
11002
11841
  console.log(
11003
- chalk12.gray("No AI agents detected. Install Claude Code, Gemini CLI, Cursor, or Codex")
11842
+ chalk12.gray(
11843
+ "No AI agents detected. Install Claude Code, Gemini CLI, Cursor, Windsurf, VSCode, or Codex"
11844
+ )
11845
+ );
11846
+ console.log(
11847
+ chalk12.gray("then run: node9 agents add <claude|gemini|cursor|windsurf|vscode|codex>")
11004
11848
  );
11005
- console.log(chalk12.gray("then run: node9 addto <claude|gemini|cursor|codex>"));
11006
11849
  return;
11007
11850
  }
11008
11851
  console.log(chalk12.bold("Detected agents:"));
@@ -11016,6 +11859,32 @@ function registerInitCommand(program2) {
11016
11859
  else if (agent === "gemini") await setupGemini();
11017
11860
  else if (agent === "cursor") await setupCursor();
11018
11861
  else if (agent === "codex") await setupCodex();
11862
+ else if (agent === "windsurf") await setupWindsurf();
11863
+ else if (agent === "vscode") await setupVSCode();
11864
+ console.log("");
11865
+ }
11866
+ if ((process.platform === "darwin" || process.platform === "linux") && process.stdout.isTTY) {
11867
+ const alreadyInstalled = isDaemonServiceInstalled();
11868
+ if (!alreadyInstalled) {
11869
+ const { confirm: confirm3 } = await import("@inquirer/prompts");
11870
+ const installService = await confirm3({
11871
+ message: "Install daemon as a login service? (starts automatically on login)",
11872
+ default: true
11873
+ });
11874
+ if (installService) {
11875
+ const result = installDaemonService();
11876
+ if (result.ok) {
11877
+ console.log(
11878
+ chalk12.green(` \u2713 Daemon installed as login service (${result.platform})`)
11879
+ );
11880
+ } else {
11881
+ console.log(chalk12.yellow(` \u26A0\uFE0F Could not install service: ${result.reason}`));
11882
+ console.log(chalk12.gray(" You can try again later with: node9 daemon install"));
11883
+ }
11884
+ }
11885
+ } else {
11886
+ console.log(chalk12.green(" \u2713 Daemon login service already installed"));
11887
+ }
11019
11888
  console.log("");
11020
11889
  }
11021
11890
  {
@@ -11042,7 +11911,7 @@ function registerInitCommand(program2) {
11042
11911
  }
11043
11912
 
11044
11913
  // src/cli/commands/undo.ts
11045
- import path29 from "path";
11914
+ import path31 from "path";
11046
11915
  import chalk14 from "chalk";
11047
11916
 
11048
11917
  // src/tui/undo-navigator.ts
@@ -11201,7 +12070,7 @@ function findMatchingCwd(startDir, history) {
11201
12070
  let dir = startDir;
11202
12071
  while (true) {
11203
12072
  if (cwds.has(dir)) return dir;
11204
- const parent = path29.dirname(dir);
12073
+ const parent = path31.dirname(dir);
11205
12074
  if (parent === dir) return null;
11206
12075
  dir = parent;
11207
12076
  }
@@ -11330,7 +12199,7 @@ function registerUndoCommand(program2) {
11330
12199
  // src/cli/commands/watch.ts
11331
12200
  init_daemon();
11332
12201
  import chalk15 from "chalk";
11333
- import { spawn as spawn8, spawnSync as spawnSync5 } from "child_process";
12202
+ import { spawn as spawn8, spawnSync as spawnSync6 } from "child_process";
11334
12203
  function registerWatchCommand(program2) {
11335
12204
  program2.command("watch").description("Run a command under Node9 watch mode (daemon stays alive for the session)").argument("<command>", "Command to run").argument("[args...]", "Arguments for the command").action(async (cmd, args) => {
11336
12205
  let port = DAEMON_PORT;
@@ -11376,7 +12245,7 @@ function registerWatchCommand(program2) {
11376
12245
  "\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
11377
12246
  )
11378
12247
  );
11379
- const result = spawnSync5(cmd, args, {
12248
+ const result = spawnSync6(cmd, args, {
11380
12249
  stdio: "inherit",
11381
12250
  env: { ...process.env, NODE9_WATCH_MODE: "1" }
11382
12251
  });
@@ -11397,12 +12266,12 @@ import { execa as execa2 } from "execa";
11397
12266
  init_provenance();
11398
12267
 
11399
12268
  // src/mcp-pin.ts
11400
- import fs27 from "fs";
11401
- import path30 from "path";
11402
- import os23 from "os";
12269
+ import fs29 from "fs";
12270
+ import path32 from "path";
12271
+ import os25 from "os";
11403
12272
  import crypto4 from "crypto";
11404
12273
  function getPinsFilePath() {
11405
- return path30.join(os23.homedir(), ".node9", "mcp-pins.json");
12274
+ return path32.join(os25.homedir(), ".node9", "mcp-pins.json");
11406
12275
  }
11407
12276
  function hashToolDefinitions(tools) {
11408
12277
  const sorted = [...tools].sort((a, b) => {
@@ -11419,7 +12288,7 @@ function getServerKey(upstreamCommand) {
11419
12288
  function readMcpPinsSafe() {
11420
12289
  const filePath = getPinsFilePath();
11421
12290
  try {
11422
- const raw = fs27.readFileSync(filePath, "utf-8");
12291
+ const raw = fs29.readFileSync(filePath, "utf-8");
11423
12292
  if (!raw.trim()) {
11424
12293
  return { ok: false, reason: "corrupt", detail: "empty file" };
11425
12294
  }
@@ -11443,10 +12312,10 @@ function readMcpPins() {
11443
12312
  }
11444
12313
  function writeMcpPins(data) {
11445
12314
  const filePath = getPinsFilePath();
11446
- fs27.mkdirSync(path30.dirname(filePath), { recursive: true });
12315
+ fs29.mkdirSync(path32.dirname(filePath), { recursive: true });
11447
12316
  const tmp = `${filePath}.${crypto4.randomBytes(6).toString("hex")}.tmp`;
11448
- fs27.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
11449
- fs27.renameSync(tmp, filePath);
12317
+ fs29.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
12318
+ fs29.renameSync(tmp, filePath);
11450
12319
  }
11451
12320
  function checkPin(serverKey, currentHash) {
11452
12321
  const result = readMcpPinsSafe();
@@ -11818,9 +12687,9 @@ function registerMcpGatewayCommand(program2) {
11818
12687
 
11819
12688
  // src/mcp-server/index.ts
11820
12689
  import readline4 from "readline";
11821
- import fs28 from "fs";
11822
- import os24 from "os";
11823
- import path31 from "path";
12690
+ import fs30 from "fs";
12691
+ import os26 from "os";
12692
+ import path33 from "path";
11824
12693
  init_core();
11825
12694
  init_daemon();
11826
12695
  init_shields();
@@ -11995,13 +12864,13 @@ function handleStatus() {
11995
12864
  lines.push(`Active shields: ${activeShields.length > 0 ? activeShields.join(", ") : "none"}`);
11996
12865
  lines.push(`Smart rules: ${config.policy.smartRules.length} loaded`);
11997
12866
  lines.push(`DLP: ${config.policy.dlp?.enabled !== false ? "enabled" : "disabled"}`);
11998
- const projectConfig = path31.join(process.cwd(), "node9.config.json");
11999
- const globalConfig = path31.join(os24.homedir(), ".node9", "config.json");
12867
+ const projectConfig = path33.join(process.cwd(), "node9.config.json");
12868
+ const globalConfig = path33.join(os26.homedir(), ".node9", "config.json");
12000
12869
  lines.push(
12001
- `Project config (node9.config.json): ${fs28.existsSync(projectConfig) ? "present" : "not found"}`
12870
+ `Project config (node9.config.json): ${fs30.existsSync(projectConfig) ? "present" : "not found"}`
12002
12871
  );
12003
12872
  lines.push(
12004
- `Global config (~/.node9/config.json): ${fs28.existsSync(globalConfig) ? "present" : "not found"}`
12873
+ `Global config (~/.node9/config.json): ${fs30.existsSync(globalConfig) ? "present" : "not found"}`
12005
12874
  );
12006
12875
  return lines.join("\n");
12007
12876
  }
@@ -12075,21 +12944,21 @@ function handleShieldDisable(args) {
12075
12944
  writeActiveShields(active.filter((s) => s !== name));
12076
12945
  return `Shield "${name}" disabled.`;
12077
12946
  }
12078
- var GLOBAL_CONFIG_PATH2 = path31.join(os24.homedir(), ".node9", "config.json");
12947
+ var GLOBAL_CONFIG_PATH2 = path33.join(os26.homedir(), ".node9", "config.json");
12079
12948
  var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
12080
12949
  function readGlobalConfigRaw() {
12081
12950
  try {
12082
- if (fs28.existsSync(GLOBAL_CONFIG_PATH2)) {
12083
- return JSON.parse(fs28.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
12951
+ if (fs30.existsSync(GLOBAL_CONFIG_PATH2)) {
12952
+ return JSON.parse(fs30.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
12084
12953
  }
12085
12954
  } catch {
12086
12955
  }
12087
12956
  return {};
12088
12957
  }
12089
12958
  function writeGlobalConfigRaw(data) {
12090
- const dir = path31.dirname(GLOBAL_CONFIG_PATH2);
12091
- if (!fs28.existsSync(dir)) fs28.mkdirSync(dir, { recursive: true });
12092
- fs28.writeFileSync(GLOBAL_CONFIG_PATH2, JSON.stringify(data, null, 2) + "\n");
12959
+ const dir = path33.dirname(GLOBAL_CONFIG_PATH2);
12960
+ if (!fs30.existsSync(dir)) fs30.mkdirSync(dir, { recursive: true });
12961
+ fs30.writeFileSync(GLOBAL_CONFIG_PATH2, JSON.stringify(data, null, 2) + "\n");
12093
12962
  }
12094
12963
  function handleApproverList() {
12095
12964
  const config = getConfig();
@@ -12132,9 +13001,9 @@ function handleApproverSet(args) {
12132
13001
  }
12133
13002
  function handleAuditGet(args) {
12134
13003
  const limit = Math.min(typeof args.limit === "number" ? args.limit : 20, 100);
12135
- const auditPath = path31.join(os24.homedir(), ".node9", "audit.log");
12136
- if (!fs28.existsSync(auditPath)) return "No audit log found.";
12137
- const lines = fs28.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
13004
+ const auditPath = path33.join(os26.homedir(), ".node9", "audit.log");
13005
+ if (!fs30.existsSync(auditPath)) return "No audit log found.";
13006
+ const lines = fs30.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
12138
13007
  const recent = lines.slice(-limit);
12139
13008
  const entries = recent.map((line) => {
12140
13009
  try {
@@ -12452,25 +13321,950 @@ function registerMcpPinCommand(program2) {
12452
13321
  });
12453
13322
  }
12454
13323
 
13324
+ // src/cli/commands/sync.ts
13325
+ init_sync();
13326
+ import chalk19 from "chalk";
13327
+ function registerSyncCommand(program2) {
13328
+ const policy = program2.command("policy").description("Manage cloud policy rules");
13329
+ policy.command("sync").description("Sync cloud policy rules to local cache (~/.node9/rules-cache.json)").action(async () => {
13330
+ process.stdout.write(chalk19.cyan("Syncing cloud policy rules\u2026"));
13331
+ const result = await runCloudSync();
13332
+ process.stdout.write("\n");
13333
+ if (!result.ok) {
13334
+ console.error(chalk19.red(`\u2717 ${result.reason}`));
13335
+ process.exit(1);
13336
+ }
13337
+ console.log(
13338
+ chalk19.green(`\u2713 Synced ${result.rules} rule${result.rules === 1 ? "" : "s"} from cloud`)
13339
+ );
13340
+ console.log(chalk19.gray(` Cached at: ${result.fetchedAt}`));
13341
+ console.log(chalk19.gray(` File: ~/.node9/rules-cache.json`));
13342
+ });
13343
+ policy.command("show").description("List all cloud policy rules in the local cache").action(() => {
13344
+ const status = getCloudSyncStatus();
13345
+ if (!status.cached) {
13346
+ console.log(chalk19.yellow("\n No cloud rules cached \u2014 run: node9 policy sync\n"));
13347
+ return;
13348
+ }
13349
+ const rules = getCloudRules() ?? [];
13350
+ const age = Math.round((Date.now() - new Date(status.fetchedAt).getTime()) / 6e4);
13351
+ console.log(
13352
+ chalk19.bold(`
13353
+ Cloud policy rules`) + chalk19.gray(
13354
+ ` (${rules.length} rule${rules.length === 1 ? "" : "s"}, synced ${age}m ago)
13355
+ `
13356
+ )
13357
+ );
13358
+ if (rules.length === 0) {
13359
+ console.log(chalk19.gray(" No rules defined in cloud policy.\n"));
13360
+ return;
13361
+ }
13362
+ for (const rule of rules) {
13363
+ const r = rule;
13364
+ const verdictColor = r.verdict === "block" ? chalk19.red : r.verdict === "allow" ? chalk19.green : chalk19.yellow;
13365
+ console.log(
13366
+ ` ${verdictColor(
13367
+ String(r.verdict ?? "unknown").toUpperCase().padEnd(6)
13368
+ )} ${chalk19.white(String(r.name ?? "(unnamed)"))}`
13369
+ );
13370
+ if (r.reason) console.log(chalk19.gray(` ${String(r.reason)}`));
13371
+ }
13372
+ console.log("");
13373
+ });
13374
+ policy.command("status").description("Show current cloud policy cache status").action(() => {
13375
+ const s = getCloudSyncStatus();
13376
+ if (!s.cached) {
13377
+ console.log(chalk19.yellow("\n No cache yet \u2014 run: node9 policy sync\n"));
13378
+ } else {
13379
+ const age = Math.round((Date.now() - new Date(s.fetchedAt).getTime()) / 6e4);
13380
+ console.log(`
13381
+ Rules : ${chalk19.green(String(s.rules))} cloud rules loaded`);
13382
+ console.log(
13383
+ ` Synced : ${chalk19.gray(`${age} minute${age === 1 ? "" : "s"} ago`)} (${s.fetchedAt})
13384
+ `
13385
+ );
13386
+ }
13387
+ });
13388
+ }
13389
+
13390
+ // src/cli/commands/agents.ts
13391
+ import chalk20 from "chalk";
13392
+ var SETUP_FN = {
13393
+ claude: setupClaude,
13394
+ gemini: setupGemini,
13395
+ cursor: setupCursor,
13396
+ codex: setupCodex,
13397
+ windsurf: setupWindsurf,
13398
+ vscode: setupVSCode
13399
+ };
13400
+ var TEARDOWN_FN = {
13401
+ claude: teardownClaude,
13402
+ gemini: teardownGemini,
13403
+ cursor: teardownCursor,
13404
+ codex: teardownCodex,
13405
+ windsurf: teardownWindsurf,
13406
+ vscode: teardownVSCode
13407
+ };
13408
+ var AGENT_NAMES = Object.keys(SETUP_FN);
13409
+ function registerAgentsCommand(program2) {
13410
+ const agents = program2.command("agents").description("List and manage AI agent integrations");
13411
+ agents.command("list").description("Show all supported agents and their Node9 status").action(() => {
13412
+ const statuses = getAgentsStatus();
13413
+ const anyInstalled = statuses.some((s) => s.installed);
13414
+ console.log("");
13415
+ console.log(` ${"Agent".padEnd(14)}${"Installed".padEnd(11)}${"Wired".padEnd(8)}Mode`);
13416
+ console.log(" " + "\u2500".repeat(44));
13417
+ for (const s of statuses) {
13418
+ const installed = s.installed ? chalk20.green("\u2713") : chalk20.gray("\u2717");
13419
+ const wired = !s.installed ? chalk20.gray("\u2014") : s.wired ? chalk20.green("\u2713") : chalk20.yellow("\u2717");
13420
+ const mode = s.mode ? chalk20.gray(s.mode) : chalk20.gray("\u2014");
13421
+ const hint = s.installed && !s.wired ? chalk20.gray(` \u2190 node9 agents add ${s.name}`) : "";
13422
+ console.log(` ${s.label.padEnd(14)}${installed} ${wired} ${mode}${hint}`);
13423
+ }
13424
+ console.log("");
13425
+ if (!anyInstalled) {
13426
+ console.log(
13427
+ chalk20.gray(" No AI agents detected. Install Claude Code, Gemini CLI, Cursor,\n") + chalk20.gray(" Windsurf, VSCode, or Codex then run: node9 agents list\n")
13428
+ );
13429
+ return;
13430
+ }
13431
+ const unwired = statuses.filter((s) => s.installed && !s.wired);
13432
+ if (unwired.length > 0) {
13433
+ console.log(
13434
+ chalk20.yellow(` ${unwired.length} agent(s) not yet wired. Run: `) + chalk20.white(`node9 agents add ${unwired[0].name}`) + "\n"
13435
+ );
13436
+ }
13437
+ });
13438
+ agents.command("add").description("Wire Node9 into an agent").argument("<agent>", `Agent to wire: ${AGENT_NAMES.join(" | ")}`).action(async (agent) => {
13439
+ const name = agent.toLowerCase();
13440
+ const fn = SETUP_FN[name];
13441
+ if (!fn) {
13442
+ console.error(chalk20.red(`Unknown agent: "${agent}". Supported: ${AGENT_NAMES.join(", ")}`));
13443
+ process.exit(1);
13444
+ }
13445
+ await fn();
13446
+ });
13447
+ agents.command("remove").description("Remove Node9 from an agent").argument("<agent>", `Agent to unwire: ${AGENT_NAMES.join(" | ")}`).action((agent) => {
13448
+ const name = agent.toLowerCase();
13449
+ const fn = TEARDOWN_FN[name];
13450
+ if (!fn) {
13451
+ console.error(chalk20.red(`Unknown agent: "${agent}". Supported: ${AGENT_NAMES.join(", ")}`));
13452
+ process.exit(1);
13453
+ }
13454
+ console.log(chalk20.cyan(`
13455
+ \u{1F6E1}\uFE0F Node9: removing from ${name}...
13456
+ `));
13457
+ fn();
13458
+ console.log(chalk20.gray("\n Restart the agent for changes to take effect."));
13459
+ });
13460
+ }
13461
+
13462
+ // src/cli/commands/scan.ts
13463
+ init_shields();
13464
+ init_config();
13465
+ init_policy();
13466
+ init_dlp();
13467
+ import chalk21 from "chalk";
13468
+ import fs31 from "fs";
13469
+ import path34 from "path";
13470
+ import os27 from "os";
13471
+ var CLAUDE_PRICING2 = {
13472
+ "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
13473
+ "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
13474
+ "claude-opus-4": { i: 15e-6, o: 75e-6, cw: 1875e-8, cr: 15e-7 },
13475
+ "claude-sonnet-4-6": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13476
+ "claude-sonnet-4-5": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13477
+ "claude-sonnet-4": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13478
+ "claude-3-7-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13479
+ "claude-3-5-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13480
+ "claude-haiku-4-5": { i: 1e-6, o: 5e-6, cw: 125e-8, cr: 1e-7 },
13481
+ "claude-3-5-haiku": { i: 8e-7, o: 4e-6, cw: 1e-6, cr: 8e-8 }
13482
+ };
13483
+ function claudeModelPrice2(model) {
13484
+ const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
13485
+ for (const [key, p] of Object.entries(CLAUDE_PRICING2)) {
13486
+ if (base === key || base.startsWith(key)) return p;
13487
+ }
13488
+ return null;
13489
+ }
13490
+ function num2(n) {
13491
+ return n.toLocaleString();
13492
+ }
13493
+ function fmtCost2(usd) {
13494
+ if (usd < 1e-3) return "< $0.001";
13495
+ if (usd < 1) return "$" + usd.toFixed(4);
13496
+ return "$" + usd.toFixed(2);
13497
+ }
13498
+ function fmtTs(ts) {
13499
+ try {
13500
+ return new Date(ts).toLocaleDateString("en-US", {
13501
+ month: "short",
13502
+ day: "numeric",
13503
+ year: "numeric"
13504
+ });
13505
+ } catch {
13506
+ return ts.slice(0, 10);
13507
+ }
13508
+ }
13509
+ function preview(input, max) {
13510
+ const cmd = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
13511
+ const s = String(cmd).replace(/\s+/g, " ").trim();
13512
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
13513
+ }
13514
+ function buildRuleSources() {
13515
+ const sources = [];
13516
+ for (const [shieldName, shield] of Object.entries(SHIELDS)) {
13517
+ for (const rule of shield.smartRules) {
13518
+ sources.push({ shieldName, shieldLabel: shieldName, rule });
13519
+ }
13520
+ }
13521
+ try {
13522
+ const config = getConfig();
13523
+ for (const rule of config.policy.smartRules) {
13524
+ if (!rule.name) continue;
13525
+ if (rule.name.startsWith("shield:")) continue;
13526
+ const isCloud = rule.name.startsWith("cloud:");
13527
+ sources.push({
13528
+ shieldName: isCloud ? "cloud" : "custom",
13529
+ shieldLabel: isCloud ? "Cloud Policy" : "Your Rules",
13530
+ rule
13531
+ });
13532
+ }
13533
+ } catch {
13534
+ }
13535
+ return sources;
13536
+ }
13537
+ function scanClaudeHistory(startDate) {
13538
+ const projectsDir = path34.join(os27.homedir(), ".claude", "projects");
13539
+ const result = {
13540
+ filesScanned: 0,
13541
+ sessions: 0,
13542
+ totalToolCalls: 0,
13543
+ bashCalls: 0,
13544
+ findings: [],
13545
+ dlpFindings: [],
13546
+ totalCostUSD: 0,
13547
+ firstDate: null,
13548
+ lastDate: null
13549
+ };
13550
+ if (!fs31.existsSync(projectsDir)) return result;
13551
+ let projDirs;
13552
+ try {
13553
+ projDirs = fs31.readdirSync(projectsDir);
13554
+ } catch {
13555
+ return result;
13556
+ }
13557
+ const ruleSources = buildRuleSources();
13558
+ for (const proj of projDirs) {
13559
+ const projPath = path34.join(projectsDir, proj);
13560
+ try {
13561
+ if (!fs31.statSync(projPath).isDirectory()) continue;
13562
+ } catch {
13563
+ continue;
13564
+ }
13565
+ const projLabel = decodeURIComponent(proj).replace(os27.homedir(), "~").slice(0, 40);
13566
+ let files;
13567
+ try {
13568
+ files = fs31.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
13569
+ } catch {
13570
+ continue;
13571
+ }
13572
+ for (const file of files) {
13573
+ result.filesScanned++;
13574
+ result.sessions++;
13575
+ let raw;
13576
+ try {
13577
+ raw = fs31.readFileSync(path34.join(projPath, file), "utf-8");
13578
+ } catch {
13579
+ continue;
13580
+ }
13581
+ for (const line of raw.split("\n")) {
13582
+ if (!line.trim()) continue;
13583
+ let entry;
13584
+ try {
13585
+ entry = JSON.parse(line);
13586
+ } catch {
13587
+ continue;
13588
+ }
13589
+ if (entry.type !== "assistant") continue;
13590
+ if (startDate && entry.timestamp) {
13591
+ if (new Date(entry.timestamp) < startDate) continue;
13592
+ }
13593
+ if (entry.timestamp) {
13594
+ if (!result.firstDate || entry.timestamp < result.firstDate)
13595
+ result.firstDate = entry.timestamp;
13596
+ if (!result.lastDate || entry.timestamp > result.lastDate)
13597
+ result.lastDate = entry.timestamp;
13598
+ }
13599
+ const usage = entry.message?.usage;
13600
+ const model = entry.message?.model;
13601
+ if (usage && model) {
13602
+ const p = claudeModelPrice2(model);
13603
+ if (p) {
13604
+ result.totalCostUSD += (usage.input_tokens ?? 0) * p.i + (usage.output_tokens ?? 0) * p.o + (usage.cache_creation_input_tokens ?? 0) * p.cw + (usage.cache_read_input_tokens ?? 0) * p.cr;
13605
+ }
13606
+ }
13607
+ const content = entry.message?.content;
13608
+ if (!Array.isArray(content)) continue;
13609
+ for (const block of content) {
13610
+ if (block.type !== "tool_use") continue;
13611
+ result.totalToolCalls++;
13612
+ const toolName = block.name ?? "";
13613
+ const toolNameLower = toolName.toLowerCase();
13614
+ const input = block.input ?? {};
13615
+ if (toolNameLower === "bash" || toolNameLower === "execute_bash") {
13616
+ result.bashCalls++;
13617
+ }
13618
+ const dlpMatch = scanArgs(input);
13619
+ if (dlpMatch) {
13620
+ const isDupe = result.dlpFindings.some(
13621
+ (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
13622
+ );
13623
+ if (!isDupe) {
13624
+ result.dlpFindings.push({
13625
+ patternName: dlpMatch.patternName,
13626
+ redactedSample: dlpMatch.redactedSample,
13627
+ toolName,
13628
+ timestamp: entry.timestamp ?? "",
13629
+ project: projLabel
13630
+ });
13631
+ }
13632
+ }
13633
+ for (const source of ruleSources) {
13634
+ const { rule } = source;
13635
+ if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
13636
+ if (!evaluateSmartConditions(input, rule)) continue;
13637
+ const inputPreview = preview(input, 120);
13638
+ const isDupe = result.findings.some(
13639
+ (f) => f.source.rule.name === rule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
13640
+ );
13641
+ if (!isDupe) {
13642
+ result.findings.push({
13643
+ source,
13644
+ toolName,
13645
+ input,
13646
+ timestamp: entry.timestamp ?? "",
13647
+ project: projLabel
13648
+ });
13649
+ }
13650
+ break;
13651
+ }
13652
+ }
13653
+ }
13654
+ }
13655
+ }
13656
+ return result;
13657
+ }
13658
+ function registerScanCommand(program2) {
13659
+ program2.command("scan").description("Forecast: scan agent history and show what node9 would catch if installed").option("--all", "Scan all history (default: last 90 days)").option("--days <n>", "Scan last N days of history", "90").option("--top <n>", "Max findings to show per shield", "5").action((options) => {
13660
+ const topN = Math.max(1, parseInt(options.top, 10) || 5);
13661
+ const startDate = options.all ? null : (() => {
13662
+ const d = /* @__PURE__ */ new Date();
13663
+ d.setDate(d.getDate() - (parseInt(options.days, 10) || 90));
13664
+ d.setHours(0, 0, 0, 0);
13665
+ return d;
13666
+ })();
13667
+ console.log("");
13668
+ console.log(chalk21.cyan.bold("\u{1F50D} node9 scan") + chalk21.dim(" \u2014 what would node9 catch?"));
13669
+ console.log("");
13670
+ const projectsDir = path34.join(os27.homedir(), ".claude", "projects");
13671
+ if (!fs31.existsSync(projectsDir)) {
13672
+ console.log(chalk21.yellow(" No Claude history found at ~/.claude/projects/"));
13673
+ console.log(chalk21.gray(" Install Claude Code, run a few sessions, then try again.\n"));
13674
+ return;
13675
+ }
13676
+ process.stdout.write(chalk21.dim(" Scanning\u2026"));
13677
+ const scan = scanClaudeHistory(startDate);
13678
+ process.stdout.write("\r" + " ".repeat(20) + "\r");
13679
+ if (scan.filesScanned === 0) {
13680
+ console.log(chalk21.yellow(" No JSONL session files found.\n"));
13681
+ return;
13682
+ }
13683
+ const rangeLabel = options.all ? chalk21.dim("all time") : chalk21.dim(`last ${options.days ?? 90} days`);
13684
+ const dateRange = scan.firstDate && scan.lastDate ? chalk21.dim(` ${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}`) : "";
13685
+ console.log(
13686
+ " " + chalk21.white(num2(scan.sessions)) + chalk21.dim(" sessions ") + chalk21.white(num2(scan.totalToolCalls)) + chalk21.dim(" tool calls ") + chalk21.white(num2(scan.bashCalls)) + chalk21.dim(" bash commands ") + rangeLabel + dateRange
13687
+ );
13688
+ console.log("");
13689
+ const byShield = /* @__PURE__ */ new Map();
13690
+ for (const f of scan.findings) {
13691
+ const key = f.source.shieldName;
13692
+ const entry = byShield.get(key) ?? { label: f.source.shieldLabel, findings: [] };
13693
+ entry.findings.push(f);
13694
+ byShield.set(key, entry);
13695
+ }
13696
+ const totalFindings = scan.findings.length;
13697
+ if (totalFindings === 0 && scan.dlpFindings.length === 0) {
13698
+ console.log(chalk21.green(" \u2705 No findings across all shields and rules."));
13699
+ console.log(chalk21.dim(" node9 is still worth running \u2014 it monitors in real time.\n"));
13700
+ } else {
13701
+ if (totalFindings > 0) {
13702
+ console.log(
13703
+ " " + chalk21.bold("If node9 had been installed:") + " " + chalk21.yellow.bold(
13704
+ `${num2(totalFindings)} command${totalFindings !== 1 ? "s" : ""} flagged for review`
13705
+ )
13706
+ );
13707
+ console.log("");
13708
+ const sorted = [...byShield.entries()].sort(
13709
+ (a, b) => b[1].findings.length - a[1].findings.length
13710
+ );
13711
+ for (const [shieldName, { label, findings }] of sorted) {
13712
+ const count = findings.length;
13713
+ const isUserRule = shieldName === "custom" || shieldName === "cloud";
13714
+ const shieldBadge = isUserRule ? chalk21.magenta(label) : chalk21.cyan(label);
13715
+ console.log(" " + chalk21.dim("\u2500".repeat(70)));
13716
+ console.log(
13717
+ " " + shieldBadge + chalk21.dim(" \xB7 ") + chalk21.yellow(`${num2(count)} finding${count !== 1 ? "s" : ""}`) + (isUserRule ? "" : chalk21.dim(` \u2192 node9 shield enable ${shieldName}`))
13718
+ );
13719
+ const byRule = /* @__PURE__ */ new Map();
13720
+ for (const f of findings) {
13721
+ const ruleKey = f.source.rule.name ?? "unnamed";
13722
+ const arr = byRule.get(ruleKey) ?? [];
13723
+ arr.push(f);
13724
+ byRule.set(ruleKey, arr);
13725
+ }
13726
+ for (const [, ruleFindings] of byRule) {
13727
+ const rule = ruleFindings[0].source.rule;
13728
+ const ruleCount = ruleFindings.length;
13729
+ const countBadge = ruleCount > 1 ? chalk21.white(` \xD7${ruleCount}`) : "";
13730
+ const shortName = (rule.name ?? "unnamed").replace(/^shield:[^:]+:/, "");
13731
+ console.log(
13732
+ " " + chalk21.white(shortName) + countBadge + (rule.reason ? chalk21.dim(` \u2014 ${rule.reason}`) : "")
13733
+ );
13734
+ const shown = ruleFindings.slice(0, topN);
13735
+ for (const f of shown) {
13736
+ const ts = f.timestamp ? chalk21.dim(fmtTs(f.timestamp) + " ") : "";
13737
+ const proj = chalk21.dim(f.project.slice(0, 22).padEnd(22) + " ");
13738
+ const cmd = chalk21.gray(preview(f.input, 55));
13739
+ console.log(` ${ts}${proj}${cmd}`);
13740
+ }
13741
+ if (ruleFindings.length > topN) {
13742
+ console.log(
13743
+ chalk21.dim(
13744
+ ` \u2026 and ${ruleFindings.length - topN} more (--top ${ruleFindings.length})`
13745
+ )
13746
+ );
13747
+ }
13748
+ }
13749
+ console.log("");
13750
+ }
13751
+ }
13752
+ if (scan.dlpFindings.length > 0) {
13753
+ console.log(" " + chalk21.dim("\u2500".repeat(70)));
13754
+ console.log(
13755
+ " " + chalk21.red.bold("Secrets / DLP") + chalk21.dim(" \xB7 ") + chalk21.red(
13756
+ `${num2(scan.dlpFindings.length)} potential secret leak${scan.dlpFindings.length !== 1 ? "s" : ""}`
13757
+ )
13758
+ );
13759
+ const shownDlp = scan.dlpFindings.slice(0, topN);
13760
+ for (const f of shownDlp) {
13761
+ const ts = f.timestamp ? chalk21.dim(fmtTs(f.timestamp) + " ") : "";
13762
+ const proj = chalk21.dim(f.project.slice(0, 22).padEnd(22) + " ");
13763
+ console.log(
13764
+ ` ${ts}${proj}` + chalk21.yellow(f.patternName) + chalk21.dim(" ") + chalk21.gray(f.redactedSample)
13765
+ );
13766
+ }
13767
+ if (scan.dlpFindings.length > topN) {
13768
+ console.log(
13769
+ chalk21.dim(
13770
+ ` \u2026 and ${scan.dlpFindings.length - topN} more (--top ${scan.dlpFindings.length})`
13771
+ )
13772
+ );
13773
+ }
13774
+ console.log("");
13775
+ }
13776
+ }
13777
+ if (scan.totalCostUSD > 0) {
13778
+ console.log(
13779
+ " " + chalk21.bold("Claude spend:") + " " + chalk21.yellow(fmtCost2(scan.totalCostUSD)) + chalk21.dim(" (for per-period breakdown: node9 report)")
13780
+ );
13781
+ console.log("");
13782
+ }
13783
+ const auditLog = path34.join(os27.homedir(), ".node9", "audit.log");
13784
+ if (fs31.existsSync(auditLog)) {
13785
+ console.log(chalk21.green(" \u2705 node9 is active \u2014 future sessions are protected."));
13786
+ console.log(
13787
+ chalk21.dim(" Run ") + chalk21.cyan("node9 report") + chalk21.dim(" to see live stats.")
13788
+ );
13789
+ } else {
13790
+ console.log(chalk21.yellow.bold(" \u26A1 node9 was not running during these sessions."));
13791
+ console.log(
13792
+ " " + chalk21.white("Run ") + chalk21.cyan("node9 init") + chalk21.white(" to start protecting your AI agents.")
13793
+ );
13794
+ }
13795
+ console.log("");
13796
+ });
13797
+ }
13798
+
13799
+ // src/cli/commands/sessions.ts
13800
+ import chalk22 from "chalk";
13801
+ import fs32 from "fs";
13802
+ import path35 from "path";
13803
+ import os28 from "os";
13804
+ var CLAUDE_PRICING3 = {
13805
+ "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
13806
+ "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
13807
+ "claude-opus-4": { i: 15e-6, o: 75e-6, cw: 1875e-8, cr: 15e-7 },
13808
+ "claude-sonnet-4-6": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13809
+ "claude-sonnet-4-5": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13810
+ "claude-sonnet-4": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13811
+ "claude-3-7-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13812
+ "claude-3-5-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13813
+ "claude-haiku-4-5": { i: 1e-6, o: 5e-6, cw: 125e-8, cr: 1e-7 },
13814
+ "claude-3-5-haiku": { i: 8e-7, o: 4e-6, cw: 1e-6, cr: 8e-8 }
13815
+ };
13816
+ function modelPrice(model) {
13817
+ const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
13818
+ for (const [key, p] of Object.entries(CLAUDE_PRICING3)) {
13819
+ if (base === key || base.startsWith(key)) return p;
13820
+ }
13821
+ return null;
13822
+ }
13823
+ function encodeProjectPath(projectPath) {
13824
+ return projectPath.replace(/\//g, "-");
13825
+ }
13826
+ function sessionJsonlPath(projectPath, sessionId) {
13827
+ const encoded = encodeProjectPath(projectPath);
13828
+ return path35.join(os28.homedir(), ".claude", "projects", encoded, `${sessionId}.jsonl`);
13829
+ }
13830
+ function projectLabel(projectPath) {
13831
+ return projectPath.replace(os28.homedir(), "~");
13832
+ }
13833
+ function parseHistoryLines(lines) {
13834
+ const entries = [];
13835
+ for (const line of lines) {
13836
+ if (!line.trim()) continue;
13837
+ try {
13838
+ const obj = JSON.parse(line);
13839
+ if (typeof obj["display"] === "string" && (typeof obj["timestamp"] === "string" || typeof obj["timestamp"] === "number") && typeof obj["project"] === "string" && typeof obj["sessionId"] === "string") {
13840
+ const ts = typeof obj["timestamp"] === "number" ? new Date(obj["timestamp"]).toISOString() : obj["timestamp"];
13841
+ entries.push({
13842
+ display: obj["display"],
13843
+ timestamp: ts,
13844
+ project: obj["project"],
13845
+ sessionId: obj["sessionId"]
13846
+ });
13847
+ }
13848
+ } catch {
13849
+ }
13850
+ }
13851
+ return entries;
13852
+ }
13853
+ function parseSessionLines(lines) {
13854
+ const toolCalls = [];
13855
+ let costUSD = 0;
13856
+ let hasSnapshot = false;
13857
+ const modifiedFiles = [];
13858
+ const seenFiles = /* @__PURE__ */ new Set();
13859
+ for (const line of lines) {
13860
+ if (!line.trim()) continue;
13861
+ let entry;
13862
+ try {
13863
+ entry = JSON.parse(line);
13864
+ } catch {
13865
+ continue;
13866
+ }
13867
+ if (entry.type === "file-history-snapshot") {
13868
+ hasSnapshot = true;
13869
+ continue;
13870
+ }
13871
+ if (entry.type !== "assistant") continue;
13872
+ const usage = entry.message?.usage;
13873
+ const model = entry.message?.model;
13874
+ if (usage && model) {
13875
+ const p = modelPrice(model);
13876
+ if (p) {
13877
+ costUSD += (usage.input_tokens ?? 0) * p.i + (usage.output_tokens ?? 0) * p.o + (usage.cache_creation_input_tokens ?? 0) * p.cw + (usage.cache_read_input_tokens ?? 0) * p.cr;
13878
+ }
13879
+ }
13880
+ const content = entry.message?.content;
13881
+ if (!Array.isArray(content)) continue;
13882
+ for (const block of content) {
13883
+ if (block.type !== "tool_use") continue;
13884
+ const tool = block.name ?? "";
13885
+ const input = block.input ?? {};
13886
+ toolCalls.push({ tool, input, timestamp: entry.timestamp ?? "" });
13887
+ const toolLower = tool.toLowerCase();
13888
+ if (toolLower === "write" || toolLower === "edit" || toolLower === "notebookedit") {
13889
+ const fp = input.file_path ?? input.path;
13890
+ if (typeof fp === "string" && !seenFiles.has(fp)) {
13891
+ seenFiles.add(fp);
13892
+ modifiedFiles.push(fp);
13893
+ }
13894
+ }
13895
+ }
13896
+ }
13897
+ return { toolCalls, costUSD, hasSnapshot, modifiedFiles };
13898
+ }
13899
+ function loadAuditEntries(auditPath) {
13900
+ const aPath = auditPath ?? path35.join(os28.homedir(), ".node9", "audit.log");
13901
+ let raw;
13902
+ try {
13903
+ raw = fs32.readFileSync(aPath, "utf-8");
13904
+ } catch {
13905
+ return [];
13906
+ }
13907
+ const entries = [];
13908
+ for (const line of raw.split("\n")) {
13909
+ if (!line.trim()) continue;
13910
+ try {
13911
+ const e = JSON.parse(line);
13912
+ if (!e.ts || !e.tool || !e.decision) continue;
13913
+ if (e.decision === "allow" || e.decision === "allowed") continue;
13914
+ entries.push(e);
13915
+ } catch {
13916
+ }
13917
+ }
13918
+ return entries;
13919
+ }
13920
+ function auditEntriesInWindow(entries, windowStart, windowEnd) {
13921
+ const start = new Date(windowStart).getTime();
13922
+ const end = new Date(windowEnd).getTime();
13923
+ const result = [];
13924
+ for (const e of entries) {
13925
+ const t = new Date(e.ts).getTime();
13926
+ if (t < start || t > end) continue;
13927
+ result.push({
13928
+ tool: e.tool,
13929
+ args: e.args,
13930
+ argsHash: e.argsHash,
13931
+ timestamp: e.ts,
13932
+ decision: e.decision,
13933
+ checkedBy: e.checkedBy
13934
+ });
13935
+ }
13936
+ return result;
13937
+ }
13938
+ function buildSessions(days, historyPath) {
13939
+ const hPath = historyPath ?? path35.join(os28.homedir(), ".claude", "history.jsonl");
13940
+ let historyRaw;
13941
+ try {
13942
+ historyRaw = fs32.readFileSync(hPath, "utf-8");
13943
+ } catch {
13944
+ return [];
13945
+ }
13946
+ const cutoff = days !== null ? (() => {
13947
+ const d = /* @__PURE__ */ new Date();
13948
+ d.setDate(d.getDate() - days);
13949
+ d.setHours(0, 0, 0, 0);
13950
+ return d;
13951
+ })() : null;
13952
+ const entries = parseHistoryLines(historyRaw.split("\n"));
13953
+ const bySession = /* @__PURE__ */ new Map();
13954
+ for (const e of entries) {
13955
+ if (cutoff && new Date(e.timestamp) < cutoff) continue;
13956
+ const existing = bySession.get(e.sessionId);
13957
+ if (!existing || e.timestamp < existing.timestamp) {
13958
+ bySession.set(e.sessionId, e);
13959
+ }
13960
+ }
13961
+ const allAuditEntries = loadAuditEntries();
13962
+ const summaries = [];
13963
+ for (const entry of bySession.values()) {
13964
+ const jsonlFile = sessionJsonlPath(entry.project, entry.sessionId);
13965
+ let sessionLines = [];
13966
+ try {
13967
+ sessionLines = fs32.readFileSync(jsonlFile, "utf-8").split("\n");
13968
+ } catch {
13969
+ }
13970
+ const { toolCalls, costUSD, hasSnapshot, modifiedFiles } = parseSessionLines(sessionLines);
13971
+ const windowStart = entry.timestamp;
13972
+ const lastToolTs = toolCalls.length > 0 ? toolCalls[toolCalls.length - 1].timestamp : "";
13973
+ const windowEnd = new Date(
13974
+ Math.max(new Date(windowStart).getTime(), lastToolTs ? new Date(lastToolTs).getTime() : 0) + 5 * 60 * 1e3
13975
+ // 5 min buffer
13976
+ ).toISOString();
13977
+ const blockedCalls = auditEntriesInWindow(allAuditEntries, windowStart, windowEnd);
13978
+ summaries.push({
13979
+ sessionId: entry.sessionId,
13980
+ project: entry.project,
13981
+ projectLabel: projectLabel(entry.project),
13982
+ firstPrompt: entry.display,
13983
+ startTime: entry.timestamp,
13984
+ toolCalls,
13985
+ blockedCalls,
13986
+ costUSD,
13987
+ hasSnapshot,
13988
+ modifiedFiles
13989
+ });
13990
+ }
13991
+ summaries.sort((a, b) => a.startTime > b.startTime ? -1 : 1);
13992
+ return summaries;
13993
+ }
13994
+ function fmtCost3(usd) {
13995
+ if (usd === 0) return "";
13996
+ if (usd < 1e-3) return "< $0.001";
13997
+ if (usd < 1) return "$" + usd.toFixed(3);
13998
+ return "$" + usd.toFixed(2);
13999
+ }
14000
+ function fmtDate2(iso) {
14001
+ try {
14002
+ return new Date(iso).toLocaleDateString("en-US", { month: "short", day: "numeric" });
14003
+ } catch {
14004
+ return iso.slice(0, 10);
14005
+ }
14006
+ }
14007
+ function fmtTime(iso) {
14008
+ try {
14009
+ return new Date(iso).toLocaleTimeString("en-US", {
14010
+ hour: "2-digit",
14011
+ minute: "2-digit",
14012
+ hour12: false
14013
+ });
14014
+ } catch {
14015
+ return iso.slice(11, 16);
14016
+ }
14017
+ }
14018
+ function fmtDateTime(iso) {
14019
+ try {
14020
+ return new Date(iso).toLocaleString("en-US", {
14021
+ month: "short",
14022
+ day: "numeric",
14023
+ year: "numeric",
14024
+ hour: "2-digit",
14025
+ minute: "2-digit",
14026
+ hour12: false
14027
+ });
14028
+ } catch {
14029
+ return iso;
14030
+ }
14031
+ }
14032
+ function truncate(s, max) {
14033
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
14034
+ }
14035
+ function toolInputSummary(tool, input) {
14036
+ const fp = input.file_path ?? input.path;
14037
+ if (typeof fp === "string") return fp;
14038
+ const cmd = input.command;
14039
+ if (typeof cmd === "string") return truncate(cmd.replace(/\s+/g, " "), 60);
14040
+ const q = input.query ?? input.url;
14041
+ if (typeof q === "string") return truncate(String(q), 60);
14042
+ return "";
14043
+ }
14044
+ function toolColor(tool) {
14045
+ const t = tool.toLowerCase();
14046
+ if (t === "bash" || t === "execute_bash") return chalk22.red;
14047
+ if (t === "write") return chalk22.green;
14048
+ if (t === "edit" || t === "notebookedit") return chalk22.yellow;
14049
+ if (t === "read") return chalk22.cyan;
14050
+ return chalk22.gray;
14051
+ }
14052
+ function barStr2(value, max, width) {
14053
+ if (max === 0 || width <= 0) return "\u2591".repeat(width);
14054
+ const filled = Math.max(1, Math.round(value / max * width));
14055
+ return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
14056
+ }
14057
+ function colorBar2(value, max, width) {
14058
+ const s = barStr2(value, max, width);
14059
+ const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
14060
+ return chalk22.cyan(s.slice(0, filled)) + chalk22.dim(s.slice(filled));
14061
+ }
14062
+ function renderSummary(summaries) {
14063
+ const totalTools = summaries.reduce((n, s) => n + s.toolCalls.length, 0);
14064
+ const totalCost = summaries.reduce((n, s) => n + s.costUSD, 0);
14065
+ const totalFiles = summaries.reduce((n, s) => n + s.modifiedFiles.length, 0);
14066
+ const totalBlocked = summaries.reduce((n, s) => n + s.blockedCalls.length, 0);
14067
+ const snapshots = summaries.filter((s) => s.hasSnapshot).length;
14068
+ const avgCost = summaries.length > 0 ? totalCost / summaries.length : 0;
14069
+ const toolCounts = /* @__PURE__ */ new Map();
14070
+ for (const s of summaries) {
14071
+ for (const tc of s.toolCalls) {
14072
+ const key = tc.tool.toLowerCase();
14073
+ toolCounts.set(key, (toolCounts.get(key) ?? 0) + 1);
14074
+ }
14075
+ }
14076
+ const groups = { Bash: 0, Read: 0, Write: 0, Edit: 0, Other: 0 };
14077
+ for (const [tool, count] of toolCounts) {
14078
+ if (tool === "bash" || tool === "execute_bash") groups["Bash"] += count;
14079
+ else if (tool === "read") groups["Read"] += count;
14080
+ else if (tool === "write") groups["Write"] += count;
14081
+ else if (tool === "edit" || tool === "notebookedit") groups["Edit"] += count;
14082
+ else groups["Other"] += count;
14083
+ }
14084
+ const projCosts = /* @__PURE__ */ new Map();
14085
+ for (const s of summaries) {
14086
+ projCosts.set(s.projectLabel, (projCosts.get(s.projectLabel) ?? 0) + s.costUSD);
14087
+ }
14088
+ const topProjects = [...projCosts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
14089
+ const W = 20;
14090
+ console.log(chalk22.dim(" " + "\u2500".repeat(70)));
14091
+ console.log(
14092
+ " " + chalk22.bold.white(String(summaries.length).padEnd(4)) + chalk22.dim("sessions ") + chalk22.bold.yellow(fmtCost3(totalCost).padEnd(10)) + chalk22.dim("total ") + chalk22.bold.white(String(totalTools).padEnd(6)) + chalk22.dim("tool calls ") + chalk22.bold.white(String(totalFiles)) + chalk22.dim(" files modified") + (totalBlocked > 0 ? chalk22.dim(" ") + chalk22.red.bold(String(totalBlocked)) + chalk22.dim(" blocked by node9") : "")
14093
+ );
14094
+ console.log(
14095
+ " " + chalk22.dim("avg ") + chalk22.white(fmtCost3(avgCost).padEnd(10)) + chalk22.dim("/session ") + chalk22.green(String(snapshots)) + chalk22.dim(` of ${summaries.length} sessions had snapshots`)
14096
+ );
14097
+ console.log("");
14098
+ console.log(" " + chalk22.dim("Tool breakdown:"));
14099
+ const maxGroup = Math.max(...Object.values(groups));
14100
+ for (const [label, count] of Object.entries(groups)) {
14101
+ if (count === 0) continue;
14102
+ const pct2 = totalTools > 0 ? Math.round(count / totalTools * 100) : 0;
14103
+ console.log(
14104
+ " " + label.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " + chalk22.white(String(count).padStart(4)) + chalk22.dim(` (${String(pct2)}%)`)
14105
+ );
14106
+ }
14107
+ console.log("");
14108
+ if (topProjects.length > 1) {
14109
+ console.log(" " + chalk22.dim("Cost by project:"));
14110
+ const maxProjCost = topProjects[0][1];
14111
+ for (const [proj, cost] of topProjects) {
14112
+ console.log(
14113
+ " " + proj.slice(0, 28).padEnd(28) + " " + colorBar2(cost, maxProjCost, W) + " " + chalk22.yellow(fmtCost3(cost))
14114
+ );
14115
+ }
14116
+ console.log("");
14117
+ }
14118
+ console.log(chalk22.dim(" " + "\u2500".repeat(70)));
14119
+ console.log("");
14120
+ }
14121
+ function renderList(summaries, totalCost) {
14122
+ if (summaries.length === 0) {
14123
+ console.log(chalk22.yellow(" No sessions found in the requested range.\n"));
14124
+ return;
14125
+ }
14126
+ const totalLabel = totalCost > 0 ? chalk22.dim(" ~" + fmtCost3(totalCost) + " total") : "";
14127
+ console.log(
14128
+ " " + chalk22.white(String(summaries.length)) + chalk22.dim(` session${summaries.length !== 1 ? "s" : ""}`) + totalLabel
14129
+ );
14130
+ console.log("");
14131
+ let lastGroup = "";
14132
+ for (const s of summaries) {
14133
+ const group = fmtDate2(s.startTime) + " " + s.projectLabel;
14134
+ if (group !== lastGroup) {
14135
+ console.log(
14136
+ chalk22.dim(" \u2500\u2500\u2500 ") + chalk22.bold(fmtDate2(s.startTime)) + chalk22.dim(" " + s.projectLabel)
14137
+ );
14138
+ lastGroup = group;
14139
+ }
14140
+ const timeStr = chalk22.dim(fmtTime(s.startTime));
14141
+ const prompt = chalk22.white(truncate(s.firstPrompt.replace(/\n/g, " "), 50).padEnd(50));
14142
+ const tools = s.toolCalls.length > 0 ? chalk22.dim(String(s.toolCalls.length).padStart(3) + " tools") : chalk22.dim(" 0 tools");
14143
+ const cost = s.costUSD > 0 ? chalk22.dim(" " + fmtCost3(s.costUSD).padEnd(8)) : " ";
14144
+ const blocked = s.blockedCalls.length > 0 ? chalk22.red(" \u{1F6D1} " + String(s.blockedCalls.length)) : "";
14145
+ const snap = s.hasSnapshot ? chalk22.green(" \u{1F4F8}") : "";
14146
+ const sid = chalk22.dim(" " + s.sessionId.slice(0, 8));
14147
+ console.log(` ${timeStr} ${prompt} ${tools}${cost}${blocked}${snap}${sid}`);
14148
+ }
14149
+ console.log("");
14150
+ console.log(
14151
+ chalk22.dim(" Run") + " " + chalk22.cyan("node9 sessions --detail <session-id>") + chalk22.dim(" for full tool trace.")
14152
+ );
14153
+ console.log("");
14154
+ }
14155
+ function renderDetail(s) {
14156
+ console.log("");
14157
+ console.log(chalk22.bold(" Session ") + chalk22.dim(s.sessionId));
14158
+ console.log(
14159
+ chalk22.bold(" Prompt ") + chalk22.white(s.firstPrompt.replace(/\n/g, " ").slice(0, 120))
14160
+ );
14161
+ console.log(chalk22.bold(" Project ") + chalk22.white(s.projectLabel));
14162
+ console.log(chalk22.bold(" When ") + chalk22.white(fmtDateTime(s.startTime)));
14163
+ if (s.costUSD > 0)
14164
+ console.log(chalk22.bold(" Cost ") + chalk22.yellow("~" + fmtCost3(s.costUSD)));
14165
+ console.log(
14166
+ chalk22.bold(" Snapshot ") + (s.hasSnapshot ? chalk22.green("\u2713 taken") : chalk22.dim("none"))
14167
+ );
14168
+ console.log("");
14169
+ if (s.toolCalls.length === 0 && s.blockedCalls.length === 0) {
14170
+ console.log(chalk22.dim(" No tool calls recorded.\n"));
14171
+ return;
14172
+ }
14173
+ const timeline = [
14174
+ ...s.toolCalls.map((tc) => ({ kind: "tool", tc })),
14175
+ ...s.blockedCalls.map((bc) => ({ kind: "blocked", bc }))
14176
+ ].sort((a, b) => {
14177
+ const ta = a.kind === "tool" ? a.tc.timestamp : a.bc.timestamp;
14178
+ const tb = b.kind === "tool" ? b.tc.timestamp : b.bc.timestamp;
14179
+ return ta < tb ? -1 : ta > tb ? 1 : 0;
14180
+ });
14181
+ const headerParts = [`Tool calls (${s.toolCalls.length})`];
14182
+ if (s.blockedCalls.length > 0)
14183
+ headerParts.push(chalk22.red(`${s.blockedCalls.length} blocked by node9`));
14184
+ console.log(chalk22.bold(" " + headerParts.join(" \xB7 ")));
14185
+ console.log("");
14186
+ for (const entry of timeline) {
14187
+ if (entry.kind === "tool") {
14188
+ const tc = entry.tc;
14189
+ const colorFn = toolColor(tc.tool);
14190
+ const toolPad = colorFn(tc.tool.padEnd(16));
14191
+ const detail = chalk22.gray(truncate(toolInputSummary(tc.tool, tc.input), 70));
14192
+ const ts = tc.timestamp ? chalk22.dim(fmtTime(tc.timestamp) + " ") : " ";
14193
+ console.log(` ${ts}${toolPad} ${detail}`);
14194
+ } else {
14195
+ const bc = entry.bc;
14196
+ const ts = bc.timestamp ? chalk22.dim(fmtTime(bc.timestamp) + " ") : " ";
14197
+ const label = chalk22.red("\u{1F6D1} BLOCKED".padEnd(16));
14198
+ const toolName = chalk22.red(bc.tool.padEnd(10));
14199
+ const argsSummary = bc.args ? chalk22.gray(truncate(toolInputSummary(bc.tool, bc.args), 40)) : chalk22.dim("[args not logged]");
14200
+ const reason = bc.checkedBy ? chalk22.dim(" \u2190 " + bc.checkedBy) : "";
14201
+ console.log(` ${ts}${label} ${toolName} ${argsSummary}${reason}`);
14202
+ }
14203
+ }
14204
+ console.log("");
14205
+ if (s.modifiedFiles.length > 0) {
14206
+ console.log(chalk22.bold(` Files modified (${s.modifiedFiles.length}):`));
14207
+ for (const f of s.modifiedFiles) {
14208
+ console.log(" " + chalk22.yellow(f));
14209
+ }
14210
+ console.log("");
14211
+ }
14212
+ }
14213
+ function registerSessionsCommand(program2) {
14214
+ program2.command("sessions").description("Show what your AI agent did \u2014 sessions, tool calls, cost, and file changes").option("--all", "Show all sessions (default: last 7 days)").option("--days <n>", "Show last N days of sessions", "7").option("--detail <sessionId>", "Show full tool trace for a session").action((options) => {
14215
+ console.log("");
14216
+ console.log(chalk22.cyan.bold("\u{1F4CB} node9 sessions") + chalk22.dim(" \u2014 what your AI agent did"));
14217
+ console.log("");
14218
+ const historyPath = path35.join(os28.homedir(), ".claude", "history.jsonl");
14219
+ if (!fs32.existsSync(historyPath)) {
14220
+ console.log(chalk22.yellow(" No Claude session history found at ~/.claude/history.jsonl"));
14221
+ console.log(chalk22.gray(" Install Claude Code, run a few sessions, then try again.\n"));
14222
+ return;
14223
+ }
14224
+ const days = options.detail || options.all ? null : Math.max(1, parseInt(options.days, 10) || 7);
14225
+ const rangeLabel = options.detail ? "all time" : options.all ? "all time" : `last ${String(days)} days`;
14226
+ console.log(chalk22.dim(" " + rangeLabel));
14227
+ console.log("");
14228
+ process.stdout.write(chalk22.dim(" Loading\u2026"));
14229
+ const summaries = buildSessions(days);
14230
+ process.stdout.write("\r" + " ".repeat(20) + "\r");
14231
+ if (options.detail) {
14232
+ const target = summaries.find(
14233
+ (s) => s.sessionId === options.detail || s.sessionId.startsWith(options.detail)
14234
+ );
14235
+ if (!target) {
14236
+ console.log(chalk22.red(` Session not found: ${options.detail}`));
14237
+ console.log(chalk22.dim(" Run `node9 sessions` to list recent sessions.\n"));
14238
+ return;
14239
+ }
14240
+ renderDetail(target);
14241
+ return;
14242
+ }
14243
+ const totalCost = summaries.reduce((sum, s) => sum + s.costUSD, 0);
14244
+ if (summaries.length > 0) renderSummary(summaries);
14245
+ renderList(summaries, totalCost);
14246
+ });
14247
+ }
14248
+
12455
14249
  // src/cli.ts
12456
14250
  var { version } = JSON.parse(
12457
- fs31.readFileSync(path34.join(__dirname, "../package.json"), "utf-8")
14251
+ fs35.readFileSync(path38.join(__dirname, "../package.json"), "utf-8")
12458
14252
  );
12459
14253
  var program = new Command();
12460
14254
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
12461
14255
  program.command("login").argument("<apiKey>").option("--local", "Save key for audit/logging only \u2014 local config still controls all decisions").option("--profile <name>", 'Save as a named profile (default: "default")').action((apiKey, options) => {
12462
- const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
12463
- const credPath = path34.join(os27.homedir(), ".node9", "credentials.json");
12464
- if (!fs31.existsSync(path34.dirname(credPath)))
12465
- fs31.mkdirSync(path34.dirname(credPath), { recursive: true });
14256
+ const DEFAULT_API_URL2 = "https://api.node9.ai/api/v1/intercept";
14257
+ const credPath = path38.join(os31.homedir(), ".node9", "credentials.json");
14258
+ if (!fs35.existsSync(path38.dirname(credPath)))
14259
+ fs35.mkdirSync(path38.dirname(credPath), { recursive: true });
12466
14260
  const profileName = options.profile || "default";
12467
14261
  let existingCreds = {};
12468
14262
  try {
12469
- if (fs31.existsSync(credPath)) {
12470
- const raw = JSON.parse(fs31.readFileSync(credPath, "utf-8"));
14263
+ if (fs35.existsSync(credPath)) {
14264
+ const raw = JSON.parse(fs35.readFileSync(credPath, "utf-8"));
12471
14265
  if (raw.apiKey) {
12472
14266
  existingCreds = {
12473
- default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
14267
+ default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL2 }
12474
14268
  };
12475
14269
  } else {
12476
14270
  existingCreds = raw;
@@ -12478,14 +14272,14 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
12478
14272
  }
12479
14273
  } catch {
12480
14274
  }
12481
- existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
12482
- fs31.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
14275
+ existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL2 };
14276
+ fs35.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
12483
14277
  if (profileName === "default") {
12484
- const configPath = path34.join(os27.homedir(), ".node9", "config.json");
14278
+ const configPath = path38.join(os31.homedir(), ".node9", "config.json");
12485
14279
  let config = {};
12486
14280
  try {
12487
- if (fs31.existsSync(configPath))
12488
- config = JSON.parse(fs31.readFileSync(configPath, "utf-8"));
14281
+ if (fs35.existsSync(configPath))
14282
+ config = JSON.parse(fs35.readFileSync(configPath, "utf-8"));
12489
14283
  } catch {
12490
14284
  }
12491
14285
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -12500,39 +14294,47 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
12500
14294
  approvers.cloud = false;
12501
14295
  }
12502
14296
  s.approvers = approvers;
12503
- if (!fs31.existsSync(path34.dirname(configPath)))
12504
- fs31.mkdirSync(path34.dirname(configPath), { recursive: true });
12505
- fs31.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
14297
+ if (!fs35.existsSync(path38.dirname(configPath)))
14298
+ fs35.mkdirSync(path38.dirname(configPath), { recursive: true });
14299
+ fs35.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
12506
14300
  }
12507
14301
  if (options.profile && profileName !== "default") {
12508
- console.log(chalk20.green(`\u2705 Profile "${profileName}" saved`));
12509
- console.log(chalk20.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
14302
+ console.log(chalk24.green(`\u2705 Profile "${profileName}" saved`));
14303
+ console.log(chalk24.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
12510
14304
  } else if (options.local) {
12511
- console.log(chalk20.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
12512
- console.log(chalk20.gray(` All decisions stay on this machine.`));
14305
+ console.log(chalk24.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
14306
+ console.log(chalk24.gray(` All decisions stay on this machine.`));
12513
14307
  } else {
12514
- console.log(chalk20.green(`\u2705 Logged in \u2014 agent mode`));
12515
- console.log(chalk20.gray(` Team policy enforced for all calls via Node9 cloud.`));
14308
+ console.log(chalk24.green(`\u2705 Logged in \u2014 agent mode`));
14309
+ console.log(chalk24.gray(` Team policy enforced for all calls via Node9 cloud.`));
12516
14310
  }
12517
14311
  });
12518
- program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor hud").argument("<target>", "The agent to protect: claude | gemini | cursor | hud").action(async (target) => {
14312
+ program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor windsurf vscode hud").argument("<target>", "The agent to protect: claude | gemini | cursor | windsurf | vscode | hud").action(async (target) => {
12519
14313
  if (target === "gemini") return await setupGemini();
12520
14314
  if (target === "claude") return await setupClaude();
12521
14315
  if (target === "cursor") return await setupCursor();
14316
+ if (target === "windsurf") return await setupWindsurf();
14317
+ if (target === "vscode") return await setupVSCode();
12522
14318
  if (target === "hud") return setupHud();
12523
- console.error(chalk20.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
14319
+ console.error(
14320
+ chalk24.red(
14321
+ `Unknown target: "${target}". Supported: claude, gemini, cursor, windsurf, vscode, hud`
14322
+ )
14323
+ );
12524
14324
  process.exit(1);
12525
14325
  });
12526
- program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor hud").argument("[target]", "The agent to protect: claude | gemini | cursor | hud").action(async (target) => {
14326
+ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor windsurf vscode hud").argument("[target]", "The agent to protect: claude | gemini | cursor | windsurf | vscode | hud").action(async (target) => {
12527
14327
  if (!target) {
12528
- console.log(chalk20.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
12529
- console.log(" Usage: " + chalk20.white("node9 setup <target>") + "\n");
14328
+ console.log(chalk24.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
14329
+ console.log(" Usage: " + chalk24.white("node9 setup <target>") + "\n");
12530
14330
  console.log(" Targets:");
12531
- console.log(" " + chalk20.green("claude") + " \u2014 Claude Code (hook mode)");
12532
- console.log(" " + chalk20.green("gemini") + " \u2014 Gemini CLI (hook mode)");
12533
- console.log(" " + chalk20.green("cursor") + " \u2014 Cursor (hook mode)");
14331
+ console.log(" " + chalk24.green("claude") + " \u2014 Claude Code (hook mode)");
14332
+ console.log(" " + chalk24.green("gemini") + " \u2014 Gemini CLI (hook mode)");
14333
+ console.log(" " + chalk24.green("cursor") + " \u2014 Cursor (MCP proxy)");
14334
+ console.log(" " + chalk24.green("windsurf") + " \u2014 Windsurf (MCP proxy)");
14335
+ console.log(" " + chalk24.green("vscode") + " \u2014 VSCode / Copilot (MCP proxy)");
12534
14336
  process.stdout.write(
12535
- " " + chalk20.green("hud") + " \u2014 Claude Code security statusline\n"
14337
+ " " + chalk24.green("hud") + " \u2014 Claude Code security statusline\n"
12536
14338
  );
12537
14339
  console.log("");
12538
14340
  return;
@@ -12541,93 +14343,108 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
12541
14343
  if (t === "gemini") return await setupGemini();
12542
14344
  if (t === "claude") return await setupClaude();
12543
14345
  if (t === "cursor") return await setupCursor();
14346
+ if (t === "windsurf") return await setupWindsurf();
14347
+ if (t === "vscode") return await setupVSCode();
12544
14348
  if (t === "hud") return setupHud();
12545
- console.error(chalk20.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
14349
+ console.error(
14350
+ chalk24.red(
14351
+ `Unknown target: "${target}". Supported: claude, gemini, cursor, windsurf, vscode, hud`
14352
+ )
14353
+ );
12546
14354
  process.exit(1);
12547
14355
  });
12548
- program.command("removefrom").description("Remove Node9 hooks from an AI agent configuration").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to remove from: claude | gemini | cursor").action((target) => {
14356
+ program.command("removefrom").description("Remove Node9 hooks from an AI agent configuration").addHelpText("after", "\n Supported targets: claude gemini cursor windsurf vscode hud").argument(
14357
+ "<target>",
14358
+ "The agent to remove from: claude | gemini | cursor | windsurf | vscode | hud"
14359
+ ).action((target) => {
12549
14360
  let fn;
12550
14361
  if (target === "claude") fn = teardownClaude;
12551
14362
  else if (target === "gemini") fn = teardownGemini;
12552
14363
  else if (target === "cursor") fn = teardownCursor;
14364
+ else if (target === "windsurf") fn = teardownWindsurf;
14365
+ else if (target === "vscode") fn = teardownVSCode;
12553
14366
  else if (target === "hud") fn = teardownHud;
12554
14367
  else {
12555
14368
  console.error(
12556
- chalk20.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`)
14369
+ chalk24.red(
14370
+ `Unknown target: "${target}". Supported: claude, gemini, cursor, windsurf, vscode, hud`
14371
+ )
12557
14372
  );
12558
14373
  process.exit(1);
12559
14374
  }
12560
- console.log(chalk20.cyan(`
14375
+ console.log(chalk24.cyan(`
12561
14376
  \u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
12562
14377
  `));
12563
14378
  try {
12564
14379
  fn();
12565
14380
  } catch (err2) {
12566
- console.error(chalk20.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
14381
+ console.error(chalk24.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
12567
14382
  process.exit(1);
12568
14383
  }
12569
- console.log(chalk20.gray("\n Restart the agent for changes to take effect."));
14384
+ console.log(chalk24.gray("\n Restart the agent for changes to take effect."));
12570
14385
  });
12571
14386
  program.command("uninstall").description("Remove all Node9 hooks and optionally delete config files").option("--purge", "Also delete ~/.node9/ directory (config, audit log, credentials)").action(async (options) => {
12572
- console.log(chalk20.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
12573
- console.log(chalk20.bold("Stopping daemon..."));
14387
+ console.log(chalk24.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
14388
+ console.log(chalk24.bold("Stopping daemon..."));
12574
14389
  try {
12575
14390
  stopDaemon();
12576
- console.log(chalk20.green(" \u2705 Daemon stopped"));
14391
+ console.log(chalk24.green(" \u2705 Daemon stopped"));
12577
14392
  } catch {
12578
- console.log(chalk20.blue(" \u2139\uFE0F Daemon was not running"));
14393
+ console.log(chalk24.blue(" \u2139\uFE0F Daemon was not running"));
12579
14394
  }
12580
- console.log(chalk20.bold("\nRemoving hooks..."));
14395
+ console.log(chalk24.bold("\nRemoving hooks..."));
12581
14396
  let teardownFailed = false;
12582
14397
  for (const [label, fn] of [
12583
14398
  ["Claude", teardownClaude],
12584
14399
  ["Gemini", teardownGemini],
12585
- ["Cursor", teardownCursor]
14400
+ ["Cursor", teardownCursor],
14401
+ ["Windsurf", teardownWindsurf],
14402
+ ["VSCode", teardownVSCode]
12586
14403
  ]) {
12587
14404
  try {
12588
14405
  fn();
12589
14406
  } catch (err2) {
12590
14407
  teardownFailed = true;
12591
14408
  console.error(
12592
- chalk20.red(
14409
+ chalk24.red(
12593
14410
  ` \u26A0\uFE0F Failed to remove ${label} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
12594
14411
  )
12595
14412
  );
12596
14413
  }
12597
14414
  }
12598
14415
  if (options.purge) {
12599
- const node9Dir = path34.join(os27.homedir(), ".node9");
12600
- if (fs31.existsSync(node9Dir)) {
14416
+ const node9Dir = path38.join(os31.homedir(), ".node9");
14417
+ if (fs35.existsSync(node9Dir)) {
12601
14418
  const confirmed = await confirm2({
12602
14419
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
12603
14420
  default: false
12604
14421
  });
12605
14422
  if (confirmed) {
12606
- fs31.rmSync(node9Dir, { recursive: true });
12607
- if (fs31.existsSync(node9Dir)) {
14423
+ fs35.rmSync(node9Dir, { recursive: true });
14424
+ if (fs35.existsSync(node9Dir)) {
12608
14425
  console.error(
12609
- chalk20.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
14426
+ chalk24.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
12610
14427
  );
12611
14428
  } else {
12612
- console.log(chalk20.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
14429
+ console.log(chalk24.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
12613
14430
  }
12614
14431
  } else {
12615
- console.log(chalk20.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
14432
+ console.log(chalk24.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
12616
14433
  }
12617
14434
  } else {
12618
- console.log(chalk20.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
14435
+ console.log(chalk24.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
12619
14436
  }
12620
14437
  } else {
12621
14438
  console.log(
12622
- chalk20.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
14439
+ chalk24.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
12623
14440
  );
12624
14441
  }
12625
14442
  if (teardownFailed) {
12626
- console.error(chalk20.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
14443
+ console.error(chalk24.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
12627
14444
  process.exit(1);
12628
14445
  }
12629
- console.log(chalk20.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
12630
- console.log(chalk20.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
14446
+ console.log(chalk24.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
14447
+ console.log(chalk24.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
12631
14448
  });
12632
14449
  registerDoctorCommand(program, version);
12633
14450
  program.command("explain").description(
@@ -12640,7 +14457,7 @@ program.command("explain").description(
12640
14457
  try {
12641
14458
  args = JSON.parse(trimmed);
12642
14459
  } catch {
12643
- console.error(chalk20.red(`
14460
+ console.error(chalk24.red(`
12644
14461
  \u274C Invalid JSON: ${trimmed}
12645
14462
  `));
12646
14463
  process.exit(1);
@@ -12651,54 +14468,54 @@ program.command("explain").description(
12651
14468
  }
12652
14469
  const result = await explainPolicy(tool, args);
12653
14470
  console.log("");
12654
- console.log(chalk20.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
14471
+ console.log(chalk24.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
12655
14472
  console.log("");
12656
- console.log(` ${chalk20.bold("Tool:")} ${chalk20.white(result.tool)}`);
14473
+ console.log(` ${chalk24.bold("Tool:")} ${chalk24.white(result.tool)}`);
12657
14474
  if (argsRaw) {
12658
- const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
12659
- console.log(` ${chalk20.bold("Input:")} ${chalk20.gray(preview)}`);
14475
+ const preview2 = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
14476
+ console.log(` ${chalk24.bold("Input:")} ${chalk24.gray(preview2)}`);
12660
14477
  }
12661
14478
  console.log("");
12662
- console.log(chalk20.bold("Config Sources (Waterfall):"));
14479
+ console.log(chalk24.bold("Config Sources (Waterfall):"));
12663
14480
  for (const tier of result.waterfall) {
12664
- const num2 = chalk20.gray(` ${tier.tier}.`);
14481
+ const num3 = chalk24.gray(` ${tier.tier}.`);
12665
14482
  const label = tier.label.padEnd(16);
12666
14483
  let statusStr;
12667
14484
  if (tier.tier === 1) {
12668
- statusStr = chalk20.gray(tier.note ?? "");
14485
+ statusStr = chalk24.gray(tier.note ?? "");
12669
14486
  } else if (tier.status === "active") {
12670
- const loc = tier.path ? chalk20.gray(tier.path) : "";
12671
- const note = tier.note ? chalk20.gray(`(${tier.note})`) : "";
12672
- statusStr = chalk20.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
14487
+ const loc = tier.path ? chalk24.gray(tier.path) : "";
14488
+ const note = tier.note ? chalk24.gray(`(${tier.note})`) : "";
14489
+ statusStr = chalk24.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
12673
14490
  } else {
12674
- statusStr = chalk20.gray("\u25CB " + (tier.note ?? "not found"));
14491
+ statusStr = chalk24.gray("\u25CB " + (tier.note ?? "not found"));
12675
14492
  }
12676
- console.log(`${num2} ${chalk20.white(label)} ${statusStr}`);
14493
+ console.log(`${num3} ${chalk24.white(label)} ${statusStr}`);
12677
14494
  }
12678
14495
  console.log("");
12679
- console.log(chalk20.bold("Policy Evaluation:"));
14496
+ console.log(chalk24.bold("Policy Evaluation:"));
12680
14497
  for (const step of result.steps) {
12681
14498
  const isFinal = step.isFinal;
12682
14499
  let icon;
12683
- if (step.outcome === "allow") icon = chalk20.green(" \u2705");
12684
- else if (step.outcome === "review") icon = chalk20.red(" \u{1F534}");
12685
- else if (step.outcome === "skip") icon = chalk20.gray(" \u2500 ");
12686
- else icon = chalk20.gray(" \u25CB ");
14500
+ if (step.outcome === "allow") icon = chalk24.green(" \u2705");
14501
+ else if (step.outcome === "review") icon = chalk24.red(" \u{1F534}");
14502
+ else if (step.outcome === "skip") icon = chalk24.gray(" \u2500 ");
14503
+ else icon = chalk24.gray(" \u25CB ");
12687
14504
  const name = step.name.padEnd(18);
12688
- const nameStr = isFinal ? chalk20.white.bold(name) : chalk20.white(name);
12689
- const detail = isFinal ? chalk20.white(step.detail) : chalk20.gray(step.detail);
12690
- const arrow = isFinal ? chalk20.yellow(" \u2190 STOP") : "";
14505
+ const nameStr = isFinal ? chalk24.white.bold(name) : chalk24.white(name);
14506
+ const detail = isFinal ? chalk24.white(step.detail) : chalk24.gray(step.detail);
14507
+ const arrow = isFinal ? chalk24.yellow(" \u2190 STOP") : "";
12691
14508
  console.log(`${icon} ${nameStr} ${detail}${arrow}`);
12692
14509
  }
12693
14510
  console.log("");
12694
14511
  if (result.decision === "allow") {
12695
- console.log(chalk20.green.bold(" Decision: \u2705 ALLOW") + chalk20.gray(" \u2014 no approval needed"));
14512
+ console.log(chalk24.green.bold(" Decision: \u2705 ALLOW") + chalk24.gray(" \u2014 no approval needed"));
12696
14513
  } else {
12697
14514
  console.log(
12698
- chalk20.red.bold(" Decision: \u{1F534} REVIEW") + chalk20.gray(" \u2014 human approval required")
14515
+ chalk24.red.bold(" Decision: \u{1F534} REVIEW") + chalk24.gray(" \u2014 human approval required")
12699
14516
  );
12700
14517
  if (result.blockedByLabel) {
12701
- console.log(chalk20.gray(` Reason: ${result.blockedByLabel}`));
14518
+ console.log(chalk24.gray(` Reason: ${result.blockedByLabel}`));
12702
14519
  }
12703
14520
  }
12704
14521
  console.log("");
@@ -12713,7 +14530,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
12713
14530
  try {
12714
14531
  await startTail2(options);
12715
14532
  } catch (err2) {
12716
- console.error(chalk20.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
14533
+ console.error(chalk24.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
12717
14534
  process.exit(1);
12718
14535
  }
12719
14536
  });
@@ -12745,14 +14562,14 @@ Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
12745
14562
  Run "node9 addto claude" to register it as the statusLine.`
12746
14563
  ).argument("[subcommand]", 'Optional: "debug on" / "debug off" to toggle stdin logging').argument("[state]", 'on|off \u2014 used with "debug" subcommand').action(async (subcommand, state) => {
12747
14564
  if (subcommand === "debug") {
12748
- const flagFile = path34.join(os27.homedir(), ".node9", "hud-debug");
14565
+ const flagFile = path38.join(os31.homedir(), ".node9", "hud-debug");
12749
14566
  if (state === "on") {
12750
- fs31.mkdirSync(path34.dirname(flagFile), { recursive: true });
12751
- fs31.writeFileSync(flagFile, "");
14567
+ fs35.mkdirSync(path38.dirname(flagFile), { recursive: true });
14568
+ fs35.writeFileSync(flagFile, "");
12752
14569
  console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
12753
14570
  console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
12754
14571
  } else if (state === "off") {
12755
- if (fs31.existsSync(flagFile)) fs31.unlinkSync(flagFile);
14572
+ if (fs35.existsSync(flagFile)) fs35.unlinkSync(flagFile);
12756
14573
  console.log("HUD debug logging disabled.");
12757
14574
  } else {
12758
14575
  console.error("Usage: node9 hud debug on|off");
@@ -12767,7 +14584,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
12767
14584
  const ms = parseDuration(options.duration);
12768
14585
  if (ms === null) {
12769
14586
  console.error(
12770
- chalk20.red(`
14587
+ chalk24.red(`
12771
14588
  \u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
12772
14589
  `)
12773
14590
  );
@@ -12775,20 +14592,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
12775
14592
  }
12776
14593
  pauseNode9(ms, options.duration);
12777
14594
  const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
12778
- console.log(chalk20.yellow(`
14595
+ console.log(chalk24.yellow(`
12779
14596
  \u23F8 Node9 paused until ${expiresAt}`));
12780
- console.log(chalk20.gray(` All tool calls will be allowed without review.`));
12781
- console.log(chalk20.gray(` Run "node9 resume" to re-enable early.
14597
+ console.log(chalk24.gray(` All tool calls will be allowed without review.`));
14598
+ console.log(chalk24.gray(` Run "node9 resume" to re-enable early.
12782
14599
  `));
12783
14600
  });
12784
14601
  program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
12785
14602
  const { paused } = checkPause();
12786
14603
  if (!paused) {
12787
- console.log(chalk20.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
14604
+ console.log(chalk24.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
12788
14605
  return;
12789
14606
  }
12790
14607
  resumeNode9();
12791
- console.log(chalk20.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
14608
+ console.log(chalk24.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
12792
14609
  });
12793
14610
  var HOOK_BASED_AGENTS = {
12794
14611
  claude: "claude",
@@ -12801,15 +14618,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
12801
14618
  if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
12802
14619
  const target = HOOK_BASED_AGENTS[firstArg2];
12803
14620
  console.error(
12804
- chalk20.yellow(`
14621
+ chalk24.yellow(`
12805
14622
  \u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
12806
14623
  );
12807
- console.error(chalk20.white(`
14624
+ console.error(chalk24.white(`
12808
14625
  "${target}" uses its own hook system. Use:`));
12809
14626
  console.error(
12810
- chalk20.green(` node9 addto ${target} `) + chalk20.gray("# one-time setup")
14627
+ chalk24.green(` node9 addto ${target} `) + chalk24.gray("# one-time setup")
12811
14628
  );
12812
- console.error(chalk20.green(` ${target} `) + chalk20.gray("# run normally"));
14629
+ console.error(chalk24.green(` ${target} `) + chalk24.gray("# run normally"));
12813
14630
  process.exit(1);
12814
14631
  }
12815
14632
  const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
@@ -12826,7 +14643,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
12826
14643
  }
12827
14644
  );
12828
14645
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
12829
- console.error(chalk20.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
14646
+ console.error(chalk24.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
12830
14647
  const daemonReady = await autoStartDaemonAndWait();
12831
14648
  if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
12832
14649
  }
@@ -12839,12 +14656,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
12839
14656
  }
12840
14657
  if (!result.approved) {
12841
14658
  console.error(
12842
- chalk20.red(`
14659
+ chalk24.red(`
12843
14660
  \u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
12844
14661
  );
12845
14662
  process.exit(1);
12846
14663
  }
12847
- console.error(chalk20.green("\n\u2705 Approved \u2014 running command...\n"));
14664
+ console.error(chalk24.green("\n\u2705 Approved \u2014 running command...\n"));
12848
14665
  await runProxy(fullCommand);
12849
14666
  } else {
12850
14667
  program.help();
@@ -12854,14 +14671,18 @@ registerUndoCommand(program);
12854
14671
  registerShieldCommand(program);
12855
14672
  registerConfigShowCommand(program);
12856
14673
  registerTrustCommand(program);
14674
+ registerSyncCommand(program);
14675
+ registerAgentsCommand(program);
14676
+ registerScanCommand(program);
14677
+ registerSessionsCommand(program);
12857
14678
  if (process.argv[2] !== "daemon") {
12858
14679
  process.on("unhandledRejection", (reason) => {
12859
14680
  const isCheckHook = process.argv[2] === "check";
12860
14681
  if (isCheckHook) {
12861
14682
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
12862
- const logPath = path34.join(os27.homedir(), ".node9", "hook-debug.log");
14683
+ const logPath = path38.join(os31.homedir(), ".node9", "hook-debug.log");
12863
14684
  const msg = reason instanceof Error ? reason.message : String(reason);
12864
- fs31.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
14685
+ fs35.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
12865
14686
  `);
12866
14687
  }
12867
14688
  process.exit(0);