@node9/proxy 1.10.2 → 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
  }
@@ -2452,8 +2471,8 @@ function isDaemonRunning() {
2452
2471
  if (port !== DAEMON_PORT) {
2453
2472
  return false;
2454
2473
  }
2455
- const MAX_PID = 4194304;
2456
- if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || pid > MAX_PID) {
2474
+ const MAX_PID2 = 4194304;
2475
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || pid > MAX_PID2) {
2457
2476
  return false;
2458
2477
  }
2459
2478
  try {
@@ -6243,15 +6262,152 @@ var init_costSync = __esm({
6243
6262
  }
6244
6263
  });
6245
6264
 
6246
- // src/daemon/server.ts
6247
- import http from "http";
6265
+ // src/daemon/sync.ts
6248
6266
  import fs17 from "fs";
6267
+ import https from "https";
6268
+ import os15 from "os";
6249
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";
6250
6405
  import { randomUUID as randomUUID4 } from "crypto";
6251
6406
  import { spawnSync as spawnSync2 } from "child_process";
6252
6407
  import chalk2 from "chalk";
6253
6408
  function startDaemon() {
6254
6409
  startCostSync();
6410
+ startCloudSync();
6255
6411
  loadInsightCounts();
6256
6412
  const csrfToken = randomUUID4();
6257
6413
  const internalToken = randomUUID4();
@@ -6267,7 +6423,7 @@ function startDaemon() {
6267
6423
  idleTimer = setTimeout(() => {
6268
6424
  if (autoStarted) {
6269
6425
  try {
6270
- fs17.unlinkSync(DAEMON_PID_FILE);
6426
+ fs18.unlinkSync(DAEMON_PID_FILE);
6271
6427
  } catch {
6272
6428
  }
6273
6429
  }
@@ -6430,7 +6586,7 @@ data: ${JSON.stringify(item.data)}
6430
6586
  status: "pending"
6431
6587
  });
6432
6588
  }
6433
- const projectCwd = typeof cwd === "string" && path20.isAbsolute(cwd) ? cwd : void 0;
6589
+ const projectCwd = typeof cwd === "string" && path21.isAbsolute(cwd) ? cwd : void 0;
6434
6590
  const projectConfig = getConfig(projectCwd);
6435
6591
  const browserEnabled = projectConfig.settings.approvers?.browser !== false;
6436
6592
  const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
@@ -6820,8 +6976,8 @@ data: ${JSON.stringify(item.data)}
6820
6976
  const body = await readBody(req);
6821
6977
  const data = body ? JSON.parse(body) : {};
6822
6978
  const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
6823
- const node9Dir = path20.dirname(GLOBAL_CONFIG_PATH);
6824
- 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)) {
6825
6981
  res.writeHead(400, { "Content-Type": "application/json" });
6826
6982
  return res.end(
6827
6983
  JSON.stringify({ error: "configPath must be within the node9 config directory" })
@@ -6932,14 +7088,14 @@ data: ${JSON.stringify(item.data)}
6932
7088
  server.on("error", (e) => {
6933
7089
  if (e.code === "EADDRINUSE") {
6934
7090
  try {
6935
- if (fs17.existsSync(DAEMON_PID_FILE)) {
6936
- 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"));
6937
7093
  process.kill(pid, 0);
6938
7094
  return process.exit(0);
6939
7095
  }
6940
7096
  } catch {
6941
7097
  try {
6942
- fs17.unlinkSync(DAEMON_PID_FILE);
7098
+ fs18.unlinkSync(DAEMON_PID_FILE);
6943
7099
  } catch {
6944
7100
  }
6945
7101
  server.listen(DAEMON_PORT, DAEMON_HOST);
@@ -7008,56 +7164,327 @@ var init_server = __esm({
7008
7164
  init_patch();
7009
7165
  init_config_schema();
7010
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");
7011
7415
  }
7012
7416
  });
7013
7417
 
7014
7418
  // src/daemon/index.ts
7015
- import fs18 from "fs";
7419
+ import fs20 from "fs";
7016
7420
  import chalk3 from "chalk";
7017
- import { spawnSync as spawnSync3 } from "child_process";
7421
+ import { spawnSync as spawnSync4 } from "child_process";
7018
7422
  function stopDaemon() {
7019
- 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."));
7020
7424
  try {
7021
- 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
+ }
7022
7431
  process.kill(pid, "SIGTERM");
7023
7432
  console.log(chalk3.green("\u2705 Stopped."));
7024
7433
  } catch {
7025
7434
  console.log(chalk3.gray("Cleaned up stale PID file."));
7026
7435
  } finally {
7027
7436
  try {
7028
- fs18.unlinkSync(DAEMON_PID_FILE);
7437
+ fs20.unlinkSync(DAEMON_PID_FILE);
7029
7438
  } catch {
7030
7439
  }
7031
7440
  }
7032
7441
  }
7033
7442
  function daemonStatus() {
7034
- 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)) {
7035
7447
  try {
7036
- const { pid } = JSON.parse(fs18.readFileSync(DAEMON_PID_FILE, "utf-8"));
7037
- process.kill(pid, 0);
7038
- console.log(chalk3.green("Node9 daemon: running"));
7039
- 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
+ }
7040
7459
  } catch {
7041
- console.log(chalk3.yellow("Node9 daemon: not running (stale PID)"));
7042
- return;
7460
+ processStatus = chalk3.yellow("not running (stale PID file)");
7043
7461
  }
7044
- }
7045
- const r = spawnSync3("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
7046
- encoding: "utf8",
7047
- timeout: 500
7048
- });
7049
- if (r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`)) {
7050
- console.log(chalk3.yellow("Node9 daemon: running (no PID file \u2014 orphaned)"));
7051
7462
  } else {
7052
- 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
+ }
7053
7472
  }
7473
+ console.log(`
7474
+ Process : ${processStatus}`);
7475
+ console.log(` Service : ${serviceLabel}
7476
+ `);
7054
7477
  }
7478
+ var MAX_PID;
7055
7479
  var init_daemon2 = __esm({
7056
7480
  "src/daemon/index.ts"() {
7057
7481
  "use strict";
7058
7482
  init_server();
7059
7483
  init_state2();
7484
+ init_service();
7060
7485
  init_state2();
7486
+ init_service();
7487
+ MAX_PID = 4194304;
7061
7488
  }
7062
7489
  });
7063
7490
 
@@ -7067,10 +7494,10 @@ __export(tail_exports, {
7067
7494
  startTail: () => startTail
7068
7495
  });
7069
7496
  import http2 from "http";
7070
- import chalk19 from "chalk";
7071
- import fs29 from "fs";
7072
- import os25 from "os";
7073
- import path32 from "path";
7497
+ import chalk23 from "chalk";
7498
+ import fs33 from "fs";
7499
+ import os29 from "os";
7500
+ import path36 from "path";
7074
7501
  import readline5 from "readline";
7075
7502
  import { spawn as spawn10, execSync as execSync3 } from "child_process";
7076
7503
  function getIcon(tool) {
@@ -7093,22 +7520,22 @@ function formatBase(activity) {
7093
7520
  const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
7094
7521
  const icon = getIcon(activity.tool);
7095
7522
  const toolName = activity.tool.slice(0, 16).padEnd(16);
7096
- 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(), "~");
7097
7524
  const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
7098
- 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)}`;
7099
7526
  }
7100
7527
  function renderResult(activity, result) {
7101
7528
  const base = formatBase(activity);
7102
7529
  let status;
7103
7530
  if (result.status === "allow") {
7104
- status = chalk19.green("\u2713 ALLOW");
7531
+ status = chalk23.green("\u2713 ALLOW");
7105
7532
  } else if (result.status === "dlp") {
7106
- status = chalk19.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
7533
+ status = chalk23.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
7107
7534
  } else {
7108
- status = chalk19.red("\u2717 BLOCK");
7535
+ status = chalk23.red("\u2717 BLOCK");
7109
7536
  }
7110
7537
  const cost = result.costEstimate ?? activity.costEstimate;
7111
- 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"}`);
7112
7539
  if (process.stdout.isTTY) {
7113
7540
  if (pendingShownForId === activity.id && pendingWrappedLines > 1) {
7114
7541
  readline5.moveCursor(process.stdout, 0, -(pendingWrappedLines - 1));
@@ -7125,19 +7552,19 @@ function renderResult(activity, result) {
7125
7552
  }
7126
7553
  function renderPending(activity) {
7127
7554
  if (!process.stdout.isTTY) return;
7128
- const line = `${formatBase(activity)} ${chalk19.yellow("\u25CF \u2026")}`;
7555
+ const line = `${formatBase(activity)} ${chalk23.yellow("\u25CF \u2026")}`;
7129
7556
  pendingShownForId = activity.id;
7130
7557
  pendingWrappedLines = wrappedLineCount(line);
7131
7558
  process.stdout.write(`${line}\r`);
7132
7559
  }
7133
7560
  async function ensureDaemon() {
7134
7561
  let pidPort = null;
7135
- if (fs29.existsSync(PID_FILE)) {
7562
+ if (fs33.existsSync(PID_FILE)) {
7136
7563
  try {
7137
- const { port } = JSON.parse(fs29.readFileSync(PID_FILE, "utf-8"));
7564
+ const { port } = JSON.parse(fs33.readFileSync(PID_FILE, "utf-8"));
7138
7565
  pidPort = port;
7139
7566
  } catch {
7140
- 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."));
7141
7568
  }
7142
7569
  }
7143
7570
  const checkPort = pidPort ?? DAEMON_PORT;
@@ -7148,7 +7575,7 @@ async function ensureDaemon() {
7148
7575
  if (res.ok) return checkPort;
7149
7576
  } catch {
7150
7577
  }
7151
- console.log(chalk19.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
7578
+ console.log(chalk23.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
7152
7579
  const child = spawn10(process.execPath, [process.argv[1], "daemon"], {
7153
7580
  detached: true,
7154
7581
  stdio: "ignore",
@@ -7165,7 +7592,7 @@ async function ensureDaemon() {
7165
7592
  } catch {
7166
7593
  }
7167
7594
  }
7168
- 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"));
7169
7596
  process.exit(1);
7170
7597
  }
7171
7598
  function postDecisionHttp(id, decision, csrfToken, port, opts) {
@@ -7197,27 +7624,56 @@ function postDecisionHttp(id, decision, csrfToken, port, opts) {
7197
7624
  req.end(body);
7198
7625
  });
7199
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
+ }
7200
7649
  function buildCardLines(req, localCount = 0) {
7201
7650
  if (req.recoveryCommand) {
7202
7651
  return buildRecoveryCardLines(req);
7203
7652
  }
7204
- const argsStr = JSON.stringify(req.args ?? {}).replace(/\s+/g, " ");
7205
- const argsPreview = argsStr.length > 60 ? argsStr.slice(0, 60) + "\u2026" : argsStr;
7206
- 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`;
7207
- 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) : "";
7208
7661
  const lines = [
7209
7662
  ``,
7210
7663
  `${BOLD2}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET2}`,
7211
7664
  `${CYAN}\u2551${RESET2} Tool: ${BOLD2}${req.toolName}${RESET2}`,
7212
- `${CYAN}\u2551${RESET2} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET2}`
7665
+ `${CYAN}\u2551${RESET2} Policy: ${severityIcon} ${blockedBy}${RESET2}`
7213
7666
  ];
7214
- if (req.riskMetadata?.ruleDescription) {
7215
- lines.push(`${CYAN}\u2551${RESET2} ${YELLOW}\u2139 ${req.riskMetadata.ruleDescription}${RESET2}`);
7667
+ if (description) {
7668
+ lines.push(`${CYAN}\u2551${RESET2} Why: ${YELLOW}${description}${RESET2}`);
7216
7669
  }
7217
- if (req.riskMetadata?.ruleName && blockedBy.includes("Taint")) {
7670
+ if (req.riskMetadata?.ruleName && rawBlockedBy.includes("Taint")) {
7218
7671
  lines.push(`${CYAN}\u2551${RESET2} ${YELLOW}\u26A0 ${req.riskMetadata.ruleName}${RESET2}`);
7219
7672
  }
7220
- 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
+ }
7221
7677
  if (localCount >= 2) {
7222
7678
  lines.push(
7223
7679
  `${CYAN}\u2551${RESET2} ${YELLOW}\u{1F4A1}${RESET2} Approved ${localCount}\xD7 before \u2014 ${BOLD2}[a]${RESET2}${YELLOW} creates a permanent rule${RESET2}`
@@ -7257,9 +7713,9 @@ function buildRecoveryCardLines(req) {
7257
7713
  ];
7258
7714
  }
7259
7715
  function readApproversFromDisk() {
7260
- const configPath = path32.join(os25.homedir(), ".node9", "config.json");
7716
+ const configPath = path36.join(os29.homedir(), ".node9", "config.json");
7261
7717
  try {
7262
- const raw = JSON.parse(fs29.readFileSync(configPath, "utf-8"));
7718
+ const raw = JSON.parse(fs33.readFileSync(configPath, "utf-8"));
7263
7719
  const settings = raw.settings ?? {};
7264
7720
  return settings.approvers ?? {};
7265
7721
  } catch {
@@ -7270,20 +7726,20 @@ function approverStatusLine() {
7270
7726
  const a = readApproversFromDisk();
7271
7727
  const fmt = (label, key) => {
7272
7728
  const on = a[key] !== false;
7273
- 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")}`;
7274
7730
  };
7275
7731
  return `${fmt("native", "native")} ${fmt("browser", "browser")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
7276
7732
  }
7277
7733
  function toggleApprover(channel) {
7278
- const configPath = path32.join(os25.homedir(), ".node9", "config.json");
7734
+ const configPath = path36.join(os29.homedir(), ".node9", "config.json");
7279
7735
  try {
7280
- const raw = JSON.parse(fs29.readFileSync(configPath, "utf-8"));
7736
+ const raw = JSON.parse(fs33.readFileSync(configPath, "utf-8"));
7281
7737
  const settings = raw.settings ?? {};
7282
7738
  const approvers = settings.approvers ?? {};
7283
7739
  approvers[channel] = approvers[channel] === false;
7284
7740
  settings.approvers = approvers;
7285
7741
  raw.settings = settings;
7286
- fs29.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
7742
+ fs33.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
7287
7743
  } catch (err2) {
7288
7744
  process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
7289
7745
  `);
@@ -7315,7 +7771,7 @@ async function startTail(options = {}) {
7315
7771
  req2.end();
7316
7772
  });
7317
7773
  if (result.ok) {
7318
- console.log(chalk19.green("\u2713 Flight Recorder buffer cleared."));
7774
+ console.log(chalk23.green("\u2713 Flight Recorder buffer cleared."));
7319
7775
  } else if (result.code === "ECONNREFUSED") {
7320
7776
  throw new Error("Daemon is not running. Start it with: node9 daemon start");
7321
7777
  } else if (result.code === "ETIMEDOUT") {
@@ -7359,7 +7815,7 @@ async function startTail(options = {}) {
7359
7815
  const channel = name === "n" ? "native" : name === "b" ? "browser" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
7360
7816
  if (channel) {
7361
7817
  toggleApprover(channel);
7362
- console.log(chalk19.dim(` Approvers: ${approverStatusLine()}`));
7818
+ console.log(chalk23.dim(` Approvers: ${approverStatusLine()}`));
7363
7819
  }
7364
7820
  };
7365
7821
  process.stdin.on("keypress", idleKeypressHandler);
@@ -7425,7 +7881,7 @@ async function startTail(options = {}) {
7425
7881
  localAllowCounts.get(req2.toolName) ?? 0
7426
7882
  )
7427
7883
  );
7428
- 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");
7429
7885
  stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
7430
7886
  for (const line of stampedLines) process.stdout.write(line + "\n");
7431
7887
  process.stdout.write(SHOW_CURSOR);
@@ -7453,8 +7909,8 @@ async function startTail(options = {}) {
7453
7909
  }
7454
7910
  postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err2) => {
7455
7911
  try {
7456
- fs29.appendFileSync(
7457
- path32.join(os25.homedir(), ".node9", "hook-debug.log"),
7912
+ fs33.appendFileSync(
7913
+ path36.join(os29.homedir(), ".node9", "hook-debug.log"),
7458
7914
  `[tail] POST /decision failed: ${String(err2)}
7459
7915
  `
7460
7916
  );
@@ -7476,7 +7932,7 @@ async function startTail(options = {}) {
7476
7932
  );
7477
7933
  const stampedLines = buildCardLines(req2, priorCount);
7478
7934
  if (externalDecision) {
7479
- 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");
7480
7936
  stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
7481
7937
  }
7482
7938
  for (const line of stampedLines) process.stdout.write(line + "\n");
@@ -7535,16 +7991,16 @@ async function startTail(options = {}) {
7535
7991
  }
7536
7992
  } catch {
7537
7993
  }
7538
- console.log(chalk19.cyan.bold(`
7539
- \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}`));
7540
7996
  if (canApprove) {
7541
- console.log(chalk19.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
7542
- 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`));
7543
7999
  }
7544
8000
  if (options.history) {
7545
- console.log(chalk19.dim("Showing history + live events.\n"));
8001
+ console.log(chalk23.dim("Showing history + live events.\n"));
7546
8002
  } else {
7547
- 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"));
7548
8004
  }
7549
8005
  process.on("SIGINT", () => {
7550
8006
  exitIdleMode();
@@ -7554,13 +8010,13 @@ async function startTail(options = {}) {
7554
8010
  readline5.clearLine(process.stdout, 0);
7555
8011
  readline5.cursorTo(process.stdout, 0);
7556
8012
  }
7557
- console.log(chalk19.dim("\n\u{1F6F0}\uFE0F Disconnected."));
8013
+ console.log(chalk23.dim("\n\u{1F6F0}\uFE0F Disconnected."));
7558
8014
  process.exit(0);
7559
8015
  });
7560
8016
  const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
7561
8017
  const req = http2.get(sseUrl, (res) => {
7562
8018
  if (res.statusCode !== 200) {
7563
- console.error(chalk19.red(`Failed to connect: HTTP ${res.statusCode}`));
8019
+ console.error(chalk23.red(`Failed to connect: HTTP ${res.statusCode}`));
7564
8020
  process.exit(1);
7565
8021
  }
7566
8022
  if (canApprove) enterIdleMode();
@@ -7591,7 +8047,7 @@ async function startTail(options = {}) {
7591
8047
  readline5.clearLine(process.stdout, 0);
7592
8048
  readline5.cursorTo(process.stdout, 0);
7593
8049
  }
7594
- console.log(chalk19.red("\n\u274C Daemon disconnected."));
8050
+ console.log(chalk23.red("\n\u274C Daemon disconnected."));
7595
8051
  process.exit(1);
7596
8052
  });
7597
8053
  });
@@ -7683,9 +8139,9 @@ async function startTail(options = {}) {
7683
8139
  const hash = data.hash ?? "";
7684
8140
  const summary = data.argsSummary ?? data.tool;
7685
8141
  const fileCount = data.fileCount ?? 0;
7686
- 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"}`) : "";
7687
8143
  process.stdout.write(
7688
- `${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}
7689
8145
  `
7690
8146
  );
7691
8147
  return;
@@ -7702,7 +8158,7 @@ async function startTail(options = {}) {
7702
8158
  }
7703
8159
  req.on("error", (err2) => {
7704
8160
  const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
7705
- console.error(chalk19.red(`
8161
+ console.error(chalk23.red(`
7706
8162
  \u274C ${msg}`));
7707
8163
  process.exit(1);
7708
8164
  });
@@ -7714,7 +8170,7 @@ var init_tail = __esm({
7714
8170
  init_daemon2();
7715
8171
  init_daemon();
7716
8172
  init_core();
7717
- PID_FILE = path32.join(os25.homedir(), ".node9", "daemon.pid");
8173
+ PID_FILE = path36.join(os29.homedir(), ".node9", "daemon.pid");
7718
8174
  ICONS = {
7719
8175
  bash: "\u{1F4BB}",
7720
8176
  shell: "\u{1F4BB}",
@@ -7755,9 +8211,9 @@ __export(hud_exports, {
7755
8211
  main: () => main,
7756
8212
  renderEnvironmentLine: () => renderEnvironmentLine
7757
8213
  });
7758
- import fs30 from "fs";
7759
- import path33 from "path";
7760
- import os26 from "os";
8214
+ import fs34 from "fs";
8215
+ import path37 from "path";
8216
+ import os30 from "os";
7761
8217
  import http3 from "http";
7762
8218
  async function readStdin() {
7763
8219
  const chunks = [];
@@ -7833,9 +8289,9 @@ function formatTimeLeft(resetsAt) {
7833
8289
  return ` (${m}m left)`;
7834
8290
  }
7835
8291
  function safeReadJson(filePath) {
7836
- if (!fs30.existsSync(filePath)) return null;
8292
+ if (!fs34.existsSync(filePath)) return null;
7837
8293
  try {
7838
- return JSON.parse(fs30.readFileSync(filePath, "utf-8"));
8294
+ return JSON.parse(fs34.readFileSync(filePath, "utf-8"));
7839
8295
  } catch {
7840
8296
  return null;
7841
8297
  }
@@ -7856,12 +8312,12 @@ function countHooksInFile(filePath) {
7856
8312
  return Object.keys(cfg.hooks).length;
7857
8313
  }
7858
8314
  function countRulesInDir(rulesDir) {
7859
- if (!fs30.existsSync(rulesDir)) return 0;
8315
+ if (!fs34.existsSync(rulesDir)) return 0;
7860
8316
  let count = 0;
7861
8317
  try {
7862
- for (const entry of fs30.readdirSync(rulesDir, { withFileTypes: true })) {
8318
+ for (const entry of fs34.readdirSync(rulesDir, { withFileTypes: true })) {
7863
8319
  if (entry.isDirectory()) {
7864
- count += countRulesInDir(path33.join(rulesDir, entry.name));
8320
+ count += countRulesInDir(path37.join(rulesDir, entry.name));
7865
8321
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
7866
8322
  count++;
7867
8323
  }
@@ -7872,46 +8328,46 @@ function countRulesInDir(rulesDir) {
7872
8328
  }
7873
8329
  function isSamePath(a, b) {
7874
8330
  try {
7875
- return path33.resolve(a) === path33.resolve(b);
8331
+ return path37.resolve(a) === path37.resolve(b);
7876
8332
  } catch {
7877
8333
  return false;
7878
8334
  }
7879
8335
  }
7880
8336
  function countConfigs(cwd) {
7881
- const homeDir2 = os26.homedir();
7882
- const claudeDir = path33.join(homeDir2, ".claude");
8337
+ const homeDir2 = os30.homedir();
8338
+ const claudeDir = path37.join(homeDir2, ".claude");
7883
8339
  let claudeMdCount = 0;
7884
8340
  let rulesCount = 0;
7885
8341
  let hooksCount = 0;
7886
8342
  const userMcpServers = /* @__PURE__ */ new Set();
7887
8343
  const projectMcpServers = /* @__PURE__ */ new Set();
7888
- if (fs30.existsSync(path33.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
7889
- rulesCount += countRulesInDir(path33.join(claudeDir, "rules"));
7890
- 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");
7891
8347
  for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
7892
8348
  hooksCount += countHooksInFile(userSettings);
7893
- const userClaudeJson = path33.join(homeDir2, ".claude.json");
8349
+ const userClaudeJson = path37.join(homeDir2, ".claude.json");
7894
8350
  for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
7895
8351
  for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
7896
8352
  userMcpServers.delete(name);
7897
8353
  }
7898
8354
  if (cwd) {
7899
- if (fs30.existsSync(path33.join(cwd, "CLAUDE.md"))) claudeMdCount++;
7900
- if (fs30.existsSync(path33.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
7901
- 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");
7902
8358
  const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
7903
8359
  if (!overlapsUserScope) {
7904
- if (fs30.existsSync(path33.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
7905
- rulesCount += countRulesInDir(path33.join(projectClaudeDir, "rules"));
7906
- 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");
7907
8363
  for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
7908
8364
  hooksCount += countHooksInFile(projSettings);
7909
8365
  }
7910
- if (fs30.existsSync(path33.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
7911
- 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");
7912
8368
  for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
7913
8369
  hooksCount += countHooksInFile(localSettings);
7914
- const mcpJsonServers = getMcpServerNames(path33.join(cwd, ".mcp.json"));
8370
+ const mcpJsonServers = getMcpServerNames(path37.join(cwd, ".mcp.json"));
7915
8371
  const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
7916
8372
  for (const name of disabledMcpJson) mcpJsonServers.delete(name);
7917
8373
  for (const name of mcpJsonServers) projectMcpServers.add(name);
@@ -7944,12 +8400,12 @@ function readActiveShieldsHud() {
7944
8400
  return shieldsCache.value;
7945
8401
  }
7946
8402
  try {
7947
- const shieldsPath = path33.join(os26.homedir(), ".node9", "shields.json");
7948
- if (!fs30.existsSync(shieldsPath)) {
8403
+ const shieldsPath = path37.join(os30.homedir(), ".node9", "shields.json");
8404
+ if (!fs34.existsSync(shieldsPath)) {
7949
8405
  shieldsCache = { value: [], ts: now };
7950
8406
  return [];
7951
8407
  }
7952
- const parsed = JSON.parse(fs30.readFileSync(shieldsPath, "utf-8"));
8408
+ const parsed = JSON.parse(fs34.readFileSync(shieldsPath, "utf-8"));
7953
8409
  if (!Array.isArray(parsed.active)) {
7954
8410
  shieldsCache = { value: [], ts: now };
7955
8411
  return [];
@@ -8051,17 +8507,17 @@ function renderContextLine(stdin) {
8051
8507
  async function main() {
8052
8508
  try {
8053
8509
  const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
8054
- if (fs30.existsSync(path33.join(os26.homedir(), ".node9", "hud-debug"))) {
8510
+ if (fs34.existsSync(path37.join(os30.homedir(), ".node9", "hud-debug"))) {
8055
8511
  try {
8056
- const logPath = path33.join(os26.homedir(), ".node9", "hud-debug.log");
8512
+ const logPath = path37.join(os30.homedir(), ".node9", "hud-debug.log");
8057
8513
  const MAX_LOG_SIZE = 10 * 1024 * 1024;
8058
8514
  let size = 0;
8059
8515
  try {
8060
- size = fs30.statSync(logPath).size;
8516
+ size = fs34.statSync(logPath).size;
8061
8517
  } catch {
8062
8518
  }
8063
8519
  if (size < MAX_LOG_SIZE) {
8064
- fs30.appendFileSync(
8520
+ fs34.appendFileSync(
8065
8521
  logPath,
8066
8522
  JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
8067
8523
  );
@@ -8082,11 +8538,11 @@ async function main() {
8082
8538
  try {
8083
8539
  const cwd = stdin.cwd ?? process.cwd();
8084
8540
  for (const configPath of [
8085
- path33.join(cwd, "node9.config.json"),
8086
- path33.join(os26.homedir(), ".node9", "config.json")
8541
+ path37.join(cwd, "node9.config.json"),
8542
+ path37.join(os30.homedir(), ".node9", "config.json")
8087
8543
  ]) {
8088
- if (!fs30.existsSync(configPath)) continue;
8089
- 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"));
8090
8546
  const hud = cfg.settings?.hud;
8091
8547
  if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
8092
8548
  }
@@ -8517,7 +8973,9 @@ function detectAgents(homeDir2 = os11.homedir()) {
8517
8973
  claude: exists(path15.join(homeDir2, ".claude")) || exists(path15.join(homeDir2, ".claude.json")),
8518
8974
  gemini: exists(path15.join(homeDir2, ".gemini")),
8519
8975
  cursor: exists(path15.join(homeDir2, ".cursor")),
8520
- 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"))
8521
8979
  };
8522
8980
  }
8523
8981
  async function setupCursor() {
@@ -8666,6 +9124,38 @@ async function setupCodex() {
8666
9124
  printDaemonTip();
8667
9125
  }
8668
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
+ }
8669
9159
  function setupHud() {
8670
9160
  const homeDir2 = os11.homedir();
8671
9161
  const hooksPath = path15.join(homeDir2, ".claude", "settings.json");
@@ -8712,43 +9202,315 @@ function teardownHud() {
8712
9202
  console.log(chalk.green(" \u2705 node9 HUD removed from ~/.claude/settings.json"));
8713
9203
  console.log(chalk.gray(" Restart Claude Code for changes to take effect."));
8714
9204
  }
8715
-
8716
- // src/cli.ts
8717
- init_daemon2();
8718
- import chalk20 from "chalk";
8719
- import fs31 from "fs";
8720
- import path34 from "path";
8721
- import os27 from "os";
8722
- import { confirm as confirm2 } from "@inquirer/prompts";
8723
-
8724
- // src/utils/duration.ts
8725
- function parseDuration(str) {
8726
- const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
8727
- if (!m) return null;
8728
- const n = parseFloat(m[1]);
8729
- switch ((m[2] ?? "m").toLowerCase()) {
8730
- case "s":
8731
- return Math.round(n * 1e3);
8732
- case "m":
8733
- return Math.round(n * 6e4);
8734
- case "h":
8735
- return Math.round(n * 36e5);
8736
- case "d":
8737
- return Math.round(n * 864e5);
8738
- default:
8739
- return null;
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;
8740
9217
  }
8741
- }
8742
-
8743
- // src/proxy/index.ts
8744
- init_orchestrator();
8745
- import readline from "readline";
8746
- import chalk4 from "chalk";
8747
- import { spawn as spawn3 } from "child_process";
8748
- import { execa } from "execa";
8749
- import { parseCommandString } from "execa";
8750
-
8751
- // src/policy/negotiation.ts
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
+ }
9504
+
9505
+ // src/proxy/index.ts
9506
+ init_orchestrator();
9507
+ import readline from "readline";
9508
+ import chalk4 from "chalk";
9509
+ import { spawn as spawn3 } from "child_process";
9510
+ import { execa } from "execa";
9511
+ import { parseCommandString } from "execa";
9512
+
9513
+ // src/policy/negotiation.ts
8752
9514
  function buildNegotiationMessage(blockedByLabel, isHumanDecision, humanReason, recoveryCommand) {
8753
9515
  if (isHumanDecision) {
8754
9516
  return `NODE9: The human user rejected this action.
@@ -8947,19 +9709,19 @@ init_daemon();
8947
9709
  init_config();
8948
9710
  init_policy();
8949
9711
  import chalk5 from "chalk";
8950
- import fs20 from "fs";
9712
+ import fs22 from "fs";
8951
9713
  import { spawn as spawn6 } from "child_process";
8952
- import path22 from "path";
8953
- import os16 from "os";
9714
+ import path24 from "path";
9715
+ import os18 from "os";
8954
9716
 
8955
9717
  // src/undo.ts
8956
- import { spawnSync as spawnSync4, spawn as spawn5 } from "child_process";
9718
+ import { spawnSync as spawnSync5, spawn as spawn5 } from "child_process";
8957
9719
  import crypto3 from "crypto";
8958
- import fs19 from "fs";
9720
+ import fs21 from "fs";
8959
9721
  import net3 from "net";
8960
- import path21 from "path";
8961
- import os15 from "os";
8962
- 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");
8963
9725
  function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
8964
9726
  try {
8965
9727
  const payload = JSON.stringify({
@@ -8979,22 +9741,22 @@ function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
8979
9741
  } catch {
8980
9742
  }
8981
9743
  }
8982
- var SNAPSHOT_STACK_PATH = path21.join(os15.homedir(), ".node9", "snapshots.json");
8983
- 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");
8984
9746
  var MAX_SNAPSHOTS = 10;
8985
9747
  var GIT_TIMEOUT = 15e3;
8986
9748
  function readStack() {
8987
9749
  try {
8988
- if (fs19.existsSync(SNAPSHOT_STACK_PATH))
8989
- 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"));
8990
9752
  } catch {
8991
9753
  }
8992
9754
  return [];
8993
9755
  }
8994
9756
  function writeStack(stack) {
8995
- const dir = path21.dirname(SNAPSHOT_STACK_PATH);
8996
- if (!fs19.existsSync(dir)) fs19.mkdirSync(dir, { recursive: true });
8997
- 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));
8998
9760
  }
8999
9761
  function extractFilePath(args) {
9000
9762
  if (!args || typeof args !== "object") return null;
@@ -9014,12 +9776,12 @@ function buildArgsSummary(tool, args) {
9014
9776
  return "";
9015
9777
  }
9016
9778
  function findProjectRoot(filePath) {
9017
- let dir = path21.dirname(filePath);
9779
+ let dir = path23.dirname(filePath);
9018
9780
  while (true) {
9019
- 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"))) {
9020
9782
  return dir;
9021
9783
  }
9022
- const parent = path21.dirname(dir);
9784
+ const parent = path23.dirname(dir);
9023
9785
  if (parent === dir) return process.cwd();
9024
9786
  dir = parent;
9025
9787
  }
@@ -9027,7 +9789,7 @@ function findProjectRoot(filePath) {
9027
9789
  function normalizeCwdForHash(cwd) {
9028
9790
  let normalized;
9029
9791
  try {
9030
- normalized = fs19.realpathSync(cwd);
9792
+ normalized = fs21.realpathSync(cwd);
9031
9793
  } catch {
9032
9794
  normalized = cwd;
9033
9795
  }
@@ -9037,16 +9799,16 @@ function normalizeCwdForHash(cwd) {
9037
9799
  }
9038
9800
  function getShadowRepoDir(cwd) {
9039
9801
  const hash = crypto3.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
9040
- return path21.join(os15.homedir(), ".node9", "snapshots", hash);
9802
+ return path23.join(os17.homedir(), ".node9", "snapshots", hash);
9041
9803
  }
9042
9804
  function cleanOrphanedIndexFiles(shadowDir) {
9043
9805
  try {
9044
9806
  const cutoff = Date.now() - 6e4;
9045
- for (const f of fs19.readdirSync(shadowDir)) {
9807
+ for (const f of fs21.readdirSync(shadowDir)) {
9046
9808
  if (f.startsWith("index_")) {
9047
- const fp = path21.join(shadowDir, f);
9809
+ const fp = path23.join(shadowDir, f);
9048
9810
  try {
9049
- if (fs19.statSync(fp).mtimeMs < cutoff) fs19.unlinkSync(fp);
9811
+ if (fs21.statSync(fp).mtimeMs < cutoff) fs21.unlinkSync(fp);
9050
9812
  } catch {
9051
9813
  }
9052
9814
  }
@@ -9058,7 +9820,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
9058
9820
  const hardcoded = [".git", ".node9"];
9059
9821
  const lines = [...hardcoded, ...ignorePaths].join("\n");
9060
9822
  try {
9061
- fs19.writeFileSync(path21.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
9823
+ fs21.writeFileSync(path23.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
9062
9824
  } catch {
9063
9825
  }
9064
9826
  }
@@ -9066,54 +9828,54 @@ function ensureShadowRepo(shadowDir, cwd) {
9066
9828
  cleanOrphanedIndexFiles(shadowDir);
9067
9829
  const normalizedCwd = normalizeCwdForHash(cwd);
9068
9830
  const shadowEnvBase = { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd };
9069
- const check = spawnSync4("git", ["rev-parse", "--git-dir"], {
9831
+ const check = spawnSync5("git", ["rev-parse", "--git-dir"], {
9070
9832
  env: shadowEnvBase,
9071
9833
  timeout: 3e3
9072
9834
  });
9073
9835
  if (check.status === 0) {
9074
- const ptPath = path21.join(shadowDir, "project-path.txt");
9836
+ const ptPath = path23.join(shadowDir, "project-path.txt");
9075
9837
  try {
9076
- const stored = fs19.readFileSync(ptPath, "utf8").trim();
9838
+ const stored = fs21.readFileSync(ptPath, "utf8").trim();
9077
9839
  if (stored === normalizedCwd) return true;
9078
9840
  if (process.env.NODE9_DEBUG === "1")
9079
9841
  console.error(
9080
9842
  `[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
9081
9843
  );
9082
- fs19.rmSync(shadowDir, { recursive: true, force: true });
9844
+ fs21.rmSync(shadowDir, { recursive: true, force: true });
9083
9845
  } catch {
9084
9846
  try {
9085
- fs19.writeFileSync(ptPath, normalizedCwd, "utf8");
9847
+ fs21.writeFileSync(ptPath, normalizedCwd, "utf8");
9086
9848
  } catch {
9087
9849
  }
9088
9850
  return true;
9089
9851
  }
9090
9852
  }
9091
9853
  try {
9092
- fs19.mkdirSync(shadowDir, { recursive: true });
9854
+ fs21.mkdirSync(shadowDir, { recursive: true });
9093
9855
  } catch {
9094
9856
  }
9095
- const init = spawnSync4("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
9857
+ const init = spawnSync5("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
9096
9858
  if (init.status !== 0 || init.error) {
9097
9859
  const reason = init.error ? init.error.message : init.stderr?.toString();
9098
9860
  if (process.env.NODE9_DEBUG === "1") console.error("[Node9] git init --bare failed:", reason);
9099
9861
  return false;
9100
9862
  }
9101
- const configFile = path21.join(shadowDir, "config");
9102
- spawnSync4("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
9863
+ const configFile = path23.join(shadowDir, "config");
9864
+ spawnSync5("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
9103
9865
  timeout: 3e3
9104
9866
  });
9105
- spawnSync4("git", ["config", "--file", configFile, "core.fsmonitor", "true"], {
9867
+ spawnSync5("git", ["config", "--file", configFile, "core.fsmonitor", "true"], {
9106
9868
  timeout: 3e3
9107
9869
  });
9108
9870
  try {
9109
- fs19.writeFileSync(path21.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
9871
+ fs21.writeFileSync(path23.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
9110
9872
  } catch {
9111
9873
  }
9112
9874
  return true;
9113
9875
  }
9114
9876
  function buildGitEnv(cwd) {
9115
9877
  const shadowDir = getShadowRepoDir(cwd);
9116
- const check = spawnSync4("git", ["rev-parse", "--git-dir"], {
9878
+ const check = spawnSync5("git", ["rev-parse", "--git-dir"], {
9117
9879
  env: { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd },
9118
9880
  timeout: 2e3
9119
9881
  });
@@ -9126,23 +9888,23 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
9126
9888
  let indexFile = null;
9127
9889
  try {
9128
9890
  const rawFilePath = extractFilePath(args);
9129
- const absFilePath = rawFilePath && path21.isAbsolute(rawFilePath) ? rawFilePath : null;
9891
+ const absFilePath = rawFilePath && path23.isAbsolute(rawFilePath) ? rawFilePath : null;
9130
9892
  const cwd = absFilePath ? findProjectRoot(absFilePath) : process.cwd();
9131
9893
  const shadowDir = getShadowRepoDir(cwd);
9132
9894
  if (!ensureShadowRepo(shadowDir, cwd)) return null;
9133
9895
  writeShadowExcludes(shadowDir, ignorePaths);
9134
- indexFile = path21.join(shadowDir, `index_${process.pid}_${Date.now()}`);
9896
+ indexFile = path23.join(shadowDir, `index_${process.pid}_${Date.now()}`);
9135
9897
  const shadowEnv = {
9136
9898
  ...process.env,
9137
9899
  GIT_DIR: shadowDir,
9138
9900
  GIT_WORK_TREE: cwd,
9139
9901
  GIT_INDEX_FILE: indexFile
9140
9902
  };
9141
- spawnSync4("git", ["add", "-A"], { env: shadowEnv, timeout: GIT_TIMEOUT });
9142
- 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 });
9143
9905
  const treeHash = treeRes.stdout?.toString().trim();
9144
9906
  if (!treeHash || treeRes.status !== 0) return null;
9145
- const commitRes = spawnSync4(
9907
+ const commitRes = spawnSync5(
9146
9908
  "git",
9147
9909
  ["commit-tree", treeHash, "-m", `Node9 AI Snapshot: ${(/* @__PURE__ */ new Date()).toISOString()}`],
9148
9910
  { env: shadowEnv, timeout: GIT_TIMEOUT }
@@ -9154,7 +9916,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
9154
9916
  let capturedFiles = [];
9155
9917
  let capturedDiff = null;
9156
9918
  if (prevEntry) {
9157
- const filesRes = spawnSync4("git", ["diff", "--name-only", prevEntry.hash, commitHash], {
9919
+ const filesRes = spawnSync5("git", ["diff", "--name-only", prevEntry.hash, commitHash], {
9158
9920
  env: shadowEnv,
9159
9921
  timeout: GIT_TIMEOUT
9160
9922
  });
@@ -9164,7 +9926,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
9164
9926
  if (capturedFiles.length === 0) {
9165
9927
  return prevEntry.hash;
9166
9928
  }
9167
- const diffRes = spawnSync4("git", ["diff", prevEntry.hash, commitHash], {
9929
+ const diffRes = spawnSync5("git", ["diff", prevEntry.hash, commitHash], {
9168
9930
  env: shadowEnv,
9169
9931
  timeout: GIT_TIMEOUT
9170
9932
  });
@@ -9172,7 +9934,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
9172
9934
  capturedDiff = diffRes.stdout?.toString() || null;
9173
9935
  }
9174
9936
  } else {
9175
- const filesRes = spawnSync4("git", ["ls-tree", "-r", "--name-only", commitHash], {
9937
+ const filesRes = spawnSync5("git", ["ls-tree", "-r", "--name-only", commitHash], {
9176
9938
  env: shadowEnv,
9177
9939
  timeout: GIT_TIMEOUT
9178
9940
  });
@@ -9203,7 +9965,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
9203
9965
  writeStack(stack);
9204
9966
  const entry = stack[stack.length - 1];
9205
9967
  notifySnapshotTaken(commitHash.slice(0, 7), tool, entry.argsSummary, capturedFiles.length);
9206
- fs19.writeFileSync(UNDO_LATEST_PATH, commitHash);
9968
+ fs21.writeFileSync(UNDO_LATEST_PATH, commitHash);
9207
9969
  if (shouldGc) {
9208
9970
  spawn5("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
9209
9971
  }
@@ -9214,7 +9976,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
9214
9976
  } finally {
9215
9977
  if (indexFile) {
9216
9978
  try {
9217
- fs19.unlinkSync(indexFile);
9979
+ fs21.unlinkSync(indexFile);
9218
9980
  } catch {
9219
9981
  }
9220
9982
  }
@@ -9226,14 +9988,14 @@ function getSnapshotHistory() {
9226
9988
  function computeUndoDiff(hash, cwd) {
9227
9989
  try {
9228
9990
  const env = buildGitEnv(cwd);
9229
- const statRes = spawnSync4("git", ["diff", hash, "--stat", "--", "."], {
9991
+ const statRes = spawnSync5("git", ["diff", hash, "--stat", "--", "."], {
9230
9992
  cwd,
9231
9993
  env,
9232
9994
  timeout: GIT_TIMEOUT
9233
9995
  });
9234
9996
  const stat = statRes.stdout?.toString().trim();
9235
9997
  if (!stat || statRes.status !== 0) return null;
9236
- const diffRes = spawnSync4("git", ["diff", hash, "--", "."], {
9998
+ const diffRes = spawnSync5("git", ["diff", hash, "--", "."], {
9237
9999
  cwd,
9238
10000
  env,
9239
10001
  timeout: GIT_TIMEOUT
@@ -9252,7 +10014,7 @@ function applyUndo(hash, cwd) {
9252
10014
  try {
9253
10015
  const dir = cwd ?? process.cwd();
9254
10016
  const env = buildGitEnv(dir);
9255
- const restore = spawnSync4("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
10017
+ const restore = spawnSync5("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
9256
10018
  cwd: dir,
9257
10019
  env,
9258
10020
  timeout: GIT_TIMEOUT
@@ -9264,7 +10026,7 @@ function applyUndo(hash, cwd) {
9264
10026
  }
9265
10027
  return false;
9266
10028
  }
9267
- const lsTree = spawnSync4("git", ["ls-tree", "-r", "--name-only", hash], {
10029
+ const lsTree = spawnSync5("git", ["ls-tree", "-r", "--name-only", hash], {
9268
10030
  cwd: dir,
9269
10031
  env,
9270
10032
  timeout: GIT_TIMEOUT
@@ -9283,16 +10045,16 @@ function applyUndo(hash, cwd) {
9283
10045
  `);
9284
10046
  return false;
9285
10047
  }
9286
- const tracked = spawnSync4("git", ["ls-files"], { cwd: dir, env, timeout: GIT_TIMEOUT }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
9287
- 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"], {
9288
10050
  cwd: dir,
9289
10051
  env,
9290
10052
  timeout: GIT_TIMEOUT
9291
10053
  }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
9292
10054
  for (const file of [...tracked, ...untracked]) {
9293
- const fullPath = path21.join(dir, file);
9294
- if (!snapshotFiles.has(file) && fs19.existsSync(fullPath)) {
9295
- fs19.unlinkSync(fullPath);
10055
+ const fullPath = path23.join(dir, file);
10056
+ if (!snapshotFiles.has(file) && fs21.existsSync(fullPath)) {
10057
+ fs21.unlinkSync(fullPath);
9296
10058
  }
9297
10059
  }
9298
10060
  return true;
@@ -9316,9 +10078,9 @@ function registerCheckCommand(program2) {
9316
10078
  } catch (err2) {
9317
10079
  const tempConfig = getConfig();
9318
10080
  if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
9319
- const logPath = path22.join(os16.homedir(), ".node9", "hook-debug.log");
10081
+ const logPath = path24.join(os18.homedir(), ".node9", "hook-debug.log");
9320
10082
  const errMsg = err2 instanceof Error ? err2.message : String(err2);
9321
- fs20.appendFileSync(
10083
+ fs22.appendFileSync(
9322
10084
  logPath,
9323
10085
  `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
9324
10086
  RAW: ${raw}
@@ -9331,11 +10093,11 @@ RAW: ${raw}
9331
10093
  if (config.settings.autoStartDaemon && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON) {
9332
10094
  try {
9333
10095
  const scriptPath = process.argv[1];
9334
- if (typeof scriptPath !== "string" || !path22.isAbsolute(scriptPath))
10096
+ if (typeof scriptPath !== "string" || !path24.isAbsolute(scriptPath))
9335
10097
  throw new Error("node9: argv[1] is not an absolute path");
9336
- const resolvedScript = fs20.realpathSync(scriptPath);
9337
- const packageDist = fs20.realpathSync(path22.resolve(__dirname, "../.."));
9338
- if (!resolvedScript.startsWith(packageDist + path22.sep) && resolvedScript !== packageDist)
10098
+ const resolvedScript = fs22.realpathSync(scriptPath);
10099
+ const packageDist = fs22.realpathSync(path24.resolve(__dirname, "../.."));
10100
+ if (!resolvedScript.startsWith(packageDist + path24.sep) && resolvedScript !== packageDist)
9339
10101
  throw new Error(
9340
10102
  `node9: daemon spawn aborted \u2014 argv[1] (${resolvedScript}) is outside package dist (${packageDist})`
9341
10103
  );
@@ -9357,10 +10119,10 @@ RAW: ${raw}
9357
10119
  });
9358
10120
  d.unref();
9359
10121
  } catch (spawnErr) {
9360
- const logPath = path22.join(os16.homedir(), ".node9", "hook-debug.log");
10122
+ const logPath = path24.join(os18.homedir(), ".node9", "hook-debug.log");
9361
10123
  const msg = spawnErr instanceof Error ? spawnErr.message : String(spawnErr);
9362
10124
  try {
9363
- fs20.appendFileSync(
10125
+ fs22.appendFileSync(
9364
10126
  logPath,
9365
10127
  `[${(/* @__PURE__ */ new Date()).toISOString()}] daemon-autostart-failed: ${msg}
9366
10128
  `
@@ -9370,10 +10132,10 @@ RAW: ${raw}
9370
10132
  }
9371
10133
  }
9372
10134
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
9373
- const logPath = path22.join(os16.homedir(), ".node9", "hook-debug.log");
9374
- if (!fs20.existsSync(path22.dirname(logPath)))
9375
- fs20.mkdirSync(path22.dirname(logPath), { recursive: true });
9376
- 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}
9377
10139
  `);
9378
10140
  }
9379
10141
  const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
@@ -9386,8 +10148,8 @@ RAW: ${raw}
9386
10148
  const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
9387
10149
  let ttyFd = null;
9388
10150
  try {
9389
- ttyFd = fs20.openSync("/dev/tty", "w");
9390
- const writeTty = (line) => fs20.writeSync(ttyFd, line + "\n");
10151
+ ttyFd = fs22.openSync("/dev/tty", "w");
10152
+ const writeTty = (line) => fs22.writeSync(ttyFd, line + "\n");
9391
10153
  if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
9392
10154
  writeTty(chalk5.bgRed.white.bold(`
9393
10155
  \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
@@ -9406,7 +10168,7 @@ RAW: ${raw}
9406
10168
  } finally {
9407
10169
  if (ttyFd !== null)
9408
10170
  try {
9409
- fs20.closeSync(ttyFd);
10171
+ fs22.closeSync(ttyFd);
9410
10172
  } catch {
9411
10173
  }
9412
10174
  }
@@ -9438,7 +10200,7 @@ RAW: ${raw}
9438
10200
  if (shouldSnapshot(toolName, toolInput, config)) {
9439
10201
  await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
9440
10202
  }
9441
- 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;
9442
10204
  const result = await authorizeHeadless(toolName, toolInput, meta, {
9443
10205
  cwd: safeCwdForAuth
9444
10206
  });
@@ -9450,12 +10212,12 @@ RAW: ${raw}
9450
10212
  }
9451
10213
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
9452
10214
  try {
9453
- const tty = fs20.openSync("/dev/tty", "w");
9454
- fs20.writeSync(
10215
+ const tty = fs22.openSync("/dev/tty", "w");
10216
+ fs22.writeSync(
9455
10217
  tty,
9456
10218
  chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
9457
10219
  );
9458
- fs20.closeSync(tty);
10220
+ fs22.closeSync(tty);
9459
10221
  } catch {
9460
10222
  }
9461
10223
  const daemonReady = await autoStartDaemonAndWait();
@@ -9482,9 +10244,9 @@ RAW: ${raw}
9482
10244
  });
9483
10245
  } catch (err2) {
9484
10246
  if (process.env.NODE9_DEBUG === "1") {
9485
- const logPath = path22.join(os16.homedir(), ".node9", "hook-debug.log");
10247
+ const logPath = path24.join(os18.homedir(), ".node9", "hook-debug.log");
9486
10248
  const errMsg = err2 instanceof Error ? err2.message : String(err2);
9487
- fs20.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
10249
+ fs22.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
9488
10250
  `);
9489
10251
  }
9490
10252
  process.exit(0);
@@ -9521,9 +10283,9 @@ RAW: ${raw}
9521
10283
  init_audit();
9522
10284
  init_config();
9523
10285
  init_policy();
9524
- import fs21 from "fs";
9525
- import path23 from "path";
9526
- import os17 from "os";
10286
+ import fs23 from "fs";
10287
+ import path25 from "path";
10288
+ import os19 from "os";
9527
10289
  init_daemon();
9528
10290
 
9529
10291
  // src/utils/cp-mv-parser.ts
@@ -9596,10 +10358,10 @@ function registerLogCommand(program2) {
9596
10358
  decision: "allowed",
9597
10359
  source: "post-hook"
9598
10360
  };
9599
- const logPath = path23.join(os17.homedir(), ".node9", "audit.log");
9600
- if (!fs21.existsSync(path23.dirname(logPath)))
9601
- fs21.mkdirSync(path23.dirname(logPath), { recursive: true });
9602
- 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");
9603
10365
  if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
9604
10366
  const command = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
9605
10367
  if (command) {
@@ -9632,7 +10394,7 @@ function registerLogCommand(program2) {
9632
10394
  }
9633
10395
  }
9634
10396
  }
9635
- 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;
9636
10398
  const config = getConfig(safeCwd);
9637
10399
  if (shouldSnapshot(tool, {}, config)) {
9638
10400
  await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
@@ -9641,9 +10403,9 @@ function registerLogCommand(program2) {
9641
10403
  const msg = err2 instanceof Error ? err2.message : String(err2);
9642
10404
  process.stderr.write(`[Node9] audit log error: ${msg}
9643
10405
  `);
9644
- const debugPath = path23.join(os17.homedir(), ".node9", "hook-debug.log");
10406
+ const debugPath = path25.join(os19.homedir(), ".node9", "hook-debug.log");
9645
10407
  try {
9646
- fs21.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
10408
+ fs23.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
9647
10409
  `);
9648
10410
  } catch {
9649
10411
  }
@@ -9673,10 +10435,10 @@ init_config();
9673
10435
  import chalk6 from "chalk";
9674
10436
 
9675
10437
  // src/utils/https-fetch.ts
9676
- import https from "https";
10438
+ import https2 from "https";
9677
10439
  function httpsFetch(url) {
9678
10440
  return new Promise((resolve, reject) => {
9679
- https.get(url, (res) => {
10441
+ https2.get(url, (res) => {
9680
10442
  if (res.statusCode !== 200) {
9681
10443
  reject(new Error(`HTTP ${String(res.statusCode)} for ${url}`));
9682
10444
  res.resume();
@@ -10044,13 +10806,13 @@ function registerConfigShowCommand(program2) {
10044
10806
  // src/cli/commands/doctor.ts
10045
10807
  init_daemon();
10046
10808
  import chalk7 from "chalk";
10047
- import fs22 from "fs";
10048
- import path24 from "path";
10049
- import os18 from "os";
10809
+ import fs24 from "fs";
10810
+ import path26 from "path";
10811
+ import os20 from "os";
10050
10812
  import { execSync as execSync2 } from "child_process";
10051
10813
  function registerDoctorCommand(program2, version2) {
10052
10814
  program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
10053
- const homeDir2 = os18.homedir();
10815
+ const homeDir2 = os20.homedir();
10054
10816
  let failures = 0;
10055
10817
  function pass(msg) {
10056
10818
  console.log(chalk7.green(" \u2705 ") + msg);
@@ -10099,10 +10861,10 @@ function registerDoctorCommand(program2, version2) {
10099
10861
  );
10100
10862
  }
10101
10863
  section("Configuration");
10102
- const globalConfigPath = path24.join(homeDir2, ".node9", "config.json");
10103
- if (fs22.existsSync(globalConfigPath)) {
10864
+ const globalConfigPath = path26.join(homeDir2, ".node9", "config.json");
10865
+ if (fs24.existsSync(globalConfigPath)) {
10104
10866
  try {
10105
- JSON.parse(fs22.readFileSync(globalConfigPath, "utf-8"));
10867
+ JSON.parse(fs24.readFileSync(globalConfigPath, "utf-8"));
10106
10868
  pass("~/.node9/config.json found and valid");
10107
10869
  } catch {
10108
10870
  fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
@@ -10110,10 +10872,10 @@ function registerDoctorCommand(program2, version2) {
10110
10872
  } else {
10111
10873
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
10112
10874
  }
10113
- const projectConfigPath = path24.join(process.cwd(), "node9.config.json");
10114
- if (fs22.existsSync(projectConfigPath)) {
10875
+ const projectConfigPath = path26.join(process.cwd(), "node9.config.json");
10876
+ if (fs24.existsSync(projectConfigPath)) {
10115
10877
  try {
10116
- JSON.parse(fs22.readFileSync(projectConfigPath, "utf-8"));
10878
+ JSON.parse(fs24.readFileSync(projectConfigPath, "utf-8"));
10117
10879
  pass("node9.config.json found and valid (project)");
10118
10880
  } catch {
10119
10881
  fail(
@@ -10122,8 +10884,8 @@ function registerDoctorCommand(program2, version2) {
10122
10884
  );
10123
10885
  }
10124
10886
  }
10125
- const credsPath = path24.join(homeDir2, ".node9", "credentials.json");
10126
- if (fs22.existsSync(credsPath)) {
10887
+ const credsPath = path26.join(homeDir2, ".node9", "credentials.json");
10888
+ if (fs24.existsSync(credsPath)) {
10127
10889
  pass("Cloud credentials found (~/.node9/credentials.json)");
10128
10890
  } else {
10129
10891
  warn(
@@ -10132,10 +10894,10 @@ function registerDoctorCommand(program2, version2) {
10132
10894
  );
10133
10895
  }
10134
10896
  section("Agent Hooks");
10135
- const claudeSettingsPath = path24.join(homeDir2, ".claude", "settings.json");
10136
- if (fs22.existsSync(claudeSettingsPath)) {
10897
+ const claudeSettingsPath = path26.join(homeDir2, ".claude", "settings.json");
10898
+ if (fs24.existsSync(claudeSettingsPath)) {
10137
10899
  try {
10138
- const cs = JSON.parse(fs22.readFileSync(claudeSettingsPath, "utf-8"));
10900
+ const cs = JSON.parse(fs24.readFileSync(claudeSettingsPath, "utf-8"));
10139
10901
  const hasHook = cs.hooks?.PreToolUse?.some(
10140
10902
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
10141
10903
  );
@@ -10151,10 +10913,10 @@ function registerDoctorCommand(program2, version2) {
10151
10913
  } else {
10152
10914
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
10153
10915
  }
10154
- const geminiSettingsPath = path24.join(homeDir2, ".gemini", "settings.json");
10155
- if (fs22.existsSync(geminiSettingsPath)) {
10916
+ const geminiSettingsPath = path26.join(homeDir2, ".gemini", "settings.json");
10917
+ if (fs24.existsSync(geminiSettingsPath)) {
10156
10918
  try {
10157
- const gs = JSON.parse(fs22.readFileSync(geminiSettingsPath, "utf-8"));
10919
+ const gs = JSON.parse(fs24.readFileSync(geminiSettingsPath, "utf-8"));
10158
10920
  const hasHook = gs.hooks?.BeforeTool?.some(
10159
10921
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
10160
10922
  );
@@ -10170,10 +10932,10 @@ function registerDoctorCommand(program2, version2) {
10170
10932
  } else {
10171
10933
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
10172
10934
  }
10173
- const cursorHooksPath = path24.join(homeDir2, ".cursor", "hooks.json");
10174
- if (fs22.existsSync(cursorHooksPath)) {
10935
+ const cursorHooksPath = path26.join(homeDir2, ".cursor", "hooks.json");
10936
+ if (fs24.existsSync(cursorHooksPath)) {
10175
10937
  try {
10176
- const cur = JSON.parse(fs22.readFileSync(cursorHooksPath, "utf-8"));
10938
+ const cur = JSON.parse(fs24.readFileSync(cursorHooksPath, "utf-8"));
10177
10939
  const hasHook = cur.hooks?.preToolUse?.some(
10178
10940
  (h) => h.command?.includes("node9") || h.command?.includes("cli.js")
10179
10941
  );
@@ -10211,9 +10973,9 @@ function registerDoctorCommand(program2, version2) {
10211
10973
 
10212
10974
  // src/cli/commands/audit.ts
10213
10975
  import chalk8 from "chalk";
10214
- import fs23 from "fs";
10215
- import path25 from "path";
10216
- import os19 from "os";
10976
+ import fs25 from "fs";
10977
+ import path27 from "path";
10978
+ import os21 from "os";
10217
10979
  function formatRelativeTime(timestamp) {
10218
10980
  const diff = Date.now() - new Date(timestamp).getTime();
10219
10981
  const sec = Math.floor(diff / 1e3);
@@ -10226,14 +10988,14 @@ function formatRelativeTime(timestamp) {
10226
10988
  }
10227
10989
  function registerAuditCommand(program2) {
10228
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) => {
10229
- const logPath = path25.join(os19.homedir(), ".node9", "audit.log");
10230
- if (!fs23.existsSync(logPath)) {
10991
+ const logPath = path27.join(os21.homedir(), ".node9", "audit.log");
10992
+ if (!fs25.existsSync(logPath)) {
10231
10993
  console.log(
10232
10994
  chalk8.yellow("No audit logs found. Run node9 with an agent to generate entries.")
10233
10995
  );
10234
10996
  return;
10235
10997
  }
10236
- const raw = fs23.readFileSync(logPath, "utf-8");
10998
+ const raw = fs25.readFileSync(logPath, "utf-8");
10237
10999
  const lines = raw.split("\n").filter((l) => l.trim() !== "");
10238
11000
  let entries = lines.flatMap((line) => {
10239
11001
  try {
@@ -10287,9 +11049,9 @@ function registerAuditCommand(program2) {
10287
11049
 
10288
11050
  // src/cli/commands/report.ts
10289
11051
  import chalk9 from "chalk";
10290
- import fs24 from "fs";
10291
- import path26 from "path";
10292
- import os20 from "os";
11052
+ import fs26 from "fs";
11053
+ import path28 from "path";
11054
+ import os22 from "os";
10293
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;
10294
11056
  function buildTestTimestamps(allEntries) {
10295
11057
  const testTs = /* @__PURE__ */ new Set();
@@ -10336,8 +11098,8 @@ function getDateRange(period) {
10336
11098
  }
10337
11099
  }
10338
11100
  function parseAuditLog(logPath) {
10339
- if (!fs24.existsSync(logPath)) return [];
10340
- const raw = fs24.readFileSync(logPath, "utf-8");
11101
+ if (!fs26.existsSync(logPath)) return [];
11102
+ const raw = fs26.readFileSync(logPath, "utf-8");
10341
11103
  return raw.split("\n").flatMap((line) => {
10342
11104
  if (!line.trim()) return [];
10343
11105
  try {
@@ -10363,9 +11125,9 @@ function colorBar(value, max, width) {
10363
11125
  const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
10364
11126
  return chalk9.cyan(s.slice(0, filled)) + chalk9.dim(s.slice(filled));
10365
11127
  }
10366
- function pct(num2, total) {
11128
+ function pct(num3, total) {
10367
11129
  if (total === 0) return "\u2013";
10368
- return Math.round(num2 / total * 100) + "%";
11130
+ return Math.round(num3 / total * 100) + "%";
10369
11131
  }
10370
11132
  function fmtDate(d) {
10371
11133
  const date = typeof d === "string" ? /* @__PURE__ */ new Date(d + "T12:00:00") : d;
@@ -10406,11 +11168,11 @@ function loadClaudeCost(start, end) {
10406
11168
  inputTokens: 0,
10407
11169
  cacheReadTokens: 0
10408
11170
  };
10409
- const projectsDir = path26.join(os20.homedir(), ".claude", "projects");
10410
- if (!fs24.existsSync(projectsDir)) return empty;
11171
+ const projectsDir = path28.join(os22.homedir(), ".claude", "projects");
11172
+ if (!fs26.existsSync(projectsDir)) return empty;
10411
11173
  let dirs;
10412
11174
  try {
10413
- dirs = fs24.readdirSync(projectsDir);
11175
+ dirs = fs26.readdirSync(projectsDir);
10414
11176
  } catch {
10415
11177
  return empty;
10416
11178
  }
@@ -10420,18 +11182,18 @@ function loadClaudeCost(start, end) {
10420
11182
  const byDay = /* @__PURE__ */ new Map();
10421
11183
  const byModel = /* @__PURE__ */ new Map();
10422
11184
  for (const proj of dirs) {
10423
- const projPath = path26.join(projectsDir, proj);
11185
+ const projPath = path28.join(projectsDir, proj);
10424
11186
  let files;
10425
11187
  try {
10426
- const stat = fs24.statSync(projPath);
11188
+ const stat = fs26.statSync(projPath);
10427
11189
  if (!stat.isDirectory()) continue;
10428
- 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-"));
10429
11191
  } catch {
10430
11192
  continue;
10431
11193
  }
10432
11194
  for (const file of files) {
10433
11195
  try {
10434
- const raw = fs24.readFileSync(path26.join(projPath, file), "utf-8");
11196
+ const raw = fs26.readFileSync(path28.join(projPath, file), "utf-8");
10435
11197
  for (const line of raw.split("\n")) {
10436
11198
  if (!line.trim()) continue;
10437
11199
  let entry;
@@ -10474,7 +11236,7 @@ function registerReportCommand(program2) {
10474
11236
  const period = ["today", "7d", "30d", "month"].includes(
10475
11237
  options.period
10476
11238
  ) ? options.period : "7d";
10477
- const logPath = path26.join(os20.homedir(), ".node9", "audit.log");
11239
+ const logPath = path28.join(os22.homedir(), ".node9", "audit.log");
10478
11240
  const allEntries = parseAuditLog(logPath);
10479
11241
  if (allEntries.length === 0) {
10480
11242
  console.log(
@@ -10725,19 +11487,60 @@ init_daemon2();
10725
11487
  init_daemon();
10726
11488
  import chalk10 from "chalk";
10727
11489
  import { spawn as spawn7 } from "child_process";
11490
+ var VALID_ACTIONS = "start | stop | restart | status | install | uninstall";
10728
11491
  function registerDaemonCommand(program2) {
10729
- 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(
10730
11493
  "-w, --watch",
10731
11494
  "Start daemon + open browser, stay alive permanently (Flight Recorder mode)"
10732
11495
  ).action(
10733
11496
  async (action, options) => {
10734
11497
  const cmd = (action ?? "start").toLowerCase();
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);
11511
+ }
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
+ }
10735
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
+ }
10736
11541
  if (cmd === "status") return daemonStatus();
10737
11542
  if (cmd !== "start" && action !== void 0) {
10738
- console.error(
10739
- chalk10.red(`Unknown daemon action: "${action}". Use: start | stop | status`)
10740
- );
11543
+ console.error(chalk10.red(`Unknown daemon action: "${action}". Use: ${VALID_ACTIONS}`));
10741
11544
  process.exit(1);
10742
11545
  }
10743
11546
  if (options.watch) {
@@ -10788,12 +11591,12 @@ function registerDaemonCommand(program2) {
10788
11591
  init_core();
10789
11592
  init_daemon();
10790
11593
  import chalk11 from "chalk";
10791
- import fs25 from "fs";
10792
- import path27 from "path";
10793
- import os21 from "os";
10794
- function readJson2(filePath) {
11594
+ import fs27 from "fs";
11595
+ import path29 from "path";
11596
+ import os23 from "os";
11597
+ function readJson2(filePath) {
10795
11598
  try {
10796
- 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"));
10797
11600
  } catch {
10798
11601
  }
10799
11602
  return null;
@@ -10858,28 +11661,28 @@ function registerStatusCommand(program2) {
10858
11661
  console.log("");
10859
11662
  const modeLabel = settings.mode === "audit" ? chalk11.blue("audit") : settings.mode === "strict" ? chalk11.red("strict") : chalk11.white("standard");
10860
11663
  console.log(` Mode: ${modeLabel}`);
10861
- const projectConfig = path27.join(process.cwd(), "node9.config.json");
10862
- 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");
10863
11666
  console.log(
10864
- ` 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")}`
10865
11668
  );
10866
11669
  console.log(
10867
- ` 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")}`
10868
11671
  );
10869
11672
  if (mergedConfig.policy.sandboxPaths.length > 0) {
10870
11673
  console.log(
10871
11674
  ` Sandbox: ${chalk11.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
10872
11675
  );
10873
11676
  }
10874
- const homeDir2 = os21.homedir();
11677
+ const homeDir2 = os23.homedir();
10875
11678
  const claudeSettings = readJson2(
10876
- path27.join(homeDir2, ".claude", "settings.json")
11679
+ path29.join(homeDir2, ".claude", "settings.json")
10877
11680
  );
10878
- const claudeConfig = readJson2(path27.join(homeDir2, ".claude.json"));
11681
+ const claudeConfig = readJson2(path29.join(homeDir2, ".claude.json"));
10879
11682
  const geminiSettings = readJson2(
10880
- path27.join(homeDir2, ".gemini", "settings.json")
11683
+ path29.join(homeDir2, ".gemini", "settings.json")
10881
11684
  );
10882
- const cursorConfig = readJson2(path27.join(homeDir2, ".cursor", "mcp.json"));
11685
+ const cursorConfig = readJson2(path29.join(homeDir2, ".cursor", "mcp.json"));
10883
11686
  const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
10884
11687
  if (agentFound) {
10885
11688
  console.log("");
@@ -10939,11 +11742,12 @@ function registerStatusCommand(program2) {
10939
11742
  // src/cli/commands/init.ts
10940
11743
  init_core();
10941
11744
  import chalk12 from "chalk";
10942
- import fs26 from "fs";
10943
- import path28 from "path";
10944
- import os22 from "os";
10945
- import https2 from "https";
11745
+ import fs28 from "fs";
11746
+ import path30 from "path";
11747
+ import os24 from "os";
11748
+ import https3 from "https";
10946
11749
  init_shields();
11750
+ init_service();
10947
11751
  var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
10948
11752
  function fireTelemetryPing(agents) {
10949
11753
  try {
@@ -10953,7 +11757,7 @@ function fireTelemetryPing(agents) {
10953
11757
  os: process.platform,
10954
11758
  node9_version: process.env.npm_package_version ?? "unknown"
10955
11759
  });
10956
- const req = https2.request(
11760
+ const req = https3.request(
10957
11761
  {
10958
11762
  hostname: "api.node9.ai",
10959
11763
  path: "/api/v1/telemetry",
@@ -11000,15 +11804,15 @@ function registerInitCommand(program2) {
11000
11804
  }
11001
11805
  console.log("");
11002
11806
  }
11003
- const configPath = path28.join(os22.homedir(), ".node9", "config.json");
11004
- if (fs26.existsSync(configPath) && !options.force) {
11807
+ const configPath = path30.join(os24.homedir(), ".node9", "config.json");
11808
+ if (fs28.existsSync(configPath) && !options.force) {
11005
11809
  try {
11006
- const existing = JSON.parse(fs26.readFileSync(configPath, "utf-8"));
11810
+ const existing = JSON.parse(fs28.readFileSync(configPath, "utf-8"));
11007
11811
  const settings = existing.settings ?? {};
11008
11812
  if (settings.mode !== chosenMode) {
11009
11813
  settings.mode = chosenMode;
11010
11814
  existing.settings = settings;
11011
- fs26.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
11815
+ fs28.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
11012
11816
  console.log(chalk12.green(`\u2705 Mode updated: ${chosenMode}`));
11013
11817
  } else {
11014
11818
  console.log(chalk12.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
@@ -11021,9 +11825,9 @@ function registerInitCommand(program2) {
11021
11825
  ...DEFAULT_CONFIG,
11022
11826
  settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
11023
11827
  };
11024
- const dir = path28.dirname(configPath);
11025
- if (!fs26.existsSync(dir)) fs26.mkdirSync(dir, { recursive: true });
11026
- 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");
11027
11831
  console.log(chalk12.green(`\u2705 Config created: ${configPath}`));
11028
11832
  console.log(chalk12.gray(` Mode: ${chosenMode}`));
11029
11833
  }
@@ -11035,9 +11839,13 @@ function registerInitCommand(program2) {
11035
11839
  );
11036
11840
  if (found.length === 0) {
11037
11841
  console.log(
11038
- 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>")
11039
11848
  );
11040
- console.log(chalk12.gray("then run: node9 addto <claude|gemini|cursor|codex>"));
11041
11849
  return;
11042
11850
  }
11043
11851
  console.log(chalk12.bold("Detected agents:"));
@@ -11051,6 +11859,32 @@ function registerInitCommand(program2) {
11051
11859
  else if (agent === "gemini") await setupGemini();
11052
11860
  else if (agent === "cursor") await setupCursor();
11053
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
+ }
11054
11888
  console.log("");
11055
11889
  }
11056
11890
  {
@@ -11077,7 +11911,7 @@ function registerInitCommand(program2) {
11077
11911
  }
11078
11912
 
11079
11913
  // src/cli/commands/undo.ts
11080
- import path29 from "path";
11914
+ import path31 from "path";
11081
11915
  import chalk14 from "chalk";
11082
11916
 
11083
11917
  // src/tui/undo-navigator.ts
@@ -11236,7 +12070,7 @@ function findMatchingCwd(startDir, history) {
11236
12070
  let dir = startDir;
11237
12071
  while (true) {
11238
12072
  if (cwds.has(dir)) return dir;
11239
- const parent = path29.dirname(dir);
12073
+ const parent = path31.dirname(dir);
11240
12074
  if (parent === dir) return null;
11241
12075
  dir = parent;
11242
12076
  }
@@ -11365,7 +12199,7 @@ function registerUndoCommand(program2) {
11365
12199
  // src/cli/commands/watch.ts
11366
12200
  init_daemon();
11367
12201
  import chalk15 from "chalk";
11368
- import { spawn as spawn8, spawnSync as spawnSync5 } from "child_process";
12202
+ import { spawn as spawn8, spawnSync as spawnSync6 } from "child_process";
11369
12203
  function registerWatchCommand(program2) {
11370
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) => {
11371
12205
  let port = DAEMON_PORT;
@@ -11411,7 +12245,7 @@ function registerWatchCommand(program2) {
11411
12245
  "\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
11412
12246
  )
11413
12247
  );
11414
- const result = spawnSync5(cmd, args, {
12248
+ const result = spawnSync6(cmd, args, {
11415
12249
  stdio: "inherit",
11416
12250
  env: { ...process.env, NODE9_WATCH_MODE: "1" }
11417
12251
  });
@@ -11432,12 +12266,12 @@ import { execa as execa2 } from "execa";
11432
12266
  init_provenance();
11433
12267
 
11434
12268
  // src/mcp-pin.ts
11435
- import fs27 from "fs";
11436
- import path30 from "path";
11437
- import os23 from "os";
12269
+ import fs29 from "fs";
12270
+ import path32 from "path";
12271
+ import os25 from "os";
11438
12272
  import crypto4 from "crypto";
11439
12273
  function getPinsFilePath() {
11440
- return path30.join(os23.homedir(), ".node9", "mcp-pins.json");
12274
+ return path32.join(os25.homedir(), ".node9", "mcp-pins.json");
11441
12275
  }
11442
12276
  function hashToolDefinitions(tools) {
11443
12277
  const sorted = [...tools].sort((a, b) => {
@@ -11454,7 +12288,7 @@ function getServerKey(upstreamCommand) {
11454
12288
  function readMcpPinsSafe() {
11455
12289
  const filePath = getPinsFilePath();
11456
12290
  try {
11457
- const raw = fs27.readFileSync(filePath, "utf-8");
12291
+ const raw = fs29.readFileSync(filePath, "utf-8");
11458
12292
  if (!raw.trim()) {
11459
12293
  return { ok: false, reason: "corrupt", detail: "empty file" };
11460
12294
  }
@@ -11478,10 +12312,10 @@ function readMcpPins() {
11478
12312
  }
11479
12313
  function writeMcpPins(data) {
11480
12314
  const filePath = getPinsFilePath();
11481
- fs27.mkdirSync(path30.dirname(filePath), { recursive: true });
12315
+ fs29.mkdirSync(path32.dirname(filePath), { recursive: true });
11482
12316
  const tmp = `${filePath}.${crypto4.randomBytes(6).toString("hex")}.tmp`;
11483
- fs27.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
11484
- fs27.renameSync(tmp, filePath);
12317
+ fs29.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
12318
+ fs29.renameSync(tmp, filePath);
11485
12319
  }
11486
12320
  function checkPin(serverKey, currentHash) {
11487
12321
  const result = readMcpPinsSafe();
@@ -11853,9 +12687,9 @@ function registerMcpGatewayCommand(program2) {
11853
12687
 
11854
12688
  // src/mcp-server/index.ts
11855
12689
  import readline4 from "readline";
11856
- import fs28 from "fs";
11857
- import os24 from "os";
11858
- import path31 from "path";
12690
+ import fs30 from "fs";
12691
+ import os26 from "os";
12692
+ import path33 from "path";
11859
12693
  init_core();
11860
12694
  init_daemon();
11861
12695
  init_shields();
@@ -12030,13 +12864,13 @@ function handleStatus() {
12030
12864
  lines.push(`Active shields: ${activeShields.length > 0 ? activeShields.join(", ") : "none"}`);
12031
12865
  lines.push(`Smart rules: ${config.policy.smartRules.length} loaded`);
12032
12866
  lines.push(`DLP: ${config.policy.dlp?.enabled !== false ? "enabled" : "disabled"}`);
12033
- const projectConfig = path31.join(process.cwd(), "node9.config.json");
12034
- 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");
12035
12869
  lines.push(
12036
- `Project config (node9.config.json): ${fs28.existsSync(projectConfig) ? "present" : "not found"}`
12870
+ `Project config (node9.config.json): ${fs30.existsSync(projectConfig) ? "present" : "not found"}`
12037
12871
  );
12038
12872
  lines.push(
12039
- `Global config (~/.node9/config.json): ${fs28.existsSync(globalConfig) ? "present" : "not found"}`
12873
+ `Global config (~/.node9/config.json): ${fs30.existsSync(globalConfig) ? "present" : "not found"}`
12040
12874
  );
12041
12875
  return lines.join("\n");
12042
12876
  }
@@ -12110,21 +12944,21 @@ function handleShieldDisable(args) {
12110
12944
  writeActiveShields(active.filter((s) => s !== name));
12111
12945
  return `Shield "${name}" disabled.`;
12112
12946
  }
12113
- var GLOBAL_CONFIG_PATH2 = path31.join(os24.homedir(), ".node9", "config.json");
12947
+ var GLOBAL_CONFIG_PATH2 = path33.join(os26.homedir(), ".node9", "config.json");
12114
12948
  var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
12115
12949
  function readGlobalConfigRaw() {
12116
12950
  try {
12117
- if (fs28.existsSync(GLOBAL_CONFIG_PATH2)) {
12118
- 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"));
12119
12953
  }
12120
12954
  } catch {
12121
12955
  }
12122
12956
  return {};
12123
12957
  }
12124
12958
  function writeGlobalConfigRaw(data) {
12125
- const dir = path31.dirname(GLOBAL_CONFIG_PATH2);
12126
- if (!fs28.existsSync(dir)) fs28.mkdirSync(dir, { recursive: true });
12127
- 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");
12128
12962
  }
12129
12963
  function handleApproverList() {
12130
12964
  const config = getConfig();
@@ -12167,9 +13001,9 @@ function handleApproverSet(args) {
12167
13001
  }
12168
13002
  function handleAuditGet(args) {
12169
13003
  const limit = Math.min(typeof args.limit === "number" ? args.limit : 20, 100);
12170
- const auditPath = path31.join(os24.homedir(), ".node9", "audit.log");
12171
- if (!fs28.existsSync(auditPath)) return "No audit log found.";
12172
- 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);
12173
13007
  const recent = lines.slice(-limit);
12174
13008
  const entries = recent.map((line) => {
12175
13009
  try {
@@ -12487,25 +13321,950 @@ function registerMcpPinCommand(program2) {
12487
13321
  });
12488
13322
  }
12489
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
+
12490
14249
  // src/cli.ts
12491
14250
  var { version } = JSON.parse(
12492
- fs31.readFileSync(path34.join(__dirname, "../package.json"), "utf-8")
14251
+ fs35.readFileSync(path38.join(__dirname, "../package.json"), "utf-8")
12493
14252
  );
12494
14253
  var program = new Command();
12495
14254
  program.name("node9").description("The Sudo Command for AI Agents").version(version);
12496
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) => {
12497
- const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
12498
- const credPath = path34.join(os27.homedir(), ".node9", "credentials.json");
12499
- if (!fs31.existsSync(path34.dirname(credPath)))
12500
- 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 });
12501
14260
  const profileName = options.profile || "default";
12502
14261
  let existingCreds = {};
12503
14262
  try {
12504
- if (fs31.existsSync(credPath)) {
12505
- const raw = JSON.parse(fs31.readFileSync(credPath, "utf-8"));
14263
+ if (fs35.existsSync(credPath)) {
14264
+ const raw = JSON.parse(fs35.readFileSync(credPath, "utf-8"));
12506
14265
  if (raw.apiKey) {
12507
14266
  existingCreds = {
12508
- default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
14267
+ default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL2 }
12509
14268
  };
12510
14269
  } else {
12511
14270
  existingCreds = raw;
@@ -12513,14 +14272,14 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
12513
14272
  }
12514
14273
  } catch {
12515
14274
  }
12516
- existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
12517
- 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 });
12518
14277
  if (profileName === "default") {
12519
- const configPath = path34.join(os27.homedir(), ".node9", "config.json");
14278
+ const configPath = path38.join(os31.homedir(), ".node9", "config.json");
12520
14279
  let config = {};
12521
14280
  try {
12522
- if (fs31.existsSync(configPath))
12523
- config = JSON.parse(fs31.readFileSync(configPath, "utf-8"));
14281
+ if (fs35.existsSync(configPath))
14282
+ config = JSON.parse(fs35.readFileSync(configPath, "utf-8"));
12524
14283
  } catch {
12525
14284
  }
12526
14285
  if (!config.settings || typeof config.settings !== "object") config.settings = {};
@@ -12535,39 +14294,47 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
12535
14294
  approvers.cloud = false;
12536
14295
  }
12537
14296
  s.approvers = approvers;
12538
- if (!fs31.existsSync(path34.dirname(configPath)))
12539
- fs31.mkdirSync(path34.dirname(configPath), { recursive: true });
12540
- 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 });
12541
14300
  }
12542
14301
  if (options.profile && profileName !== "default") {
12543
- console.log(chalk20.green(`\u2705 Profile "${profileName}" saved`));
12544
- 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`));
12545
14304
  } else if (options.local) {
12546
- console.log(chalk20.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
12547
- 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.`));
12548
14307
  } else {
12549
- console.log(chalk20.green(`\u2705 Logged in \u2014 agent mode`));
12550
- 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.`));
12551
14310
  }
12552
14311
  });
12553
- 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) => {
12554
14313
  if (target === "gemini") return await setupGemini();
12555
14314
  if (target === "claude") return await setupClaude();
12556
14315
  if (target === "cursor") return await setupCursor();
14316
+ if (target === "windsurf") return await setupWindsurf();
14317
+ if (target === "vscode") return await setupVSCode();
12557
14318
  if (target === "hud") return setupHud();
12558
- 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
+ );
12559
14324
  process.exit(1);
12560
14325
  });
12561
- 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) => {
12562
14327
  if (!target) {
12563
- console.log(chalk20.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
12564
- 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");
12565
14330
  console.log(" Targets:");
12566
- console.log(" " + chalk20.green("claude") + " \u2014 Claude Code (hook mode)");
12567
- console.log(" " + chalk20.green("gemini") + " \u2014 Gemini CLI (hook mode)");
12568
- 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)");
12569
14336
  process.stdout.write(
12570
- " " + chalk20.green("hud") + " \u2014 Claude Code security statusline\n"
14337
+ " " + chalk24.green("hud") + " \u2014 Claude Code security statusline\n"
12571
14338
  );
12572
14339
  console.log("");
12573
14340
  return;
@@ -12576,93 +14343,108 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
12576
14343
  if (t === "gemini") return await setupGemini();
12577
14344
  if (t === "claude") return await setupClaude();
12578
14345
  if (t === "cursor") return await setupCursor();
14346
+ if (t === "windsurf") return await setupWindsurf();
14347
+ if (t === "vscode") return await setupVSCode();
12579
14348
  if (t === "hud") return setupHud();
12580
- 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
+ );
12581
14354
  process.exit(1);
12582
14355
  });
12583
- 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) => {
12584
14360
  let fn;
12585
14361
  if (target === "claude") fn = teardownClaude;
12586
14362
  else if (target === "gemini") fn = teardownGemini;
12587
14363
  else if (target === "cursor") fn = teardownCursor;
14364
+ else if (target === "windsurf") fn = teardownWindsurf;
14365
+ else if (target === "vscode") fn = teardownVSCode;
12588
14366
  else if (target === "hud") fn = teardownHud;
12589
14367
  else {
12590
14368
  console.error(
12591
- 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
+ )
12592
14372
  );
12593
14373
  process.exit(1);
12594
14374
  }
12595
- console.log(chalk20.cyan(`
14375
+ console.log(chalk24.cyan(`
12596
14376
  \u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
12597
14377
  `));
12598
14378
  try {
12599
14379
  fn();
12600
14380
  } catch (err2) {
12601
- 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)}`));
12602
14382
  process.exit(1);
12603
14383
  }
12604
- 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."));
12605
14385
  });
12606
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) => {
12607
- console.log(chalk20.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
12608
- 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..."));
12609
14389
  try {
12610
14390
  stopDaemon();
12611
- console.log(chalk20.green(" \u2705 Daemon stopped"));
14391
+ console.log(chalk24.green(" \u2705 Daemon stopped"));
12612
14392
  } catch {
12613
- console.log(chalk20.blue(" \u2139\uFE0F Daemon was not running"));
14393
+ console.log(chalk24.blue(" \u2139\uFE0F Daemon was not running"));
12614
14394
  }
12615
- console.log(chalk20.bold("\nRemoving hooks..."));
14395
+ console.log(chalk24.bold("\nRemoving hooks..."));
12616
14396
  let teardownFailed = false;
12617
14397
  for (const [label, fn] of [
12618
14398
  ["Claude", teardownClaude],
12619
14399
  ["Gemini", teardownGemini],
12620
- ["Cursor", teardownCursor]
14400
+ ["Cursor", teardownCursor],
14401
+ ["Windsurf", teardownWindsurf],
14402
+ ["VSCode", teardownVSCode]
12621
14403
  ]) {
12622
14404
  try {
12623
14405
  fn();
12624
14406
  } catch (err2) {
12625
14407
  teardownFailed = true;
12626
14408
  console.error(
12627
- chalk20.red(
14409
+ chalk24.red(
12628
14410
  ` \u26A0\uFE0F Failed to remove ${label} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
12629
14411
  )
12630
14412
  );
12631
14413
  }
12632
14414
  }
12633
14415
  if (options.purge) {
12634
- const node9Dir = path34.join(os27.homedir(), ".node9");
12635
- if (fs31.existsSync(node9Dir)) {
14416
+ const node9Dir = path38.join(os31.homedir(), ".node9");
14417
+ if (fs35.existsSync(node9Dir)) {
12636
14418
  const confirmed = await confirm2({
12637
14419
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
12638
14420
  default: false
12639
14421
  });
12640
14422
  if (confirmed) {
12641
- fs31.rmSync(node9Dir, { recursive: true });
12642
- if (fs31.existsSync(node9Dir)) {
14423
+ fs35.rmSync(node9Dir, { recursive: true });
14424
+ if (fs35.existsSync(node9Dir)) {
12643
14425
  console.error(
12644
- 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.")
12645
14427
  );
12646
14428
  } else {
12647
- 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)"));
12648
14430
  }
12649
14431
  } else {
12650
- console.log(chalk20.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
14432
+ console.log(chalk24.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
12651
14433
  }
12652
14434
  } else {
12653
- 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"));
12654
14436
  }
12655
14437
  } else {
12656
14438
  console.log(
12657
- 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")
12658
14440
  );
12659
14441
  }
12660
14442
  if (teardownFailed) {
12661
- 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."));
12662
14444
  process.exit(1);
12663
14445
  }
12664
- console.log(chalk20.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
12665
- 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"));
12666
14448
  });
12667
14449
  registerDoctorCommand(program, version);
12668
14450
  program.command("explain").description(
@@ -12675,7 +14457,7 @@ program.command("explain").description(
12675
14457
  try {
12676
14458
  args = JSON.parse(trimmed);
12677
14459
  } catch {
12678
- console.error(chalk20.red(`
14460
+ console.error(chalk24.red(`
12679
14461
  \u274C Invalid JSON: ${trimmed}
12680
14462
  `));
12681
14463
  process.exit(1);
@@ -12686,54 +14468,54 @@ program.command("explain").description(
12686
14468
  }
12687
14469
  const result = await explainPolicy(tool, args);
12688
14470
  console.log("");
12689
- console.log(chalk20.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
14471
+ console.log(chalk24.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
12690
14472
  console.log("");
12691
- console.log(` ${chalk20.bold("Tool:")} ${chalk20.white(result.tool)}`);
14473
+ console.log(` ${chalk24.bold("Tool:")} ${chalk24.white(result.tool)}`);
12692
14474
  if (argsRaw) {
12693
- const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
12694
- 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)}`);
12695
14477
  }
12696
14478
  console.log("");
12697
- console.log(chalk20.bold("Config Sources (Waterfall):"));
14479
+ console.log(chalk24.bold("Config Sources (Waterfall):"));
12698
14480
  for (const tier of result.waterfall) {
12699
- const num2 = chalk20.gray(` ${tier.tier}.`);
14481
+ const num3 = chalk24.gray(` ${tier.tier}.`);
12700
14482
  const label = tier.label.padEnd(16);
12701
14483
  let statusStr;
12702
14484
  if (tier.tier === 1) {
12703
- statusStr = chalk20.gray(tier.note ?? "");
14485
+ statusStr = chalk24.gray(tier.note ?? "");
12704
14486
  } else if (tier.status === "active") {
12705
- const loc = tier.path ? chalk20.gray(tier.path) : "";
12706
- const note = tier.note ? chalk20.gray(`(${tier.note})`) : "";
12707
- 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 : "");
12708
14490
  } else {
12709
- statusStr = chalk20.gray("\u25CB " + (tier.note ?? "not found"));
14491
+ statusStr = chalk24.gray("\u25CB " + (tier.note ?? "not found"));
12710
14492
  }
12711
- console.log(`${num2} ${chalk20.white(label)} ${statusStr}`);
14493
+ console.log(`${num3} ${chalk24.white(label)} ${statusStr}`);
12712
14494
  }
12713
14495
  console.log("");
12714
- console.log(chalk20.bold("Policy Evaluation:"));
14496
+ console.log(chalk24.bold("Policy Evaluation:"));
12715
14497
  for (const step of result.steps) {
12716
14498
  const isFinal = step.isFinal;
12717
14499
  let icon;
12718
- if (step.outcome === "allow") icon = chalk20.green(" \u2705");
12719
- else if (step.outcome === "review") icon = chalk20.red(" \u{1F534}");
12720
- else if (step.outcome === "skip") icon = chalk20.gray(" \u2500 ");
12721
- 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 ");
12722
14504
  const name = step.name.padEnd(18);
12723
- const nameStr = isFinal ? chalk20.white.bold(name) : chalk20.white(name);
12724
- const detail = isFinal ? chalk20.white(step.detail) : chalk20.gray(step.detail);
12725
- 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") : "";
12726
14508
  console.log(`${icon} ${nameStr} ${detail}${arrow}`);
12727
14509
  }
12728
14510
  console.log("");
12729
14511
  if (result.decision === "allow") {
12730
- 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"));
12731
14513
  } else {
12732
14514
  console.log(
12733
- 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")
12734
14516
  );
12735
14517
  if (result.blockedByLabel) {
12736
- console.log(chalk20.gray(` Reason: ${result.blockedByLabel}`));
14518
+ console.log(chalk24.gray(` Reason: ${result.blockedByLabel}`));
12737
14519
  }
12738
14520
  }
12739
14521
  console.log("");
@@ -12748,7 +14530,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
12748
14530
  try {
12749
14531
  await startTail2(options);
12750
14532
  } catch (err2) {
12751
- 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)}`));
12752
14534
  process.exit(1);
12753
14535
  }
12754
14536
  });
@@ -12780,14 +14562,14 @@ Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
12780
14562
  Run "node9 addto claude" to register it as the statusLine.`
12781
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) => {
12782
14564
  if (subcommand === "debug") {
12783
- const flagFile = path34.join(os27.homedir(), ".node9", "hud-debug");
14565
+ const flagFile = path38.join(os31.homedir(), ".node9", "hud-debug");
12784
14566
  if (state === "on") {
12785
- fs31.mkdirSync(path34.dirname(flagFile), { recursive: true });
12786
- fs31.writeFileSync(flagFile, "");
14567
+ fs35.mkdirSync(path38.dirname(flagFile), { recursive: true });
14568
+ fs35.writeFileSync(flagFile, "");
12787
14569
  console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
12788
14570
  console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
12789
14571
  } else if (state === "off") {
12790
- if (fs31.existsSync(flagFile)) fs31.unlinkSync(flagFile);
14572
+ if (fs35.existsSync(flagFile)) fs35.unlinkSync(flagFile);
12791
14573
  console.log("HUD debug logging disabled.");
12792
14574
  } else {
12793
14575
  console.error("Usage: node9 hud debug on|off");
@@ -12802,7 +14584,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
12802
14584
  const ms = parseDuration(options.duration);
12803
14585
  if (ms === null) {
12804
14586
  console.error(
12805
- chalk20.red(`
14587
+ chalk24.red(`
12806
14588
  \u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
12807
14589
  `)
12808
14590
  );
@@ -12810,20 +14592,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
12810
14592
  }
12811
14593
  pauseNode9(ms, options.duration);
12812
14594
  const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
12813
- console.log(chalk20.yellow(`
14595
+ console.log(chalk24.yellow(`
12814
14596
  \u23F8 Node9 paused until ${expiresAt}`));
12815
- console.log(chalk20.gray(` All tool calls will be allowed without review.`));
12816
- 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.
12817
14599
  `));
12818
14600
  });
12819
14601
  program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
12820
14602
  const { paused } = checkPause();
12821
14603
  if (!paused) {
12822
- 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"));
12823
14605
  return;
12824
14606
  }
12825
14607
  resumeNode9();
12826
- 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"));
12827
14609
  });
12828
14610
  var HOOK_BASED_AGENTS = {
12829
14611
  claude: "claude",
@@ -12836,15 +14618,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
12836
14618
  if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
12837
14619
  const target = HOOK_BASED_AGENTS[firstArg2];
12838
14620
  console.error(
12839
- chalk20.yellow(`
14621
+ chalk24.yellow(`
12840
14622
  \u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
12841
14623
  );
12842
- console.error(chalk20.white(`
14624
+ console.error(chalk24.white(`
12843
14625
  "${target}" uses its own hook system. Use:`));
12844
14626
  console.error(
12845
- chalk20.green(` node9 addto ${target} `) + chalk20.gray("# one-time setup")
14627
+ chalk24.green(` node9 addto ${target} `) + chalk24.gray("# one-time setup")
12846
14628
  );
12847
- console.error(chalk20.green(` ${target} `) + chalk20.gray("# run normally"));
14629
+ console.error(chalk24.green(` ${target} `) + chalk24.gray("# run normally"));
12848
14630
  process.exit(1);
12849
14631
  }
12850
14632
  const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
@@ -12861,7 +14643,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
12861
14643
  }
12862
14644
  );
12863
14645
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
12864
- 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..."));
12865
14647
  const daemonReady = await autoStartDaemonAndWait();
12866
14648
  if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
12867
14649
  }
@@ -12874,12 +14656,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
12874
14656
  }
12875
14657
  if (!result.approved) {
12876
14658
  console.error(
12877
- chalk20.red(`
14659
+ chalk24.red(`
12878
14660
  \u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
12879
14661
  );
12880
14662
  process.exit(1);
12881
14663
  }
12882
- console.error(chalk20.green("\n\u2705 Approved \u2014 running command...\n"));
14664
+ console.error(chalk24.green("\n\u2705 Approved \u2014 running command...\n"));
12883
14665
  await runProxy(fullCommand);
12884
14666
  } else {
12885
14667
  program.help();
@@ -12889,14 +14671,18 @@ registerUndoCommand(program);
12889
14671
  registerShieldCommand(program);
12890
14672
  registerConfigShowCommand(program);
12891
14673
  registerTrustCommand(program);
14674
+ registerSyncCommand(program);
14675
+ registerAgentsCommand(program);
14676
+ registerScanCommand(program);
14677
+ registerSessionsCommand(program);
12892
14678
  if (process.argv[2] !== "daemon") {
12893
14679
  process.on("unhandledRejection", (reason) => {
12894
14680
  const isCheckHook = process.argv[2] === "check";
12895
14681
  if (isCheckHook) {
12896
14682
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
12897
- const logPath = path34.join(os27.homedir(), ".node9", "hook-debug.log");
14683
+ const logPath = path38.join(os31.homedir(), ".node9", "hook-debug.log");
12898
14684
  const msg = reason instanceof Error ? reason.message : String(reason);
12899
- fs31.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
14685
+ fs35.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
12900
14686
  `);
12901
14687
  }
12902
14688
  process.exit(0);