@node9/proxy 1.10.2 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 path41 = issue.path.length > 0 ? issue.path.join(".") : "root";
151
+ return ` \u2022 ${path41}: ${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(),
@@ -253,6 +254,11 @@ var init_config_schema = __esm({
253
254
  enabled: z.boolean().optional(),
254
255
  threshold: z.number().min(2).optional(),
255
256
  windowSeconds: z.number().min(10).optional()
257
+ }).optional(),
258
+ skillPinning: z.object({
259
+ enabled: z.boolean().optional(),
260
+ mode: z.enum(["warn", "block"]).optional(),
261
+ roots: z.array(z.string()).optional()
256
262
  }).optional()
257
263
  }).optional(),
258
264
  environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
@@ -495,11 +501,11 @@ function getGlobalSettings() {
495
501
  };
496
502
  }
497
503
  function getCredentials() {
498
- const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
504
+ const DEFAULT_API_URL2 = "https://api.node9.ai/api/v1/intercept";
499
505
  if (process.env.NODE9_API_KEY) {
500
506
  return {
501
507
  apiKey: process.env.NODE9_API_KEY,
502
- apiUrl: process.env.NODE9_API_URL || DEFAULT_API_URL
508
+ apiUrl: process.env.NODE9_API_URL || DEFAULT_API_URL2
503
509
  };
504
510
  }
505
511
  try {
@@ -511,13 +517,13 @@ function getCredentials() {
511
517
  if (profile?.apiKey) {
512
518
  return {
513
519
  apiKey: profile.apiKey,
514
- apiUrl: profile.apiUrl || DEFAULT_API_URL
520
+ apiUrl: profile.apiUrl || DEFAULT_API_URL2
515
521
  };
516
522
  }
517
523
  if (creds.apiKey) {
518
524
  return {
519
525
  apiKey: creds.apiKey,
520
- apiUrl: creds.apiUrl || DEFAULT_API_URL
526
+ apiUrl: creds.apiUrl || DEFAULT_API_URL2
521
527
  };
522
528
  }
523
529
  }
@@ -551,7 +557,11 @@ function getConfig(cwd) {
551
557
  ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
552
558
  },
553
559
  dlp: { ...DEFAULT_CONFIG.policy.dlp },
554
- loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection }
560
+ loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection },
561
+ skillPinning: {
562
+ ...DEFAULT_CONFIG.policy.skillPinning,
563
+ roots: [...DEFAULT_CONFIG.policy.skillPinning.roots]
564
+ }
555
565
  };
556
566
  const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
557
567
  const applyLayer = (source) => {
@@ -568,6 +578,8 @@ function getConfig(cwd) {
568
578
  if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
569
579
  mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
570
580
  if (s.environment !== void 0) mergedSettings.environment = s.environment;
581
+ if (s.cloudSyncIntervalHours !== void 0)
582
+ mergedSettings.cloudSyncIntervalHours = s.cloudSyncIntervalHours;
571
583
  if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
572
584
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
573
585
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
@@ -577,7 +589,12 @@ function getConfig(cwd) {
577
589
  if (p.smartRules) {
578
590
  const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
579
591
  const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
580
- mergedPolicy.smartRules = [...defaultBlocks, ...p.smartRules, ...defaultNonBlocks];
592
+ const userRuleNames = new Set(p.smartRules.filter((r) => r.name).map((r) => r.name));
593
+ const filteredBlocks = defaultBlocks.filter((r) => !r.name || !userRuleNames.has(r.name));
594
+ const filteredNonBlocks = defaultNonBlocks.filter(
595
+ (r) => !r.name || !userRuleNames.has(r.name)
596
+ );
597
+ mergedPolicy.smartRules = [...filteredBlocks, ...p.smartRules, ...filteredNonBlocks];
581
598
  }
582
599
  if (p.snapshot) {
583
600
  const s2 = p.snapshot;
@@ -597,6 +614,16 @@ function getConfig(cwd) {
597
614
  if (ld.windowSeconds !== void 0)
598
615
  mergedPolicy.loopDetection.windowSeconds = ld.windowSeconds;
599
616
  }
617
+ if (p.skillPinning && typeof p.skillPinning === "object") {
618
+ const sp = p.skillPinning;
619
+ if (sp.enabled !== void 0) mergedPolicy.skillPinning.enabled = sp.enabled;
620
+ if (sp.mode !== void 0) mergedPolicy.skillPinning.mode = sp.mode;
621
+ if (Array.isArray(sp.roots)) {
622
+ for (const r of sp.roots) {
623
+ if (typeof r === "string" && r.length > 0) mergedPolicy.skillPinning.roots.push(r);
624
+ }
625
+ }
626
+ }
600
627
  const envs = source.environments || {};
601
628
  for (const [envName, envConfig] of Object.entries(envs)) {
602
629
  if (envConfig && typeof envConfig === "object") {
@@ -611,6 +638,16 @@ function getConfig(cwd) {
611
638
  };
612
639
  applyLayer(globalConfig);
613
640
  applyLayer(projectConfig);
641
+ {
642
+ const cacheFile = path3.join(os3.homedir(), ".node9", "rules-cache.json");
643
+ try {
644
+ const raw = JSON.parse(fs3.readFileSync(cacheFile, "utf-8"));
645
+ if (Array.isArray(raw.rules) && raw.rules.length > 0) {
646
+ applyLayer({ policy: { smartRules: raw.rules } });
647
+ }
648
+ } catch {
649
+ }
650
+ }
614
651
  const shieldOverrides = readShieldOverrides();
615
652
  for (const shieldName of readActiveShields()) {
616
653
  const shield = getShield(shieldName);
@@ -638,6 +675,7 @@ function getConfig(cwd) {
638
675
  mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
639
676
  mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
640
677
  mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
678
+ mergedPolicy.skillPinning.roots = [...new Set(mergedPolicy.skillPinning.roots)];
641
679
  mergedPolicy.snapshot.tools = [...new Set(mergedPolicy.snapshot.tools)];
642
680
  mergedPolicy.snapshot.onlyPaths = [...new Set(mergedPolicy.snapshot.onlyPaths)];
643
681
  mergedPolicy.snapshot.ignorePaths = [...new Set(mergedPolicy.snapshot.ignorePaths)];
@@ -726,7 +764,8 @@ var init_config = __esm({
726
764
  // 120-second auto-deny timeout
727
765
  flightRecorder: true,
728
766
  auditHashArgs: true,
729
- approvers: { native: true, browser: true, cloud: false, terminal: true }
767
+ approvers: { native: true, browser: true, cloud: false, terminal: true },
768
+ cloudSyncIntervalHours: 5
730
769
  },
731
770
  policy: {
732
771
  sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
@@ -901,7 +940,8 @@ var init_config = __esm({
901
940
  }
902
941
  ],
903
942
  dlp: { enabled: true, scanIgnoredTools: true },
904
- loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 }
943
+ loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 },
944
+ skillPinning: { enabled: false, mode: "warn", roots: [] }
905
945
  },
906
946
  environments: {}
907
947
  };
@@ -1710,9 +1750,9 @@ function matchesPattern(text, patterns) {
1710
1750
  const withoutDotSlash = text.replace(/^\.\//, "");
1711
1751
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
1712
1752
  }
1713
- function getNestedValue(obj, path35) {
1753
+ function getNestedValue(obj, path41) {
1714
1754
  if (!obj || typeof obj !== "object") return null;
1715
- return path35.split(".").reduce((prev, curr) => prev?.[curr], obj);
1755
+ return path41.split(".").reduce((prev, curr) => prev?.[curr], obj);
1716
1756
  }
1717
1757
  function shouldSnapshot(toolName, args, config) {
1718
1758
  if (!config.settings.enableUndo) return false;
@@ -2187,8 +2227,8 @@ async function explainPolicy(toolName, args) {
2187
2227
  const flattenedArgs = JSON.stringify(args).toLowerCase();
2188
2228
  const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
2189
2229
  allTokens.push(...extraTokens);
2190
- const preview = extraTokens.slice(0, 8).join(", ") + (extraTokens.length > 8 ? "\u2026" : "");
2191
- detail += ` + deep scan of args: [${preview}]`;
2230
+ const preview2 = extraTokens.slice(0, 8).join(", ") + (extraTokens.length > 8 ? "\u2026" : "");
2231
+ detail += ` + deep scan of args: [${preview2}]`;
2192
2232
  }
2193
2233
  steps.push({ name: "Input parsing", outcome: "checked", detail });
2194
2234
  }
@@ -2305,6 +2345,15 @@ var init_policy = __esm({
2305
2345
  import fs8 from "fs";
2306
2346
  import path9 from "path";
2307
2347
  import os7 from "os";
2348
+ function extractCommandPattern(toolName, args) {
2349
+ const lower = toolName.toLowerCase();
2350
+ if (lower !== "bash" && lower !== "execute_bash" && lower !== "shell") return void 0;
2351
+ const a = args;
2352
+ const cmd = typeof a?.["command"] === "string" ? a["command"].trim() : "";
2353
+ if (!cmd) return void 0;
2354
+ const words = cmd.split(/\s+/);
2355
+ return words.slice(0, 2).join(" ");
2356
+ }
2308
2357
  function checkPause() {
2309
2358
  try {
2310
2359
  if (!fs8.existsSync(PAUSED_FILE)) return { paused: false };
@@ -2338,7 +2387,7 @@ function resumeNode9() {
2338
2387
  } catch {
2339
2388
  }
2340
2389
  }
2341
- function getActiveTrustSession(toolName) {
2390
+ function getActiveTrustSession(toolName, args) {
2342
2391
  try {
2343
2392
  if (!fs8.existsSync(TRUST_FILE)) return false;
2344
2393
  const trust = JSON.parse(fs8.readFileSync(TRUST_FILE, "utf-8"));
@@ -2347,12 +2396,20 @@ function getActiveTrustSession(toolName) {
2347
2396
  if (active.length !== trust.entries.length) {
2348
2397
  fs8.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
2349
2398
  }
2350
- return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
2399
+ return active.some((e) => {
2400
+ if (!(e.tool === toolName || matchesPattern(toolName, e.tool))) return false;
2401
+ if (e.commandPattern) {
2402
+ const actual = extractCommandPattern(toolName, args) ?? "";
2403
+ return actual === e.commandPattern || actual.startsWith(e.commandPattern + " ");
2404
+ }
2405
+ return true;
2406
+ });
2351
2407
  } catch {
2352
2408
  return false;
2353
2409
  }
2354
2410
  }
2355
- function writeTrustSession(toolName, durationMs) {
2411
+ function writeTrustSession(toolName, durationMs, args) {
2412
+ const commandPattern = extractCommandPattern(toolName, args);
2356
2413
  try {
2357
2414
  let trust = { entries: [] };
2358
2415
  try {
@@ -2362,8 +2419,14 @@ function writeTrustSession(toolName, durationMs) {
2362
2419
  } catch {
2363
2420
  }
2364
2421
  const now = Date.now();
2365
- trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > now);
2366
- trust.entries.push({ tool: toolName, expiry: now + durationMs });
2422
+ trust.entries = trust.entries.filter(
2423
+ (e) => !(e.tool === toolName && e.commandPattern === commandPattern) && e.expiry > now
2424
+ );
2425
+ trust.entries.push({
2426
+ tool: toolName,
2427
+ ...commandPattern && { commandPattern },
2428
+ expiry: now + durationMs
2429
+ });
2367
2430
  atomicWriteSync(TRUST_FILE, JSON.stringify(trust, null, 2));
2368
2431
  } catch (err2) {
2369
2432
  if (process.env.NODE9_DEBUG === "1") {
@@ -2452,8 +2515,8 @@ function isDaemonRunning() {
2452
2515
  if (port !== DAEMON_PORT) {
2453
2516
  return false;
2454
2517
  }
2455
- const MAX_PID = 4194304;
2456
- if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || pid > MAX_PID) {
2518
+ const MAX_PID2 = 4194304;
2519
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || pid > MAX_PID2) {
2457
2520
  return false;
2458
2521
  }
2459
2522
  try {
@@ -3359,12 +3422,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3359
3422
  };
3360
3423
  }
3361
3424
  }
3362
- if (getActiveTrustSession(toolName)) {
3363
- if (approvers.cloud && creds?.apiKey)
3364
- await auditLocalAllow(toolName, args, "trust", creds, meta);
3365
- if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
3366
- return { approved: true, checkedBy: "trust" };
3367
- }
3368
3425
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
3369
3426
  if (policyResult.decision === "allow") {
3370
3427
  if (approvers.cloud && creds?.apiKey)
@@ -3446,6 +3503,12 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3446
3503
  if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta, hashAuditArgs);
3447
3504
  return { approved: true };
3448
3505
  }
3506
+ if (!taintWarning && getActiveTrustSession(toolName, args)) {
3507
+ if (approvers.cloud && creds?.apiKey)
3508
+ await auditLocalAllow(toolName, args, "trust", creds, meta);
3509
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
3510
+ return { approved: true, checkedBy: "trust" };
3511
+ }
3449
3512
  if (taintWarning) {
3450
3513
  explainableLabel = "\u{1F534} Node9 Taint (Exfiltration Prevention)";
3451
3514
  riskMetadata = computeRiskMetadata(
@@ -3578,7 +3641,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3578
3641
  riskMetadata?.ruleDescription
3579
3642
  );
3580
3643
  if (decision === "always_allow") {
3581
- writeTrustSession(toolName, 36e5);
3644
+ writeTrustSession(toolName, 36e5, args);
3582
3645
  return { approved: true, checkedBy: "trust" };
3583
3646
  }
3584
3647
  const isApproved = decision === "allow";
@@ -5756,7 +5819,7 @@ function writeGlobalSetting(key, value) {
5756
5819
  config.settings[key] = value;
5757
5820
  atomicWriteSync2(GLOBAL_CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
5758
5821
  }
5759
- function writeTrustEntry(toolName, durationMs) {
5822
+ function writeTrustEntry(toolName, durationMs, commandPattern) {
5760
5823
  try {
5761
5824
  let trust = { entries: [] };
5762
5825
  try {
@@ -5764,8 +5827,14 @@ function writeTrustEntry(toolName, durationMs) {
5764
5827
  trust = JSON.parse(fs14.readFileSync(TRUST_FILE2, "utf-8"));
5765
5828
  } catch {
5766
5829
  }
5767
- trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
5768
- trust.entries.push({ tool: toolName, expiry: Date.now() + durationMs });
5830
+ trust.entries = trust.entries.filter(
5831
+ (e) => !(e.tool === toolName && e.commandPattern === commandPattern) && e.expiry > Date.now()
5832
+ );
5833
+ trust.entries.push({
5834
+ tool: toolName,
5835
+ ...commandPattern && { commandPattern },
5836
+ expiry: Date.now() + durationMs
5837
+ });
5769
5838
  atomicWriteSync2(TRUST_FILE2, JSON.stringify(trust, null, 2));
5770
5839
  } catch {
5771
5840
  }
@@ -6243,15 +6312,152 @@ var init_costSync = __esm({
6243
6312
  }
6244
6313
  });
6245
6314
 
6246
- // src/daemon/server.ts
6247
- import http from "http";
6315
+ // src/daemon/sync.ts
6248
6316
  import fs17 from "fs";
6317
+ import https from "https";
6318
+ import os15 from "os";
6249
6319
  import path20 from "path";
6320
+ function readCredentials() {
6321
+ if (process.env.NODE9_API_KEY) {
6322
+ return {
6323
+ apiKey: process.env.NODE9_API_KEY,
6324
+ apiUrl: process.env.NODE9_API_URL ?? DEFAULT_API_URL
6325
+ };
6326
+ }
6327
+ try {
6328
+ const credPath = path20.join(os15.homedir(), ".node9", "credentials.json");
6329
+ const creds = JSON.parse(fs17.readFileSync(credPath, "utf-8"));
6330
+ const profileName = process.env.NODE9_PROFILE ?? "default";
6331
+ const profile = creds[profileName];
6332
+ if (typeof profile?.apiKey === "string" && profile.apiKey.length > 0) {
6333
+ return {
6334
+ apiKey: profile.apiKey,
6335
+ apiUrl: typeof profile.apiUrl === "string" ? profile.apiUrl.replace(/\/intercept$/, "/policy") : DEFAULT_API_URL
6336
+ };
6337
+ }
6338
+ if (typeof creds.apiKey === "string" && creds.apiKey.length > 0) {
6339
+ return { apiKey: creds.apiKey, apiUrl: DEFAULT_API_URL };
6340
+ }
6341
+ } catch {
6342
+ }
6343
+ return null;
6344
+ }
6345
+ function fetchCloudRules(apiKey, apiUrl) {
6346
+ const parsed = new URL(apiUrl);
6347
+ return new Promise((resolve, reject) => {
6348
+ const req = https.request(
6349
+ {
6350
+ hostname: parsed.hostname,
6351
+ port: parsed.port ? parseInt(parsed.port, 10) : void 0,
6352
+ path: parsed.pathname + parsed.search,
6353
+ method: "GET",
6354
+ headers: {
6355
+ Authorization: `Bearer ${apiKey}`,
6356
+ "Content-Type": "application/json"
6357
+ },
6358
+ timeout: 1e4
6359
+ },
6360
+ (res) => {
6361
+ const chunks = [];
6362
+ res.on("data", (chunk) => chunks.push(chunk));
6363
+ res.on("end", () => {
6364
+ if (res.statusCode !== 200) {
6365
+ reject(new Error(`API returned ${res.statusCode ?? "unknown"}`));
6366
+ return;
6367
+ }
6368
+ try {
6369
+ const body = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
6370
+ const rules = Array.isArray(body) ? body : Array.isArray(body.rules) ? body.rules : [];
6371
+ resolve(rules);
6372
+ } catch (e) {
6373
+ reject(e);
6374
+ }
6375
+ });
6376
+ }
6377
+ );
6378
+ req.on("error", reject);
6379
+ req.on("timeout", () => {
6380
+ req.destroy(new Error("Cloud policy fetch timed out"));
6381
+ });
6382
+ req.end();
6383
+ });
6384
+ }
6385
+ async function syncOnce() {
6386
+ const creds = readCredentials();
6387
+ if (!creds) return;
6388
+ try {
6389
+ const rules = await fetchCloudRules(creds.apiKey, creds.apiUrl);
6390
+ const cache = { fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), rules };
6391
+ const dir = path20.dirname(rulesCacheFile());
6392
+ if (!fs17.existsSync(dir)) fs17.mkdirSync(dir, { recursive: true });
6393
+ fs17.writeFileSync(rulesCacheFile(), JSON.stringify(cache, null, 2) + "\n", "utf-8");
6394
+ } catch {
6395
+ }
6396
+ }
6397
+ async function runCloudSync() {
6398
+ const creds = readCredentials();
6399
+ if (!creds) {
6400
+ return { ok: false, reason: "No API key configured. Add credentials with: node9 login" };
6401
+ }
6402
+ try {
6403
+ const rules = await fetchCloudRules(creds.apiKey, creds.apiUrl);
6404
+ const cache = { fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), rules };
6405
+ const dir = path20.dirname(rulesCacheFile());
6406
+ if (!fs17.existsSync(dir)) fs17.mkdirSync(dir, { recursive: true });
6407
+ fs17.writeFileSync(rulesCacheFile(), JSON.stringify(cache, null, 2) + "\n", "utf-8");
6408
+ return { ok: true, rules: rules.length, fetchedAt: cache.fetchedAt };
6409
+ } catch (err2) {
6410
+ return { ok: false, reason: err2 instanceof Error ? err2.message : String(err2) };
6411
+ }
6412
+ }
6413
+ function getCloudSyncStatus() {
6414
+ try {
6415
+ const raw = JSON.parse(fs17.readFileSync(rulesCacheFile(), "utf-8"));
6416
+ if (!Array.isArray(raw.rules) || typeof raw.fetchedAt !== "string") return { cached: false };
6417
+ return { cached: true, rules: raw.rules.length, fetchedAt: raw.fetchedAt };
6418
+ } catch {
6419
+ return { cached: false };
6420
+ }
6421
+ }
6422
+ function getCloudRules() {
6423
+ try {
6424
+ const raw = JSON.parse(fs17.readFileSync(rulesCacheFile(), "utf-8"));
6425
+ return Array.isArray(raw.rules) ? raw.rules : null;
6426
+ } catch {
6427
+ return null;
6428
+ }
6429
+ }
6430
+ function startCloudSync() {
6431
+ const rawHours = getConfig().settings.cloudSyncIntervalHours ?? DEFAULT_INTERVAL_HOURS;
6432
+ const intervalHours = Math.max(rawHours, MIN_INTERVAL_HOURS);
6433
+ const intervalMs = intervalHours * 60 * 60 * 1e3;
6434
+ const initial = setTimeout(() => void syncOnce(), 3e4);
6435
+ initial.unref();
6436
+ const recurring = setInterval(() => void syncOnce(), intervalMs);
6437
+ recurring.unref();
6438
+ }
6439
+ var rulesCacheFile, DEFAULT_API_URL, DEFAULT_INTERVAL_HOURS, MIN_INTERVAL_HOURS;
6440
+ var init_sync = __esm({
6441
+ "src/daemon/sync.ts"() {
6442
+ "use strict";
6443
+ init_config();
6444
+ rulesCacheFile = () => path20.join(os15.homedir(), ".node9", "rules-cache.json");
6445
+ DEFAULT_API_URL = "https://api.node9.ai/api/v1/policy";
6446
+ DEFAULT_INTERVAL_HOURS = 5;
6447
+ MIN_INTERVAL_HOURS = 1;
6448
+ }
6449
+ });
6450
+
6451
+ // src/daemon/server.ts
6452
+ import http from "http";
6453
+ import fs18 from "fs";
6454
+ import path21 from "path";
6250
6455
  import { randomUUID as randomUUID4 } from "crypto";
6251
6456
  import { spawnSync as spawnSync2 } from "child_process";
6252
6457
  import chalk2 from "chalk";
6253
6458
  function startDaemon() {
6254
6459
  startCostSync();
6460
+ startCloudSync();
6255
6461
  loadInsightCounts();
6256
6462
  const csrfToken = randomUUID4();
6257
6463
  const internalToken = randomUUID4();
@@ -6267,7 +6473,7 @@ function startDaemon() {
6267
6473
  idleTimer = setTimeout(() => {
6268
6474
  if (autoStarted) {
6269
6475
  try {
6270
- fs17.unlinkSync(DAEMON_PID_FILE);
6476
+ fs18.unlinkSync(DAEMON_PID_FILE);
6271
6477
  } catch {
6272
6478
  }
6273
6479
  }
@@ -6430,7 +6636,7 @@ data: ${JSON.stringify(item.data)}
6430
6636
  status: "pending"
6431
6637
  });
6432
6638
  }
6433
- const projectCwd = typeof cwd === "string" && path20.isAbsolute(cwd) ? cwd : void 0;
6639
+ const projectCwd = typeof cwd === "string" && path21.isAbsolute(cwd) ? cwd : void 0;
6434
6640
  const projectConfig = getConfig(projectCwd);
6435
6641
  const browserEnabled = projectConfig.settings.approvers?.browser !== false;
6436
6642
  const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
@@ -6557,7 +6763,8 @@ data: ${JSON.stringify(item.data)}
6557
6763
  );
6558
6764
  if (decision === "trust" && trustDuration) {
6559
6765
  const ms = TRUST_DURATIONS[trustDuration] ?? 60 * 6e4;
6560
- writeTrustEntry(entry.toolName, ms);
6766
+ const commandPattern = extractCommandPattern(entry.toolName, entry.args);
6767
+ writeTrustEntry(entry.toolName, ms, commandPattern);
6561
6768
  appendAuditLog({
6562
6769
  toolName: entry.toolName,
6563
6770
  args: entry.args,
@@ -6820,8 +7027,8 @@ data: ${JSON.stringify(item.data)}
6820
7027
  const body = await readBody(req);
6821
7028
  const data = body ? JSON.parse(body) : {};
6822
7029
  const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
6823
- const node9Dir = path20.dirname(GLOBAL_CONFIG_PATH);
6824
- if (!path20.resolve(configPath).startsWith(node9Dir + path20.sep)) {
7030
+ const node9Dir = path21.dirname(GLOBAL_CONFIG_PATH);
7031
+ if (!path21.resolve(configPath).startsWith(node9Dir + path21.sep)) {
6825
7032
  res.writeHead(400, { "Content-Type": "application/json" });
6826
7033
  return res.end(
6827
7034
  JSON.stringify({ error: "configPath must be within the node9 config directory" })
@@ -6932,14 +7139,14 @@ data: ${JSON.stringify(item.data)}
6932
7139
  server.on("error", (e) => {
6933
7140
  if (e.code === "EADDRINUSE") {
6934
7141
  try {
6935
- if (fs17.existsSync(DAEMON_PID_FILE)) {
6936
- const { pid } = JSON.parse(fs17.readFileSync(DAEMON_PID_FILE, "utf-8"));
7142
+ if (fs18.existsSync(DAEMON_PID_FILE)) {
7143
+ const { pid } = JSON.parse(fs18.readFileSync(DAEMON_PID_FILE, "utf-8"));
6937
7144
  process.kill(pid, 0);
6938
7145
  return process.exit(0);
6939
7146
  }
6940
7147
  } catch {
6941
7148
  try {
6942
- fs17.unlinkSync(DAEMON_PID_FILE);
7149
+ fs18.unlinkSync(DAEMON_PID_FILE);
6943
7150
  } catch {
6944
7151
  }
6945
7152
  server.listen(DAEMON_PORT, DAEMON_HOST);
@@ -7005,59 +7212,331 @@ var init_server = __esm({
7005
7212
  init_shields();
7006
7213
  init_ui2();
7007
7214
  init_state2();
7215
+ init_state();
7008
7216
  init_patch();
7009
7217
  init_config_schema();
7010
7218
  init_costSync();
7219
+ init_sync();
7220
+ }
7221
+ });
7222
+
7223
+ // src/daemon/service.ts
7224
+ import fs19 from "fs";
7225
+ import path22 from "path";
7226
+ import os16 from "os";
7227
+ import { spawnSync as spawnSync3, execFileSync } from "child_process";
7228
+ function resolveNode9Binary() {
7229
+ try {
7230
+ const script = process.argv[1];
7231
+ if (typeof script === "string" && path22.isAbsolute(script) && fs19.existsSync(script)) {
7232
+ return fs19.realpathSync(script);
7233
+ }
7234
+ } catch {
7235
+ }
7236
+ try {
7237
+ const cmd = process.platform === "win32" ? "where" : "which";
7238
+ const r = spawnSync3(cmd, ["node9"], { encoding: "utf8", timeout: 3e3 });
7239
+ if (r.status === 0 && r.stdout.trim()) {
7240
+ return r.stdout.trim().split("\n")[0].trim();
7241
+ }
7242
+ } catch {
7243
+ }
7244
+ return null;
7245
+ }
7246
+ function xmlEscape(s) {
7247
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
7248
+ }
7249
+ function launchdPlist(binaryPath) {
7250
+ const logDir = path22.join(os16.homedir(), ".node9");
7251
+ const nodePath = xmlEscape(process.execPath);
7252
+ const scriptPath = xmlEscape(binaryPath);
7253
+ const outLog = xmlEscape(path22.join(logDir, "daemon.log"));
7254
+ const errLog = xmlEscape(path22.join(logDir, "daemon-error.log"));
7255
+ return `<?xml version="1.0" encoding="UTF-8"?>
7256
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
7257
+ <plist version="1.0">
7258
+ <dict>
7259
+ <key>Label</key>
7260
+ <string>${LAUNCHD_LABEL}</string>
7261
+ <key>ProgramArguments</key>
7262
+ <array>
7263
+ <string>${nodePath}</string>
7264
+ <string>${scriptPath}</string>
7265
+ <string>daemon</string>
7266
+ </array>
7267
+ <key>RunAtLoad</key>
7268
+ <true/>
7269
+ <key>KeepAlive</key>
7270
+ <true/>
7271
+ <key>ThrottleInterval</key>
7272
+ <integer>10</integer>
7273
+ <key>StandardOutPath</key>
7274
+ <string>${outLog}</string>
7275
+ <key>StandardErrorPath</key>
7276
+ <string>${errLog}</string>
7277
+ <key>EnvironmentVariables</key>
7278
+ <dict>
7279
+ <key>NODE9_AUTO_STARTED</key>
7280
+ <string>1</string>
7281
+ <key>NODE9_BROWSER_OPENED</key>
7282
+ <string>1</string>
7283
+ </dict>
7284
+ </dict>
7285
+ </plist>
7286
+ `;
7287
+ }
7288
+ function installLaunchd(binaryPath) {
7289
+ const dir = path22.dirname(LAUNCHD_PLIST);
7290
+ if (!fs19.existsSync(dir)) fs19.mkdirSync(dir, { recursive: true });
7291
+ fs19.writeFileSync(LAUNCHD_PLIST, launchdPlist(binaryPath), "utf-8");
7292
+ spawnSync3("launchctl", ["unload", LAUNCHD_PLIST], { encoding: "utf8" });
7293
+ const r = spawnSync3("launchctl", ["load", "-w", LAUNCHD_PLIST], {
7294
+ encoding: "utf8",
7295
+ timeout: 5e3
7296
+ });
7297
+ if (r.status !== 0) {
7298
+ throw new Error(`launchctl load failed: ${r.stderr || r.stdout || "unknown error"}`);
7299
+ }
7300
+ }
7301
+ function uninstallLaunchd() {
7302
+ if (fs19.existsSync(LAUNCHD_PLIST)) {
7303
+ spawnSync3("launchctl", ["unload", "-w", LAUNCHD_PLIST], { encoding: "utf8", timeout: 5e3 });
7304
+ fs19.unlinkSync(LAUNCHD_PLIST);
7305
+ }
7306
+ }
7307
+ function isLaunchdInstalled() {
7308
+ return fs19.existsSync(LAUNCHD_PLIST);
7309
+ }
7310
+ function systemdUnit(binaryPath) {
7311
+ return `[Unit]
7312
+ Description=node9 approval daemon
7313
+ After=network.target
7314
+
7315
+ [Service]
7316
+ Type=simple
7317
+ ExecStart=${process.execPath} ${binaryPath} daemon
7318
+ Restart=on-failure
7319
+ RestartSec=10s
7320
+ Environment=NODE9_AUTO_STARTED=1
7321
+ Environment=NODE9_BROWSER_OPENED=1
7322
+
7323
+ [Install]
7324
+ WantedBy=default.target
7325
+ `;
7326
+ }
7327
+ function installSystemd(binaryPath) {
7328
+ if (!fs19.existsSync(SYSTEMD_UNIT_DIR)) {
7329
+ fs19.mkdirSync(SYSTEMD_UNIT_DIR, { recursive: true });
7330
+ }
7331
+ fs19.writeFileSync(SYSTEMD_UNIT, systemdUnit(binaryPath), "utf-8");
7332
+ try {
7333
+ execFileSync("loginctl", ["enable-linger", os16.userInfo().username], { timeout: 3e3 });
7334
+ } catch {
7335
+ }
7336
+ const reload = spawnSync3("systemctl", ["--user", "daemon-reload"], {
7337
+ encoding: "utf8",
7338
+ timeout: 5e3
7339
+ });
7340
+ if (reload.status !== 0) {
7341
+ throw new Error(`systemctl daemon-reload failed: ${reload.stderr}`);
7342
+ }
7343
+ spawnSync3("systemctl", ["--user", "stop", "node9-daemon"], { encoding: "utf8", timeout: 3e3 });
7344
+ const enable = spawnSync3("systemctl", ["--user", "enable", "--now", "node9-daemon"], {
7345
+ encoding: "utf8",
7346
+ timeout: 5e3
7347
+ });
7348
+ if (enable.status !== 0) {
7349
+ throw new Error(`systemctl enable failed: ${enable.stderr}`);
7350
+ }
7351
+ }
7352
+ function uninstallSystemd() {
7353
+ if (fs19.existsSync(SYSTEMD_UNIT)) {
7354
+ spawnSync3("systemctl", ["--user", "disable", "--now", "node9-daemon"], {
7355
+ encoding: "utf8",
7356
+ timeout: 5e3
7357
+ });
7358
+ spawnSync3("systemctl", ["--user", "daemon-reload"], { encoding: "utf8", timeout: 5e3 });
7359
+ fs19.unlinkSync(SYSTEMD_UNIT);
7360
+ }
7361
+ }
7362
+ function isSystemdInstalled() {
7363
+ return fs19.existsSync(SYSTEMD_UNIT);
7364
+ }
7365
+ function stopRunningDaemon() {
7366
+ const pidFile = path22.join(os16.homedir(), ".node9", "daemon.pid");
7367
+ if (!fs19.existsSync(pidFile)) return;
7368
+ try {
7369
+ const data = JSON.parse(fs19.readFileSync(pidFile, "utf-8"));
7370
+ const pid = data.pid;
7371
+ const MAX_PID2 = 4194304;
7372
+ if (typeof pid === "number" && Number.isInteger(pid) && pid > 0 && pid <= MAX_PID2) {
7373
+ try {
7374
+ process.kill(pid, "SIGTERM");
7375
+ const deadline = Date.now() + 3e3;
7376
+ const pollStop = spawnSync3(
7377
+ "sh",
7378
+ ["-c", `while kill -0 ${pid} 2>/dev/null; do sleep 0.1; done`],
7379
+ {
7380
+ timeout: 3100
7381
+ }
7382
+ );
7383
+ void pollStop;
7384
+ void deadline;
7385
+ } catch {
7386
+ }
7387
+ }
7388
+ try {
7389
+ fs19.unlinkSync(pidFile);
7390
+ } catch {
7391
+ }
7392
+ } catch {
7393
+ }
7394
+ }
7395
+ function installDaemonService() {
7396
+ const binary = resolveNode9Binary();
7397
+ if (!binary) {
7398
+ return { ok: false, reason: "Could not locate the node9 binary. Is it in your PATH?" };
7399
+ }
7400
+ stopRunningDaemon();
7401
+ try {
7402
+ if (process.platform === "darwin") {
7403
+ const alreadyInstalled = isLaunchdInstalled();
7404
+ installLaunchd(binary);
7405
+ return { ok: true, platform: "launchd", alreadyInstalled };
7406
+ }
7407
+ if (process.platform === "linux") {
7408
+ const check = spawnSync3("systemctl", ["--user", "--version"], {
7409
+ encoding: "utf8",
7410
+ timeout: 2e3
7411
+ });
7412
+ if (check.status !== 0) {
7413
+ return {
7414
+ ok: false,
7415
+ reason: "systemd not available. Start the daemon manually with: node9 daemon start"
7416
+ };
7417
+ }
7418
+ const alreadyInstalled = isSystemdInstalled();
7419
+ installSystemd(binary);
7420
+ return { ok: true, platform: "systemd", alreadyInstalled };
7421
+ }
7422
+ return {
7423
+ ok: false,
7424
+ reason: `Automatic service install is not supported on ${process.platform}. Start the daemon manually with: node9 daemon start`
7425
+ };
7426
+ } catch (err2) {
7427
+ return {
7428
+ ok: false,
7429
+ reason: err2 instanceof Error ? err2.message : String(err2)
7430
+ };
7431
+ }
7432
+ }
7433
+ function uninstallDaemonService() {
7434
+ try {
7435
+ if (process.platform === "darwin") {
7436
+ uninstallLaunchd();
7437
+ return { ok: true, platform: "launchd", alreadyInstalled: false };
7438
+ }
7439
+ if (process.platform === "linux") {
7440
+ uninstallSystemd();
7441
+ return { ok: true, platform: "systemd", alreadyInstalled: false };
7442
+ }
7443
+ return {
7444
+ ok: false,
7445
+ reason: `Service management not supported on ${process.platform}.`
7446
+ };
7447
+ } catch (err2) {
7448
+ return {
7449
+ ok: false,
7450
+ reason: err2 instanceof Error ? err2.message : String(err2)
7451
+ };
7452
+ }
7453
+ }
7454
+ function isDaemonServiceInstalled() {
7455
+ if (process.platform === "darwin") return isLaunchdInstalled();
7456
+ if (process.platform === "linux") return isSystemdInstalled();
7457
+ return false;
7458
+ }
7459
+ var LAUNCHD_LABEL, LAUNCHD_PLIST, SYSTEMD_UNIT_DIR, SYSTEMD_UNIT;
7460
+ var init_service = __esm({
7461
+ "src/daemon/service.ts"() {
7462
+ "use strict";
7463
+ LAUNCHD_LABEL = "ai.node9.daemon";
7464
+ LAUNCHD_PLIST = path22.join(os16.homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
7465
+ SYSTEMD_UNIT_DIR = path22.join(os16.homedir(), ".config", "systemd", "user");
7466
+ SYSTEMD_UNIT = path22.join(SYSTEMD_UNIT_DIR, "node9-daemon.service");
7011
7467
  }
7012
7468
  });
7013
7469
 
7014
7470
  // src/daemon/index.ts
7015
- import fs18 from "fs";
7471
+ import fs20 from "fs";
7016
7472
  import chalk3 from "chalk";
7017
- import { spawnSync as spawnSync3 } from "child_process";
7473
+ import { spawnSync as spawnSync4 } from "child_process";
7018
7474
  function stopDaemon() {
7019
- if (!fs18.existsSync(DAEMON_PID_FILE)) return console.log(chalk3.yellow("Not running."));
7475
+ if (!fs20.existsSync(DAEMON_PID_FILE)) return console.log(chalk3.yellow("Not running."));
7020
7476
  try {
7021
- const { pid } = JSON.parse(fs18.readFileSync(DAEMON_PID_FILE, "utf-8"));
7477
+ const data = JSON.parse(fs20.readFileSync(DAEMON_PID_FILE, "utf-8"));
7478
+ const pid = data.pid;
7479
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || pid > MAX_PID) {
7480
+ console.log(chalk3.gray("Cleaned up invalid PID file."));
7481
+ return;
7482
+ }
7022
7483
  process.kill(pid, "SIGTERM");
7023
7484
  console.log(chalk3.green("\u2705 Stopped."));
7024
7485
  } catch {
7025
7486
  console.log(chalk3.gray("Cleaned up stale PID file."));
7026
7487
  } finally {
7027
7488
  try {
7028
- fs18.unlinkSync(DAEMON_PID_FILE);
7489
+ fs20.unlinkSync(DAEMON_PID_FILE);
7029
7490
  } catch {
7030
7491
  }
7031
7492
  }
7032
7493
  }
7033
7494
  function daemonStatus() {
7034
- if (fs18.existsSync(DAEMON_PID_FILE)) {
7495
+ const serviceInstalled = isDaemonServiceInstalled();
7496
+ const serviceLabel = serviceInstalled ? chalk3.green("installed (starts on login)") : chalk3.yellow("not installed \u2014 run: node9 daemon install");
7497
+ let processStatus;
7498
+ if (fs20.existsSync(DAEMON_PID_FILE)) {
7035
7499
  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;
7500
+ const data = JSON.parse(fs20.readFileSync(DAEMON_PID_FILE, "utf-8"));
7501
+ const pid = data.pid;
7502
+ const port = data.port;
7503
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || pid > MAX_PID) {
7504
+ processStatus = chalk3.yellow("not running (invalid PID file)");
7505
+ } else {
7506
+ process.kill(pid, 0);
7507
+ processStatus = chalk3.green(
7508
+ `running (PID ${pid}, port ${typeof port === "number" ? port : DAEMON_PORT})`
7509
+ );
7510
+ }
7040
7511
  } catch {
7041
- console.log(chalk3.yellow("Node9 daemon: not running (stale PID)"));
7042
- return;
7512
+ processStatus = chalk3.yellow("not running (stale PID file)");
7043
7513
  }
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
7514
  } else {
7052
- console.log(chalk3.yellow("Node9 daemon: not running"));
7515
+ const r = spawnSync4("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
7516
+ encoding: "utf8",
7517
+ timeout: 500
7518
+ });
7519
+ if (r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`)) {
7520
+ processStatus = chalk3.yellow(`running (orphaned \u2014 no PID file)`);
7521
+ } else {
7522
+ processStatus = chalk3.yellow("not running");
7523
+ }
7053
7524
  }
7525
+ console.log(`
7526
+ Process : ${processStatus}`);
7527
+ console.log(` Service : ${serviceLabel}
7528
+ `);
7054
7529
  }
7530
+ var MAX_PID;
7055
7531
  var init_daemon2 = __esm({
7056
7532
  "src/daemon/index.ts"() {
7057
7533
  "use strict";
7058
7534
  init_server();
7059
7535
  init_state2();
7536
+ init_service();
7060
7537
  init_state2();
7538
+ init_service();
7539
+ MAX_PID = 4194304;
7061
7540
  }
7062
7541
  });
7063
7542
 
@@ -7067,10 +7546,10 @@ __export(tail_exports, {
7067
7546
  startTail: () => startTail
7068
7547
  });
7069
7548
  import http2 from "http";
7070
- import chalk19 from "chalk";
7071
- import fs29 from "fs";
7072
- import os25 from "os";
7073
- import path32 from "path";
7549
+ import chalk24 from "chalk";
7550
+ import fs35 from "fs";
7551
+ import os31 from "os";
7552
+ import path38 from "path";
7074
7553
  import readline5 from "readline";
7075
7554
  import { spawn as spawn10, execSync as execSync3 } from "child_process";
7076
7555
  function getIcon(tool) {
@@ -7093,22 +7572,22 @@ function formatBase(activity) {
7093
7572
  const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
7094
7573
  const icon = getIcon(activity.tool);
7095
7574
  const toolName = activity.tool.slice(0, 16).padEnd(16);
7096
- const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os25.homedir(), "~");
7575
+ const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os31.homedir(), "~");
7097
7576
  const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
7098
- return `${chalk19.gray(time)} ${icon} ${chalk19.white.bold(toolName)} ${chalk19.dim(argsPreview)}`;
7577
+ return `${chalk24.gray(time)} ${icon} ${chalk24.white.bold(toolName)} ${chalk24.dim(argsPreview)}`;
7099
7578
  }
7100
7579
  function renderResult(activity, result) {
7101
7580
  const base = formatBase(activity);
7102
7581
  let status;
7103
7582
  if (result.status === "allow") {
7104
- status = chalk19.green("\u2713 ALLOW");
7583
+ status = chalk24.green("\u2713 ALLOW");
7105
7584
  } else if (result.status === "dlp") {
7106
- status = chalk19.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
7585
+ status = chalk24.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
7107
7586
  } else {
7108
- status = chalk19.red("\u2717 BLOCK");
7587
+ status = chalk24.red("\u2717 BLOCK");
7109
7588
  }
7110
7589
  const cost = result.costEstimate ?? activity.costEstimate;
7111
- const costSuffix = cost == null ? "" : chalk19.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
7590
+ const costSuffix = cost == null ? "" : chalk24.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
7112
7591
  if (process.stdout.isTTY) {
7113
7592
  if (pendingShownForId === activity.id && pendingWrappedLines > 1) {
7114
7593
  readline5.moveCursor(process.stdout, 0, -(pendingWrappedLines - 1));
@@ -7125,19 +7604,19 @@ function renderResult(activity, result) {
7125
7604
  }
7126
7605
  function renderPending(activity) {
7127
7606
  if (!process.stdout.isTTY) return;
7128
- const line = `${formatBase(activity)} ${chalk19.yellow("\u25CF \u2026")}`;
7607
+ const line = `${formatBase(activity)} ${chalk24.yellow("\u25CF \u2026")}`;
7129
7608
  pendingShownForId = activity.id;
7130
7609
  pendingWrappedLines = wrappedLineCount(line);
7131
7610
  process.stdout.write(`${line}\r`);
7132
7611
  }
7133
7612
  async function ensureDaemon() {
7134
7613
  let pidPort = null;
7135
- if (fs29.existsSync(PID_FILE)) {
7614
+ if (fs35.existsSync(PID_FILE)) {
7136
7615
  try {
7137
- const { port } = JSON.parse(fs29.readFileSync(PID_FILE, "utf-8"));
7616
+ const { port } = JSON.parse(fs35.readFileSync(PID_FILE, "utf-8"));
7138
7617
  pidPort = port;
7139
7618
  } catch {
7140
- console.error(chalk19.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
7619
+ console.error(chalk24.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
7141
7620
  }
7142
7621
  }
7143
7622
  const checkPort = pidPort ?? DAEMON_PORT;
@@ -7148,7 +7627,7 @@ async function ensureDaemon() {
7148
7627
  if (res.ok) return checkPort;
7149
7628
  } catch {
7150
7629
  }
7151
- console.log(chalk19.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
7630
+ console.log(chalk24.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
7152
7631
  const child = spawn10(process.execPath, [process.argv[1], "daemon"], {
7153
7632
  detached: true,
7154
7633
  stdio: "ignore",
@@ -7165,7 +7644,7 @@ async function ensureDaemon() {
7165
7644
  } catch {
7166
7645
  }
7167
7646
  }
7168
- console.error(chalk19.red("\u274C Daemon failed to start. Try: node9 daemon start"));
7647
+ console.error(chalk24.red("\u274C Daemon failed to start. Try: node9 daemon start"));
7169
7648
  process.exit(1);
7170
7649
  }
7171
7650
  function postDecisionHttp(id, decision, csrfToken, port, opts) {
@@ -7197,27 +7676,56 @@ function postDecisionHttp(id, decision, csrfToken, port, opts) {
7197
7676
  req.end(body);
7198
7677
  });
7199
7678
  }
7679
+ function extractArgsSummary(toolName, args) {
7680
+ const a = args;
7681
+ if (!a) return "";
7682
+ const cmd = a["command"] ?? a["cmd"];
7683
+ if (typeof cmd === "string") return cmd.replace(/\s+/g, " ").trim();
7684
+ const fp = a["file_path"] ?? a["path"] ?? a["filepath"];
7685
+ if (typeof fp === "string") return fp;
7686
+ const q = a["query"] ?? a["sql"];
7687
+ if (typeof q === "string") return q.replace(/\s+/g, " ").trim();
7688
+ const s = JSON.stringify(a).replace(/\s+/g, " ");
7689
+ return s.length > 80 ? s.slice(0, 80) + "\u2026" : s;
7690
+ }
7691
+ function cleanReason(raw) {
7692
+ return raw.replace(/\s*[—–-]+\s*(blocked by|requires human approval)[^)]*(\([^)]*\))?\.?\s*$/i, "").trim();
7693
+ }
7694
+ function cleanBlockedBy(raw) {
7695
+ const shieldMatch = raw.match(/shield:([^:]+):/i);
7696
+ if (shieldMatch) return shieldMatch[1];
7697
+ const smartMatch = raw.match(/^Smart Rule:\s*(.+)$/i);
7698
+ if (smartMatch) return smartMatch[1];
7699
+ return raw;
7700
+ }
7200
7701
  function buildCardLines(req, localCount = 0) {
7201
7702
  if (req.recoveryCommand) {
7202
7703
  return buildRecoveryCardLines(req);
7203
7704
  }
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";
7705
+ const argsSummary = extractArgsSummary(req.toolName, req.args);
7706
+ const argsPreview = argsSummary.length > 72 ? argsSummary.slice(0, 72) + "\u2026" : argsSummary;
7707
+ const rawBlockedBy = req.riskMetadata?.blockedByLabel ?? "Policy rule";
7708
+ const blockedBy = cleanBlockedBy(rawBlockedBy);
7709
+ const isBlock = req.riskMetadata?.tier != null && req.riskMetadata.tier <= 2;
7710
+ const severityIcon = isBlock ? `${RED}\u{1F6D1}` : `${YELLOW}\u26A0 `;
7711
+ const rawDesc = req.riskMetadata?.ruleDescription ?? "";
7712
+ const description = rawDesc ? cleanReason(rawDesc) : "";
7208
7713
  const lines = [
7209
7714
  ``,
7210
7715
  `${BOLD2}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET2}`,
7211
7716
  `${CYAN}\u2551${RESET2} Tool: ${BOLD2}${req.toolName}${RESET2}`,
7212
- `${CYAN}\u2551${RESET2} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET2}`
7717
+ `${CYAN}\u2551${RESET2} Policy: ${severityIcon} ${blockedBy}${RESET2}`
7213
7718
  ];
7214
- if (req.riskMetadata?.ruleDescription) {
7215
- lines.push(`${CYAN}\u2551${RESET2} ${YELLOW}\u2139 ${req.riskMetadata.ruleDescription}${RESET2}`);
7719
+ if (description) {
7720
+ lines.push(`${CYAN}\u2551${RESET2} Why: ${YELLOW}${description}${RESET2}`);
7216
7721
  }
7217
- if (req.riskMetadata?.ruleName && blockedBy.includes("Taint")) {
7722
+ if (req.riskMetadata?.ruleName && rawBlockedBy.includes("Taint")) {
7218
7723
  lines.push(`${CYAN}\u2551${RESET2} ${YELLOW}\u26A0 ${req.riskMetadata.ruleName}${RESET2}`);
7219
7724
  }
7220
- lines.push(`${CYAN}\u2551${RESET2} Args: ${GRAY}${argsPreview}${RESET2}`);
7725
+ if (argsPreview) {
7726
+ const argLabel = req.toolName.toLowerCase().includes("bash") ? "Command" : "Args ";
7727
+ lines.push(`${CYAN}\u2551${RESET2} ${argLabel}: ${GRAY}${argsPreview}${RESET2}`);
7728
+ }
7221
7729
  if (localCount >= 2) {
7222
7730
  lines.push(
7223
7731
  `${CYAN}\u2551${RESET2} ${YELLOW}\u{1F4A1}${RESET2} Approved ${localCount}\xD7 before \u2014 ${BOLD2}[a]${RESET2}${YELLOW} creates a permanent rule${RESET2}`
@@ -7257,9 +7765,9 @@ function buildRecoveryCardLines(req) {
7257
7765
  ];
7258
7766
  }
7259
7767
  function readApproversFromDisk() {
7260
- const configPath = path32.join(os25.homedir(), ".node9", "config.json");
7768
+ const configPath = path38.join(os31.homedir(), ".node9", "config.json");
7261
7769
  try {
7262
- const raw = JSON.parse(fs29.readFileSync(configPath, "utf-8"));
7770
+ const raw = JSON.parse(fs35.readFileSync(configPath, "utf-8"));
7263
7771
  const settings = raw.settings ?? {};
7264
7772
  return settings.approvers ?? {};
7265
7773
  } catch {
@@ -7270,20 +7778,20 @@ function approverStatusLine() {
7270
7778
  const a = readApproversFromDisk();
7271
7779
  const fmt = (label, key) => {
7272
7780
  const on = a[key] !== false;
7273
- return `[${key[0]}]${label.slice(1)} ${on ? chalk19.green("\u2713") : chalk19.dim("\u2717")}`;
7781
+ return `[${key[0]}]${label.slice(1)} ${on ? chalk24.green("\u2713") : chalk24.dim("\u2717")}`;
7274
7782
  };
7275
7783
  return `${fmt("native", "native")} ${fmt("browser", "browser")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
7276
7784
  }
7277
7785
  function toggleApprover(channel) {
7278
- const configPath = path32.join(os25.homedir(), ".node9", "config.json");
7786
+ const configPath = path38.join(os31.homedir(), ".node9", "config.json");
7279
7787
  try {
7280
- const raw = JSON.parse(fs29.readFileSync(configPath, "utf-8"));
7788
+ const raw = JSON.parse(fs35.readFileSync(configPath, "utf-8"));
7281
7789
  const settings = raw.settings ?? {};
7282
7790
  const approvers = settings.approvers ?? {};
7283
7791
  approvers[channel] = approvers[channel] === false;
7284
7792
  settings.approvers = approvers;
7285
7793
  raw.settings = settings;
7286
- fs29.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
7794
+ fs35.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
7287
7795
  } catch (err2) {
7288
7796
  process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
7289
7797
  `);
@@ -7315,7 +7823,7 @@ async function startTail(options = {}) {
7315
7823
  req2.end();
7316
7824
  });
7317
7825
  if (result.ok) {
7318
- console.log(chalk19.green("\u2713 Flight Recorder buffer cleared."));
7826
+ console.log(chalk24.green("\u2713 Flight Recorder buffer cleared."));
7319
7827
  } else if (result.code === "ECONNREFUSED") {
7320
7828
  throw new Error("Daemon is not running. Start it with: node9 daemon start");
7321
7829
  } else if (result.code === "ETIMEDOUT") {
@@ -7359,7 +7867,7 @@ async function startTail(options = {}) {
7359
7867
  const channel = name === "n" ? "native" : name === "b" ? "browser" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
7360
7868
  if (channel) {
7361
7869
  toggleApprover(channel);
7362
- console.log(chalk19.dim(` Approvers: ${approverStatusLine()}`));
7870
+ console.log(chalk24.dim(` Approvers: ${approverStatusLine()}`));
7363
7871
  }
7364
7872
  };
7365
7873
  process.stdin.on("keypress", idleKeypressHandler);
@@ -7425,7 +7933,7 @@ async function startTail(options = {}) {
7425
7933
  localAllowCounts.get(req2.toolName) ?? 0
7426
7934
  )
7427
7935
  );
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");
7936
+ const decisionStamp = action === "always-allow" ? chalk24.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk24.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk24.green("\u2713 ALLOWED") : action === "redirect" ? chalk24.yellow("\u21A9 REDIRECT AI") : chalk24.red("\u2717 DENIED");
7429
7937
  stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
7430
7938
  for (const line of stampedLines) process.stdout.write(line + "\n");
7431
7939
  process.stdout.write(SHOW_CURSOR);
@@ -7453,8 +7961,8 @@ async function startTail(options = {}) {
7453
7961
  }
7454
7962
  postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err2) => {
7455
7963
  try {
7456
- fs29.appendFileSync(
7457
- path32.join(os25.homedir(), ".node9", "hook-debug.log"),
7964
+ fs35.appendFileSync(
7965
+ path38.join(os31.homedir(), ".node9", "hook-debug.log"),
7458
7966
  `[tail] POST /decision failed: ${String(err2)}
7459
7967
  `
7460
7968
  );
@@ -7476,7 +7984,7 @@ async function startTail(options = {}) {
7476
7984
  );
7477
7985
  const stampedLines = buildCardLines(req2, priorCount);
7478
7986
  if (externalDecision) {
7479
- const source = externalDecision === "allow" ? chalk19.green("\u2713 ALLOWED") : chalk19.red("\u2717 DENIED");
7987
+ const source = externalDecision === "allow" ? chalk24.green("\u2713 ALLOWED") : chalk24.red("\u2717 DENIED");
7480
7988
  stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
7481
7989
  }
7482
7990
  for (const line of stampedLines) process.stdout.write(line + "\n");
@@ -7535,16 +8043,16 @@ async function startTail(options = {}) {
7535
8043
  }
7536
8044
  } catch {
7537
8045
  }
7538
- console.log(chalk19.cyan.bold(`
7539
- \u{1F6F0}\uFE0F Node9 tail `) + chalk19.dim(`\u2192 ${dashboardUrl}`));
8046
+ console.log(chalk24.cyan.bold(`
8047
+ \u{1F6F0}\uFE0F Node9 tail `) + chalk24.dim(`\u2192 ${dashboardUrl}`));
7540
8048
  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`));
8049
+ console.log(chalk24.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
8050
+ console.log(chalk24.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
7543
8051
  }
7544
8052
  if (options.history) {
7545
- console.log(chalk19.dim("Showing history + live events.\n"));
8053
+ console.log(chalk24.dim("Showing history + live events.\n"));
7546
8054
  } else {
7547
- console.log(chalk19.dim("Showing live events only. Use --history to include past.\n"));
8055
+ console.log(chalk24.dim("Showing live events only. Use --history to include past.\n"));
7548
8056
  }
7549
8057
  process.on("SIGINT", () => {
7550
8058
  exitIdleMode();
@@ -7554,13 +8062,13 @@ async function startTail(options = {}) {
7554
8062
  readline5.clearLine(process.stdout, 0);
7555
8063
  readline5.cursorTo(process.stdout, 0);
7556
8064
  }
7557
- console.log(chalk19.dim("\n\u{1F6F0}\uFE0F Disconnected."));
8065
+ console.log(chalk24.dim("\n\u{1F6F0}\uFE0F Disconnected."));
7558
8066
  process.exit(0);
7559
8067
  });
7560
8068
  const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
7561
8069
  const req = http2.get(sseUrl, (res) => {
7562
8070
  if (res.statusCode !== 200) {
7563
- console.error(chalk19.red(`Failed to connect: HTTP ${res.statusCode}`));
8071
+ console.error(chalk24.red(`Failed to connect: HTTP ${res.statusCode}`));
7564
8072
  process.exit(1);
7565
8073
  }
7566
8074
  if (canApprove) enterIdleMode();
@@ -7591,7 +8099,7 @@ async function startTail(options = {}) {
7591
8099
  readline5.clearLine(process.stdout, 0);
7592
8100
  readline5.cursorTo(process.stdout, 0);
7593
8101
  }
7594
- console.log(chalk19.red("\n\u274C Daemon disconnected."));
8102
+ console.log(chalk24.red("\n\u274C Daemon disconnected."));
7595
8103
  process.exit(1);
7596
8104
  });
7597
8105
  });
@@ -7683,9 +8191,9 @@ async function startTail(options = {}) {
7683
8191
  const hash = data.hash ?? "";
7684
8192
  const summary = data.argsSummary ?? data.tool;
7685
8193
  const fileCount = data.fileCount ?? 0;
7686
- const files = fileCount > 0 ? chalk19.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
8194
+ const files = fileCount > 0 ? chalk24.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
7687
8195
  process.stdout.write(
7688
- `${chalk19.dim(time)} ${chalk19.cyan("\u{1F4F8} snapshot")} ${chalk19.dim(hash)} ${summary}${files}
8196
+ `${chalk24.dim(time)} ${chalk24.cyan("\u{1F4F8} snapshot")} ${chalk24.dim(hash)} ${summary}${files}
7689
8197
  `
7690
8198
  );
7691
8199
  return;
@@ -7702,7 +8210,7 @@ async function startTail(options = {}) {
7702
8210
  }
7703
8211
  req.on("error", (err2) => {
7704
8212
  const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
7705
- console.error(chalk19.red(`
8213
+ console.error(chalk24.red(`
7706
8214
  \u274C ${msg}`));
7707
8215
  process.exit(1);
7708
8216
  });
@@ -7714,7 +8222,7 @@ var init_tail = __esm({
7714
8222
  init_daemon2();
7715
8223
  init_daemon();
7716
8224
  init_core();
7717
- PID_FILE = path32.join(os25.homedir(), ".node9", "daemon.pid");
8225
+ PID_FILE = path38.join(os31.homedir(), ".node9", "daemon.pid");
7718
8226
  ICONS = {
7719
8227
  bash: "\u{1F4BB}",
7720
8228
  shell: "\u{1F4BB}",
@@ -7755,9 +8263,9 @@ __export(hud_exports, {
7755
8263
  main: () => main,
7756
8264
  renderEnvironmentLine: () => renderEnvironmentLine
7757
8265
  });
7758
- import fs30 from "fs";
7759
- import path33 from "path";
7760
- import os26 from "os";
8266
+ import fs36 from "fs";
8267
+ import path39 from "path";
8268
+ import os32 from "os";
7761
8269
  import http3 from "http";
7762
8270
  async function readStdin() {
7763
8271
  const chunks = [];
@@ -7833,9 +8341,9 @@ function formatTimeLeft(resetsAt) {
7833
8341
  return ` (${m}m left)`;
7834
8342
  }
7835
8343
  function safeReadJson(filePath) {
7836
- if (!fs30.existsSync(filePath)) return null;
8344
+ if (!fs36.existsSync(filePath)) return null;
7837
8345
  try {
7838
- return JSON.parse(fs30.readFileSync(filePath, "utf-8"));
8346
+ return JSON.parse(fs36.readFileSync(filePath, "utf-8"));
7839
8347
  } catch {
7840
8348
  return null;
7841
8349
  }
@@ -7856,12 +8364,12 @@ function countHooksInFile(filePath) {
7856
8364
  return Object.keys(cfg.hooks).length;
7857
8365
  }
7858
8366
  function countRulesInDir(rulesDir) {
7859
- if (!fs30.existsSync(rulesDir)) return 0;
8367
+ if (!fs36.existsSync(rulesDir)) return 0;
7860
8368
  let count = 0;
7861
8369
  try {
7862
- for (const entry of fs30.readdirSync(rulesDir, { withFileTypes: true })) {
8370
+ for (const entry of fs36.readdirSync(rulesDir, { withFileTypes: true })) {
7863
8371
  if (entry.isDirectory()) {
7864
- count += countRulesInDir(path33.join(rulesDir, entry.name));
8372
+ count += countRulesInDir(path39.join(rulesDir, entry.name));
7865
8373
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
7866
8374
  count++;
7867
8375
  }
@@ -7872,46 +8380,46 @@ function countRulesInDir(rulesDir) {
7872
8380
  }
7873
8381
  function isSamePath(a, b) {
7874
8382
  try {
7875
- return path33.resolve(a) === path33.resolve(b);
8383
+ return path39.resolve(a) === path39.resolve(b);
7876
8384
  } catch {
7877
8385
  return false;
7878
8386
  }
7879
8387
  }
7880
8388
  function countConfigs(cwd) {
7881
- const homeDir2 = os26.homedir();
7882
- const claudeDir = path33.join(homeDir2, ".claude");
8389
+ const homeDir2 = os32.homedir();
8390
+ const claudeDir = path39.join(homeDir2, ".claude");
7883
8391
  let claudeMdCount = 0;
7884
8392
  let rulesCount = 0;
7885
8393
  let hooksCount = 0;
7886
8394
  const userMcpServers = /* @__PURE__ */ new Set();
7887
8395
  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");
8396
+ if (fs36.existsSync(path39.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
8397
+ rulesCount += countRulesInDir(path39.join(claudeDir, "rules"));
8398
+ const userSettings = path39.join(claudeDir, "settings.json");
7891
8399
  for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
7892
8400
  hooksCount += countHooksInFile(userSettings);
7893
- const userClaudeJson = path33.join(homeDir2, ".claude.json");
8401
+ const userClaudeJson = path39.join(homeDir2, ".claude.json");
7894
8402
  for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
7895
8403
  for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
7896
8404
  userMcpServers.delete(name);
7897
8405
  }
7898
8406
  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");
8407
+ if (fs36.existsSync(path39.join(cwd, "CLAUDE.md"))) claudeMdCount++;
8408
+ if (fs36.existsSync(path39.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
8409
+ const projectClaudeDir = path39.join(cwd, ".claude");
7902
8410
  const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
7903
8411
  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");
8412
+ if (fs36.existsSync(path39.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
8413
+ rulesCount += countRulesInDir(path39.join(projectClaudeDir, "rules"));
8414
+ const projSettings = path39.join(projectClaudeDir, "settings.json");
7907
8415
  for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
7908
8416
  hooksCount += countHooksInFile(projSettings);
7909
8417
  }
7910
- if (fs30.existsSync(path33.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
7911
- const localSettings = path33.join(projectClaudeDir, "settings.local.json");
8418
+ if (fs36.existsSync(path39.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
8419
+ const localSettings = path39.join(projectClaudeDir, "settings.local.json");
7912
8420
  for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
7913
8421
  hooksCount += countHooksInFile(localSettings);
7914
- const mcpJsonServers = getMcpServerNames(path33.join(cwd, ".mcp.json"));
8422
+ const mcpJsonServers = getMcpServerNames(path39.join(cwd, ".mcp.json"));
7915
8423
  const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
7916
8424
  for (const name of disabledMcpJson) mcpJsonServers.delete(name);
7917
8425
  for (const name of mcpJsonServers) projectMcpServers.add(name);
@@ -7944,12 +8452,12 @@ function readActiveShieldsHud() {
7944
8452
  return shieldsCache.value;
7945
8453
  }
7946
8454
  try {
7947
- const shieldsPath = path33.join(os26.homedir(), ".node9", "shields.json");
7948
- if (!fs30.existsSync(shieldsPath)) {
8455
+ const shieldsPath = path39.join(os32.homedir(), ".node9", "shields.json");
8456
+ if (!fs36.existsSync(shieldsPath)) {
7949
8457
  shieldsCache = { value: [], ts: now };
7950
8458
  return [];
7951
8459
  }
7952
- const parsed = JSON.parse(fs30.readFileSync(shieldsPath, "utf-8"));
8460
+ const parsed = JSON.parse(fs36.readFileSync(shieldsPath, "utf-8"));
7953
8461
  if (!Array.isArray(parsed.active)) {
7954
8462
  shieldsCache = { value: [], ts: now };
7955
8463
  return [];
@@ -8051,17 +8559,17 @@ function renderContextLine(stdin) {
8051
8559
  async function main() {
8052
8560
  try {
8053
8561
  const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
8054
- if (fs30.existsSync(path33.join(os26.homedir(), ".node9", "hud-debug"))) {
8562
+ if (fs36.existsSync(path39.join(os32.homedir(), ".node9", "hud-debug"))) {
8055
8563
  try {
8056
- const logPath = path33.join(os26.homedir(), ".node9", "hud-debug.log");
8564
+ const logPath = path39.join(os32.homedir(), ".node9", "hud-debug.log");
8057
8565
  const MAX_LOG_SIZE = 10 * 1024 * 1024;
8058
8566
  let size = 0;
8059
8567
  try {
8060
- size = fs30.statSync(logPath).size;
8568
+ size = fs36.statSync(logPath).size;
8061
8569
  } catch {
8062
8570
  }
8063
8571
  if (size < MAX_LOG_SIZE) {
8064
- fs30.appendFileSync(
8572
+ fs36.appendFileSync(
8065
8573
  logPath,
8066
8574
  JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
8067
8575
  );
@@ -8082,11 +8590,11 @@ async function main() {
8082
8590
  try {
8083
8591
  const cwd = stdin.cwd ?? process.cwd();
8084
8592
  for (const configPath of [
8085
- path33.join(cwd, "node9.config.json"),
8086
- path33.join(os26.homedir(), ".node9", "config.json")
8593
+ path39.join(cwd, "node9.config.json"),
8594
+ path39.join(os32.homedir(), ".node9", "config.json")
8087
8595
  ]) {
8088
- if (!fs30.existsSync(configPath)) continue;
8089
- const cfg = JSON.parse(fs30.readFileSync(configPath, "utf-8"));
8596
+ if (!fs36.existsSync(configPath)) continue;
8597
+ const cfg = JSON.parse(fs36.readFileSync(configPath, "utf-8"));
8090
8598
  const hud = cfg.settings?.hud;
8091
8599
  if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
8092
8600
  }
@@ -8517,7 +9025,9 @@ function detectAgents(homeDir2 = os11.homedir()) {
8517
9025
  claude: exists(path15.join(homeDir2, ".claude")) || exists(path15.join(homeDir2, ".claude.json")),
8518
9026
  gemini: exists(path15.join(homeDir2, ".gemini")),
8519
9027
  cursor: exists(path15.join(homeDir2, ".cursor")),
8520
- codex: exists(path15.join(homeDir2, ".codex"))
9028
+ codex: exists(path15.join(homeDir2, ".codex")),
9029
+ windsurf: exists(path15.join(homeDir2, ".codeium", "windsurf")),
9030
+ vscode: exists(path15.join(homeDir2, ".vscode"))
8521
9031
  };
8522
9032
  }
8523
9033
  async function setupCursor() {
@@ -8666,6 +9176,38 @@ async function setupCodex() {
8666
9176
  printDaemonTip();
8667
9177
  }
8668
9178
  }
9179
+ function teardownCodex() {
9180
+ const homeDir2 = os11.homedir();
9181
+ const configPath = path15.join(homeDir2, ".codex", "config.toml");
9182
+ const config = readToml(configPath);
9183
+ if (!config?.mcp_servers) {
9184
+ console.log(chalk.blue(" \u2139\uFE0F ~/.codex/config.toml not found \u2014 nothing to remove"));
9185
+ return;
9186
+ }
9187
+ let changed = false;
9188
+ if (removeNode9McpServer(config.mcp_servers)) {
9189
+ changed = true;
9190
+ console.log(chalk.green(" \u2705 Removed node9 MCP server entry from ~/.codex/config.toml"));
9191
+ }
9192
+ for (const [name, server] of Object.entries(config.mcp_servers)) {
9193
+ const args = server.args;
9194
+ if (server.command === "node9" && Array.isArray(args) && args[0] === "mcp" && args[1] === "--upstream" && typeof args[2] === "string") {
9195
+ const [originalCmd, ...originalArgs] = args[2].split(" ");
9196
+ config.mcp_servers[name] = {
9197
+ ...server,
9198
+ command: originalCmd,
9199
+ args: originalArgs.length ? originalArgs : void 0
9200
+ };
9201
+ changed = true;
9202
+ }
9203
+ }
9204
+ if (changed) {
9205
+ writeToml(configPath, config);
9206
+ console.log(chalk.green(" \u2705 Unwrapped MCP servers in ~/.codex/config.toml"));
9207
+ } else {
9208
+ console.log(chalk.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in ~/.codex/config.toml"));
9209
+ }
9210
+ }
8669
9211
  function setupHud() {
8670
9212
  const homeDir2 = os11.homedir();
8671
9213
  const hooksPath = path15.join(homeDir2, ".claude", "settings.json");
@@ -8712,31 +9254,303 @@ function teardownHud() {
8712
9254
  console.log(chalk.green(" \u2705 node9 HUD removed from ~/.claude/settings.json"));
8713
9255
  console.log(chalk.gray(" Restart Claude Code for changes to take effect."));
8714
9256
  }
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;
9257
+ async function setupWindsurf() {
9258
+ const homeDir2 = os11.homedir();
9259
+ const mcpPath = path15.join(homeDir2, ".codeium", "windsurf", "mcp_config.json");
9260
+ const mcpConfig = readJson(mcpPath) ?? {};
9261
+ const servers = mcpConfig.mcpServers ?? {};
9262
+ let anythingChanged = false;
9263
+ if (!hasNode9McpServer(servers)) {
9264
+ servers["node9"] = NODE9_MCP_SERVER_ENTRY;
9265
+ mcpConfig.mcpServers = servers;
9266
+ writeJson(mcpPath, mcpConfig);
9267
+ console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
9268
+ anythingChanged = true;
9269
+ }
9270
+ const serversToWrap = [];
9271
+ for (const [name, server] of Object.entries(servers)) {
9272
+ if (!server.command || server.command === "node9") continue;
9273
+ serversToWrap.push({ name, upstream: [server.command, ...server.args ?? []].join(" ") });
9274
+ }
9275
+ if (serversToWrap.length > 0) {
9276
+ console.log(chalk.bold("The following existing entries will be modified:\n"));
9277
+ console.log(chalk.white(` ${mcpPath}`));
9278
+ for (const { name, upstream } of serversToWrap) {
9279
+ console.log(chalk.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
9280
+ }
9281
+ console.log("");
9282
+ const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
9283
+ if (proceed) {
9284
+ for (const { name, upstream } of serversToWrap) {
9285
+ servers[name] = {
9286
+ ...servers[name],
9287
+ command: "node9",
9288
+ args: ["mcp", "--upstream", upstream]
9289
+ };
9290
+ }
9291
+ mcpConfig.mcpServers = servers;
9292
+ writeJson(mcpPath, mcpConfig);
9293
+ console.log(chalk.green(`
9294
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
9295
+ anythingChanged = true;
9296
+ } else {
9297
+ console.log(chalk.yellow(" Skipped MCP server wrapping."));
9298
+ }
9299
+ console.log("");
9300
+ }
9301
+ console.log(
9302
+ chalk.yellow(
9303
+ " \u26A0\uFE0F Note: Windsurf does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Windsurf."
9304
+ )
9305
+ );
9306
+ console.log("");
9307
+ if (!anythingChanged && serversToWrap.length === 0) {
9308
+ console.log(chalk.blue("\u2139\uFE0F Node9 is already fully configured for Windsurf."));
9309
+ printDaemonTip();
9310
+ return;
9311
+ }
9312
+ if (anythingChanged) {
9313
+ console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Windsurf via MCP proxy!"));
9314
+ console.log(chalk.gray(" Restart Windsurf for changes to take effect."));
9315
+ printDaemonTip();
9316
+ }
9317
+ }
9318
+ function teardownWindsurf() {
9319
+ const homeDir2 = os11.homedir();
9320
+ const mcpPath = path15.join(homeDir2, ".codeium", "windsurf", "mcp_config.json");
9321
+ const mcpConfig = readJson(mcpPath);
9322
+ if (!mcpConfig?.mcpServers) {
9323
+ console.log(
9324
+ chalk.blue(" \u2139\uFE0F ~/.codeium/windsurf/mcp_config.json not found \u2014 nothing to remove")
9325
+ );
9326
+ return;
9327
+ }
9328
+ let changed = false;
9329
+ if (removeNode9McpServer(mcpConfig.mcpServers)) {
9330
+ changed = true;
9331
+ console.log(
9332
+ chalk.green(" \u2705 Removed node9 MCP server entry from ~/.codeium/windsurf/mcp_config.json")
9333
+ );
9334
+ }
9335
+ for (const [name, server] of Object.entries(mcpConfig.mcpServers)) {
9336
+ const args = server.args;
9337
+ if (server.command === "node9" && Array.isArray(args) && args[0] === "mcp" && args[1] === "--upstream" && typeof args[2] === "string") {
9338
+ const [originalCmd, ...originalArgs] = args[2].split(" ");
9339
+ mcpConfig.mcpServers[name] = {
9340
+ ...server,
9341
+ command: originalCmd,
9342
+ args: originalArgs.length ? originalArgs : void 0
9343
+ };
9344
+ changed = true;
9345
+ }
9346
+ }
9347
+ if (changed) {
9348
+ writeJson(mcpPath, mcpConfig);
9349
+ console.log(chalk.green(" \u2705 Unwrapped MCP servers in ~/.codeium/windsurf/mcp_config.json"));
9350
+ } else {
9351
+ console.log(
9352
+ chalk.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in ~/.codeium/windsurf/mcp_config.json")
9353
+ );
9354
+ }
9355
+ }
9356
+ function hasNode9McpServerVSCode(servers) {
9357
+ const entry = servers["node9"];
9358
+ return !!entry && entry.command === "node9" && Array.isArray(entry.args) && entry.args[0] === "mcp-server";
9359
+ }
9360
+ async function setupVSCode() {
9361
+ const homeDir2 = os11.homedir();
9362
+ const mcpPath = path15.join(homeDir2, ".vscode", "mcp.json");
9363
+ const mcpConfig = readJson(mcpPath) ?? {};
9364
+ const servers = mcpConfig.servers ?? {};
9365
+ let anythingChanged = false;
9366
+ if (!hasNode9McpServerVSCode(servers)) {
9367
+ servers["node9"] = { type: "stdio", command: "node9", args: ["mcp-server"] };
9368
+ mcpConfig.servers = servers;
9369
+ writeJson(mcpPath, mcpConfig);
9370
+ console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
9371
+ anythingChanged = true;
9372
+ }
9373
+ const serversToWrap = [];
9374
+ for (const [name, server] of Object.entries(servers)) {
9375
+ if (!server.command || server.command === "node9") continue;
9376
+ serversToWrap.push({ name, upstream: [server.command, ...server.args ?? []].join(" ") });
9377
+ }
9378
+ if (serversToWrap.length > 0) {
9379
+ console.log(chalk.bold("The following existing entries will be modified:\n"));
9380
+ console.log(chalk.white(` ${mcpPath}`));
9381
+ for (const { name, upstream } of serversToWrap) {
9382
+ console.log(chalk.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
9383
+ }
9384
+ console.log("");
9385
+ const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
9386
+ if (proceed) {
9387
+ for (const { name, upstream } of serversToWrap) {
9388
+ servers[name] = {
9389
+ ...servers[name],
9390
+ type: "stdio",
9391
+ command: "node9",
9392
+ args: ["mcp", "--upstream", upstream]
9393
+ };
9394
+ }
9395
+ mcpConfig.servers = servers;
9396
+ writeJson(mcpPath, mcpConfig);
9397
+ console.log(chalk.green(`
9398
+ \u2705 ${serversToWrap.length} MCP server(s) wrapped`));
9399
+ anythingChanged = true;
9400
+ } else {
9401
+ console.log(chalk.yellow(" Skipped MCP server wrapping."));
9402
+ }
9403
+ console.log("");
9404
+ }
9405
+ console.log(
9406
+ chalk.yellow(
9407
+ " \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."
9408
+ )
9409
+ );
9410
+ console.log("");
9411
+ if (!anythingChanged && serversToWrap.length === 0) {
9412
+ console.log(chalk.blue("\u2139\uFE0F Node9 is already fully configured for VSCode."));
9413
+ printDaemonTip();
9414
+ return;
9415
+ }
9416
+ if (anythingChanged) {
9417
+ console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting VSCode via MCP proxy!"));
9418
+ console.log(chalk.gray(" Restart VSCode for changes to take effect."));
9419
+ printDaemonTip();
9420
+ }
9421
+ }
9422
+ function teardownVSCode() {
9423
+ const homeDir2 = os11.homedir();
9424
+ const mcpPath = path15.join(homeDir2, ".vscode", "mcp.json");
9425
+ const mcpConfig = readJson(mcpPath);
9426
+ if (!mcpConfig?.servers) {
9427
+ console.log(chalk.blue(" \u2139\uFE0F ~/.vscode/mcp.json not found \u2014 nothing to remove"));
9428
+ return;
9429
+ }
9430
+ let changed = false;
9431
+ if (hasNode9McpServerVSCode(mcpConfig.servers)) {
9432
+ delete mcpConfig.servers["node9"];
9433
+ changed = true;
9434
+ console.log(chalk.green(" \u2705 Removed node9 MCP server entry from ~/.vscode/mcp.json"));
9435
+ }
9436
+ for (const [name, server] of Object.entries(mcpConfig.servers)) {
9437
+ const args = server.args;
9438
+ if (server.command === "node9" && Array.isArray(args) && args[0] === "mcp" && args[1] === "--upstream" && typeof args[2] === "string") {
9439
+ const [originalCmd, ...originalArgs] = args[2].split(" ");
9440
+ mcpConfig.servers[name] = {
9441
+ ...server,
9442
+ type: "stdio",
9443
+ command: originalCmd,
9444
+ args: originalArgs.length ? originalArgs : void 0
9445
+ };
9446
+ changed = true;
9447
+ }
9448
+ }
9449
+ if (changed) {
9450
+ writeJson(mcpPath, mcpConfig);
9451
+ console.log(chalk.green(" \u2705 Unwrapped MCP servers in ~/.vscode/mcp.json"));
9452
+ } else {
9453
+ console.log(chalk.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in ~/.vscode/mcp.json"));
9454
+ }
9455
+ }
9456
+ function getAgentsStatus(homeDir2 = os11.homedir()) {
9457
+ const detected = detectAgents(homeDir2);
9458
+ const claudeWired = (() => {
9459
+ const settings = readJson(path15.join(homeDir2, ".claude", "settings.json"));
9460
+ return !!settings?.hooks?.PreToolUse?.some((m) => m.hooks.some((h) => isNode9Hook(h.command)));
9461
+ })();
9462
+ const geminiWired = (() => {
9463
+ const settings = readJson(path15.join(homeDir2, ".gemini", "settings.json"));
9464
+ return !!settings?.hooks?.BeforeTool?.some((m) => m.hooks.some((h) => isNode9Hook(h.command)));
9465
+ })();
9466
+ const cursorWired = (() => {
9467
+ const cfg = readJson(path15.join(homeDir2, ".cursor", "mcp.json"));
9468
+ return !!(cfg?.mcpServers && hasNode9McpServer(cfg.mcpServers));
9469
+ })();
9470
+ const codexWired = (() => {
9471
+ const cfg = readToml(path15.join(homeDir2, ".codex", "config.toml"));
9472
+ return !!(cfg?.mcp_servers && hasNode9McpServer(cfg.mcp_servers));
9473
+ })();
9474
+ const windsurfWired = (() => {
9475
+ const cfg = readJson(
9476
+ path15.join(homeDir2, ".codeium", "windsurf", "mcp_config.json")
9477
+ );
9478
+ return !!(cfg?.mcpServers && hasNode9McpServer(cfg.mcpServers));
9479
+ })();
9480
+ const vscodeWired = (() => {
9481
+ const cfg = readJson(path15.join(homeDir2, ".vscode", "mcp.json"));
9482
+ return !!(cfg?.servers && hasNode9McpServerVSCode(cfg.servers));
9483
+ })();
9484
+ return [
9485
+ {
9486
+ name: "claude",
9487
+ label: "Claude Code",
9488
+ installed: detected.claude,
9489
+ wired: claudeWired,
9490
+ mode: detected.claude ? "hooks" : null
9491
+ },
9492
+ {
9493
+ name: "gemini",
9494
+ label: "Gemini CLI",
9495
+ installed: detected.gemini,
9496
+ wired: geminiWired,
9497
+ mode: detected.gemini ? "hooks" : null
9498
+ },
9499
+ {
9500
+ name: "cursor",
9501
+ label: "Cursor",
9502
+ installed: detected.cursor,
9503
+ wired: cursorWired,
9504
+ mode: detected.cursor ? "mcp" : null
9505
+ },
9506
+ {
9507
+ name: "windsurf",
9508
+ label: "Windsurf",
9509
+ installed: detected.windsurf,
9510
+ wired: windsurfWired,
9511
+ mode: detected.windsurf ? "mcp" : null
9512
+ },
9513
+ {
9514
+ name: "vscode",
9515
+ label: "VSCode",
9516
+ installed: detected.vscode,
9517
+ wired: vscodeWired,
9518
+ mode: detected.vscode ? "mcp" : null
9519
+ },
9520
+ {
9521
+ name: "codex",
9522
+ label: "Codex",
9523
+ installed: detected.codex,
9524
+ wired: codexWired,
9525
+ mode: detected.codex ? "mcp" : null
9526
+ }
9527
+ ];
9528
+ }
9529
+
9530
+ // src/cli.ts
9531
+ init_daemon2();
9532
+ import chalk25 from "chalk";
9533
+ import fs37 from "fs";
9534
+ import path40 from "path";
9535
+ import os33 from "os";
9536
+ import { confirm as confirm2 } from "@inquirer/prompts";
9537
+
9538
+ // src/utils/duration.ts
9539
+ function parseDuration(str) {
9540
+ const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
9541
+ if (!m) return null;
9542
+ const n = parseFloat(m[1]);
9543
+ switch ((m[2] ?? "m").toLowerCase()) {
9544
+ case "s":
9545
+ return Math.round(n * 1e3);
9546
+ case "m":
9547
+ return Math.round(n * 6e4);
9548
+ case "h":
9549
+ return Math.round(n * 36e5);
9550
+ case "d":
9551
+ return Math.round(n * 864e5);
9552
+ default:
9553
+ return null;
8740
9554
  }
8741
9555
  }
8742
9556
 
@@ -8947,19 +9761,19 @@ init_daemon();
8947
9761
  init_config();
8948
9762
  init_policy();
8949
9763
  import chalk5 from "chalk";
8950
- import fs20 from "fs";
9764
+ import fs23 from "fs";
8951
9765
  import { spawn as spawn6 } from "child_process";
8952
- import path22 from "path";
8953
- import os16 from "os";
9766
+ import path25 from "path";
9767
+ import os19 from "os";
8954
9768
 
8955
9769
  // src/undo.ts
8956
- import { spawnSync as spawnSync4, spawn as spawn5 } from "child_process";
9770
+ import { spawnSync as spawnSync5, spawn as spawn5 } from "child_process";
8957
9771
  import crypto3 from "crypto";
8958
- import fs19 from "fs";
9772
+ import fs21 from "fs";
8959
9773
  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");
9774
+ import path23 from "path";
9775
+ import os17 from "os";
9776
+ var ACTIVITY_SOCKET_PATH3 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path23.join(os17.tmpdir(), "node9-activity.sock");
8963
9777
  function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
8964
9778
  try {
8965
9779
  const payload = JSON.stringify({
@@ -8979,22 +9793,22 @@ function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
8979
9793
  } catch {
8980
9794
  }
8981
9795
  }
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");
9796
+ var SNAPSHOT_STACK_PATH = path23.join(os17.homedir(), ".node9", "snapshots.json");
9797
+ var UNDO_LATEST_PATH = path23.join(os17.homedir(), ".node9", "undo_latest.txt");
8984
9798
  var MAX_SNAPSHOTS = 10;
8985
9799
  var GIT_TIMEOUT = 15e3;
8986
9800
  function readStack() {
8987
9801
  try {
8988
- if (fs19.existsSync(SNAPSHOT_STACK_PATH))
8989
- return JSON.parse(fs19.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
9802
+ if (fs21.existsSync(SNAPSHOT_STACK_PATH))
9803
+ return JSON.parse(fs21.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
8990
9804
  } catch {
8991
9805
  }
8992
9806
  return [];
8993
9807
  }
8994
9808
  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));
9809
+ const dir = path23.dirname(SNAPSHOT_STACK_PATH);
9810
+ if (!fs21.existsSync(dir)) fs21.mkdirSync(dir, { recursive: true });
9811
+ fs21.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
8998
9812
  }
8999
9813
  function extractFilePath(args) {
9000
9814
  if (!args || typeof args !== "object") return null;
@@ -9014,12 +9828,12 @@ function buildArgsSummary(tool, args) {
9014
9828
  return "";
9015
9829
  }
9016
9830
  function findProjectRoot(filePath) {
9017
- let dir = path21.dirname(filePath);
9831
+ let dir = path23.dirname(filePath);
9018
9832
  while (true) {
9019
- if (fs19.existsSync(path21.join(dir, ".git")) || fs19.existsSync(path21.join(dir, "package.json"))) {
9833
+ if (fs21.existsSync(path23.join(dir, ".git")) || fs21.existsSync(path23.join(dir, "package.json"))) {
9020
9834
  return dir;
9021
9835
  }
9022
- const parent = path21.dirname(dir);
9836
+ const parent = path23.dirname(dir);
9023
9837
  if (parent === dir) return process.cwd();
9024
9838
  dir = parent;
9025
9839
  }
@@ -9027,7 +9841,7 @@ function findProjectRoot(filePath) {
9027
9841
  function normalizeCwdForHash(cwd) {
9028
9842
  let normalized;
9029
9843
  try {
9030
- normalized = fs19.realpathSync(cwd);
9844
+ normalized = fs21.realpathSync(cwd);
9031
9845
  } catch {
9032
9846
  normalized = cwd;
9033
9847
  }
@@ -9037,16 +9851,16 @@ function normalizeCwdForHash(cwd) {
9037
9851
  }
9038
9852
  function getShadowRepoDir(cwd) {
9039
9853
  const hash = crypto3.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
9040
- return path21.join(os15.homedir(), ".node9", "snapshots", hash);
9854
+ return path23.join(os17.homedir(), ".node9", "snapshots", hash);
9041
9855
  }
9042
9856
  function cleanOrphanedIndexFiles(shadowDir) {
9043
9857
  try {
9044
9858
  const cutoff = Date.now() - 6e4;
9045
- for (const f of fs19.readdirSync(shadowDir)) {
9859
+ for (const f of fs21.readdirSync(shadowDir)) {
9046
9860
  if (f.startsWith("index_")) {
9047
- const fp = path21.join(shadowDir, f);
9861
+ const fp = path23.join(shadowDir, f);
9048
9862
  try {
9049
- if (fs19.statSync(fp).mtimeMs < cutoff) fs19.unlinkSync(fp);
9863
+ if (fs21.statSync(fp).mtimeMs < cutoff) fs21.unlinkSync(fp);
9050
9864
  } catch {
9051
9865
  }
9052
9866
  }
@@ -9058,7 +9872,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
9058
9872
  const hardcoded = [".git", ".node9"];
9059
9873
  const lines = [...hardcoded, ...ignorePaths].join("\n");
9060
9874
  try {
9061
- fs19.writeFileSync(path21.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
9875
+ fs21.writeFileSync(path23.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
9062
9876
  } catch {
9063
9877
  }
9064
9878
  }
@@ -9066,54 +9880,54 @@ function ensureShadowRepo(shadowDir, cwd) {
9066
9880
  cleanOrphanedIndexFiles(shadowDir);
9067
9881
  const normalizedCwd = normalizeCwdForHash(cwd);
9068
9882
  const shadowEnvBase = { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd };
9069
- const check = spawnSync4("git", ["rev-parse", "--git-dir"], {
9883
+ const check = spawnSync5("git", ["rev-parse", "--git-dir"], {
9070
9884
  env: shadowEnvBase,
9071
9885
  timeout: 3e3
9072
9886
  });
9073
9887
  if (check.status === 0) {
9074
- const ptPath = path21.join(shadowDir, "project-path.txt");
9888
+ const ptPath = path23.join(shadowDir, "project-path.txt");
9075
9889
  try {
9076
- const stored = fs19.readFileSync(ptPath, "utf8").trim();
9890
+ const stored = fs21.readFileSync(ptPath, "utf8").trim();
9077
9891
  if (stored === normalizedCwd) return true;
9078
9892
  if (process.env.NODE9_DEBUG === "1")
9079
9893
  console.error(
9080
9894
  `[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
9081
9895
  );
9082
- fs19.rmSync(shadowDir, { recursive: true, force: true });
9896
+ fs21.rmSync(shadowDir, { recursive: true, force: true });
9083
9897
  } catch {
9084
9898
  try {
9085
- fs19.writeFileSync(ptPath, normalizedCwd, "utf8");
9899
+ fs21.writeFileSync(ptPath, normalizedCwd, "utf8");
9086
9900
  } catch {
9087
9901
  }
9088
9902
  return true;
9089
9903
  }
9090
9904
  }
9091
9905
  try {
9092
- fs19.mkdirSync(shadowDir, { recursive: true });
9906
+ fs21.mkdirSync(shadowDir, { recursive: true });
9093
9907
  } catch {
9094
9908
  }
9095
- const init = spawnSync4("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
9909
+ const init = spawnSync5("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
9096
9910
  if (init.status !== 0 || init.error) {
9097
9911
  const reason = init.error ? init.error.message : init.stderr?.toString();
9098
9912
  if (process.env.NODE9_DEBUG === "1") console.error("[Node9] git init --bare failed:", reason);
9099
9913
  return false;
9100
9914
  }
9101
- const configFile = path21.join(shadowDir, "config");
9102
- spawnSync4("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
9915
+ const configFile = path23.join(shadowDir, "config");
9916
+ spawnSync5("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
9103
9917
  timeout: 3e3
9104
9918
  });
9105
- spawnSync4("git", ["config", "--file", configFile, "core.fsmonitor", "true"], {
9919
+ spawnSync5("git", ["config", "--file", configFile, "core.fsmonitor", "true"], {
9106
9920
  timeout: 3e3
9107
9921
  });
9108
9922
  try {
9109
- fs19.writeFileSync(path21.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
9923
+ fs21.writeFileSync(path23.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
9110
9924
  } catch {
9111
9925
  }
9112
9926
  return true;
9113
9927
  }
9114
9928
  function buildGitEnv(cwd) {
9115
9929
  const shadowDir = getShadowRepoDir(cwd);
9116
- const check = spawnSync4("git", ["rev-parse", "--git-dir"], {
9930
+ const check = spawnSync5("git", ["rev-parse", "--git-dir"], {
9117
9931
  env: { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd },
9118
9932
  timeout: 2e3
9119
9933
  });
@@ -9126,23 +9940,23 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
9126
9940
  let indexFile = null;
9127
9941
  try {
9128
9942
  const rawFilePath = extractFilePath(args);
9129
- const absFilePath = rawFilePath && path21.isAbsolute(rawFilePath) ? rawFilePath : null;
9943
+ const absFilePath = rawFilePath && path23.isAbsolute(rawFilePath) ? rawFilePath : null;
9130
9944
  const cwd = absFilePath ? findProjectRoot(absFilePath) : process.cwd();
9131
9945
  const shadowDir = getShadowRepoDir(cwd);
9132
9946
  if (!ensureShadowRepo(shadowDir, cwd)) return null;
9133
9947
  writeShadowExcludes(shadowDir, ignorePaths);
9134
- indexFile = path21.join(shadowDir, `index_${process.pid}_${Date.now()}`);
9948
+ indexFile = path23.join(shadowDir, `index_${process.pid}_${Date.now()}`);
9135
9949
  const shadowEnv = {
9136
9950
  ...process.env,
9137
9951
  GIT_DIR: shadowDir,
9138
9952
  GIT_WORK_TREE: cwd,
9139
9953
  GIT_INDEX_FILE: indexFile
9140
9954
  };
9141
- spawnSync4("git", ["add", "-A"], { env: shadowEnv, timeout: GIT_TIMEOUT });
9142
- const treeRes = spawnSync4("git", ["write-tree"], { env: shadowEnv, timeout: GIT_TIMEOUT });
9955
+ spawnSync5("git", ["add", "-A"], { env: shadowEnv, timeout: GIT_TIMEOUT });
9956
+ const treeRes = spawnSync5("git", ["write-tree"], { env: shadowEnv, timeout: GIT_TIMEOUT });
9143
9957
  const treeHash = treeRes.stdout?.toString().trim();
9144
9958
  if (!treeHash || treeRes.status !== 0) return null;
9145
- const commitRes = spawnSync4(
9959
+ const commitRes = spawnSync5(
9146
9960
  "git",
9147
9961
  ["commit-tree", treeHash, "-m", `Node9 AI Snapshot: ${(/* @__PURE__ */ new Date()).toISOString()}`],
9148
9962
  { env: shadowEnv, timeout: GIT_TIMEOUT }
@@ -9154,7 +9968,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
9154
9968
  let capturedFiles = [];
9155
9969
  let capturedDiff = null;
9156
9970
  if (prevEntry) {
9157
- const filesRes = spawnSync4("git", ["diff", "--name-only", prevEntry.hash, commitHash], {
9971
+ const filesRes = spawnSync5("git", ["diff", "--name-only", prevEntry.hash, commitHash], {
9158
9972
  env: shadowEnv,
9159
9973
  timeout: GIT_TIMEOUT
9160
9974
  });
@@ -9164,7 +9978,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
9164
9978
  if (capturedFiles.length === 0) {
9165
9979
  return prevEntry.hash;
9166
9980
  }
9167
- const diffRes = spawnSync4("git", ["diff", prevEntry.hash, commitHash], {
9981
+ const diffRes = spawnSync5("git", ["diff", prevEntry.hash, commitHash], {
9168
9982
  env: shadowEnv,
9169
9983
  timeout: GIT_TIMEOUT
9170
9984
  });
@@ -9172,7 +9986,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
9172
9986
  capturedDiff = diffRes.stdout?.toString() || null;
9173
9987
  }
9174
9988
  } else {
9175
- const filesRes = spawnSync4("git", ["ls-tree", "-r", "--name-only", commitHash], {
9989
+ const filesRes = spawnSync5("git", ["ls-tree", "-r", "--name-only", commitHash], {
9176
9990
  env: shadowEnv,
9177
9991
  timeout: GIT_TIMEOUT
9178
9992
  });
@@ -9203,7 +10017,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
9203
10017
  writeStack(stack);
9204
10018
  const entry = stack[stack.length - 1];
9205
10019
  notifySnapshotTaken(commitHash.slice(0, 7), tool, entry.argsSummary, capturedFiles.length);
9206
- fs19.writeFileSync(UNDO_LATEST_PATH, commitHash);
10020
+ fs21.writeFileSync(UNDO_LATEST_PATH, commitHash);
9207
10021
  if (shouldGc) {
9208
10022
  spawn5("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
9209
10023
  }
@@ -9214,7 +10028,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
9214
10028
  } finally {
9215
10029
  if (indexFile) {
9216
10030
  try {
9217
- fs19.unlinkSync(indexFile);
10031
+ fs21.unlinkSync(indexFile);
9218
10032
  } catch {
9219
10033
  }
9220
10034
  }
@@ -9226,14 +10040,14 @@ function getSnapshotHistory() {
9226
10040
  function computeUndoDiff(hash, cwd) {
9227
10041
  try {
9228
10042
  const env = buildGitEnv(cwd);
9229
- const statRes = spawnSync4("git", ["diff", hash, "--stat", "--", "."], {
10043
+ const statRes = spawnSync5("git", ["diff", hash, "--stat", "--", "."], {
9230
10044
  cwd,
9231
10045
  env,
9232
10046
  timeout: GIT_TIMEOUT
9233
10047
  });
9234
10048
  const stat = statRes.stdout?.toString().trim();
9235
10049
  if (!stat || statRes.status !== 0) return null;
9236
- const diffRes = spawnSync4("git", ["diff", hash, "--", "."], {
10050
+ const diffRes = spawnSync5("git", ["diff", hash, "--", "."], {
9237
10051
  cwd,
9238
10052
  env,
9239
10053
  timeout: GIT_TIMEOUT
@@ -9252,7 +10066,7 @@ function applyUndo(hash, cwd) {
9252
10066
  try {
9253
10067
  const dir = cwd ?? process.cwd();
9254
10068
  const env = buildGitEnv(dir);
9255
- const restore = spawnSync4("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
10069
+ const restore = spawnSync5("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
9256
10070
  cwd: dir,
9257
10071
  env,
9258
10072
  timeout: GIT_TIMEOUT
@@ -9264,7 +10078,7 @@ function applyUndo(hash, cwd) {
9264
10078
  }
9265
10079
  return false;
9266
10080
  }
9267
- const lsTree = spawnSync4("git", ["ls-tree", "-r", "--name-only", hash], {
10081
+ const lsTree = spawnSync5("git", ["ls-tree", "-r", "--name-only", hash], {
9268
10082
  cwd: dir,
9269
10083
  env,
9270
10084
  timeout: GIT_TIMEOUT
@@ -9283,16 +10097,16 @@ function applyUndo(hash, cwd) {
9283
10097
  `);
9284
10098
  return false;
9285
10099
  }
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"], {
10100
+ const tracked = spawnSync5("git", ["ls-files"], { cwd: dir, env, timeout: GIT_TIMEOUT }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
10101
+ const untracked = spawnSync5("git", ["ls-files", "--others", "--exclude-standard"], {
9288
10102
  cwd: dir,
9289
10103
  env,
9290
10104
  timeout: GIT_TIMEOUT
9291
10105
  }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
9292
10106
  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);
10107
+ const fullPath = path23.join(dir, file);
10108
+ if (!snapshotFiles.has(file) && fs21.existsSync(fullPath)) {
10109
+ fs21.unlinkSync(fullPath);
9296
10110
  }
9297
10111
  }
9298
10112
  return true;
@@ -9301,52 +10115,233 @@ function applyUndo(hash, cwd) {
9301
10115
  }
9302
10116
  }
9303
10117
 
9304
- // src/cli/commands/check.ts
9305
- function sanitize2(value) {
9306
- return value.replace(/[\x00-\x1F\x7F]/g, "");
9307
- }
9308
- function registerCheckCommand(program2) {
9309
- program2.command("check").description("Hook handler \u2014 evaluates a tool call before execution").argument("[data]", "JSON string of the tool call").action(async (data) => {
9310
- const processPayload = async (raw) => {
10118
+ // src/skill-pin.ts
10119
+ import fs22 from "fs";
10120
+ import path24 from "path";
10121
+ import os18 from "os";
10122
+ import crypto4 from "crypto";
10123
+ function getPinsFilePath() {
10124
+ return path24.join(os18.homedir(), ".node9", "skill-pins.json");
10125
+ }
10126
+ var MAX_FILES = 5e3;
10127
+ var MAX_TOTAL_BYTES = 50 * 1024 * 1024;
10128
+ function sha256Bytes(buf) {
10129
+ return crypto4.createHash("sha256").update(buf).digest("hex");
10130
+ }
10131
+ function walkDir(root) {
10132
+ const out = [];
10133
+ let totalBytes = 0;
10134
+ const visit = (dir, relDir) => {
10135
+ if (out.length >= MAX_FILES) return;
10136
+ let entries;
10137
+ try {
10138
+ entries = fs22.readdirSync(dir, { withFileTypes: true });
10139
+ } catch {
10140
+ return;
10141
+ }
10142
+ entries.sort((a, b) => a.name.localeCompare(b.name));
10143
+ for (const entry of entries) {
10144
+ if (out.length >= MAX_FILES) return;
10145
+ const full = path24.join(dir, entry.name);
10146
+ const rel = relDir ? path24.posix.join(relDir, entry.name) : entry.name;
10147
+ let lst;
9311
10148
  try {
9312
- if (!raw || raw.trim() === "") process.exit(0);
9313
- let payload = JSON.parse(raw);
9314
- try {
9315
- payload = JSON.parse(raw);
9316
- } catch (err2) {
9317
- const tempConfig = getConfig();
9318
- if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
9319
- const logPath = path22.join(os16.homedir(), ".node9", "hook-debug.log");
9320
- const errMsg = err2 instanceof Error ? err2.message : String(err2);
9321
- fs20.appendFileSync(
9322
- logPath,
9323
- `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
9324
- RAW: ${raw}
9325
- `
9326
- );
9327
- }
9328
- process.exit(0);
9329
- }
9330
- const config = getConfig(payload.cwd || void 0);
9331
- if (config.settings.autoStartDaemon && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON) {
9332
- try {
9333
- const scriptPath = process.argv[1];
9334
- if (typeof scriptPath !== "string" || !path22.isAbsolute(scriptPath))
9335
- 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)
9339
- throw new Error(
9340
- `node9: daemon spawn aborted \u2014 argv[1] (${resolvedScript}) is outside package dist (${packageDist})`
9341
- );
9342
- const safeEnv = { ...process.env };
9343
- for (const key of [
9344
- "NODE_OPTIONS",
9345
- "LD_PRELOAD",
9346
- "LD_LIBRARY_PATH",
9347
- "DYLD_INSERT_LIBRARIES",
9348
- "NODE_PATH",
9349
- "ELECTRON_RUN_AS_NODE"
10149
+ lst = fs22.lstatSync(full);
10150
+ } catch {
10151
+ continue;
10152
+ }
10153
+ if (lst.isSymbolicLink()) continue;
10154
+ if (lst.isDirectory()) {
10155
+ visit(full, rel);
10156
+ continue;
10157
+ }
10158
+ if (!lst.isFile()) continue;
10159
+ if (totalBytes + lst.size > MAX_TOTAL_BYTES) continue;
10160
+ try {
10161
+ const buf = fs22.readFileSync(full);
10162
+ totalBytes += buf.length;
10163
+ out.push({ rel, hash: sha256Bytes(buf) });
10164
+ } catch {
10165
+ }
10166
+ }
10167
+ };
10168
+ visit(root, "");
10169
+ out.sort((a, b) => a.rel.localeCompare(b.rel));
10170
+ return out.map((e) => `${e.rel}\0${e.hash}`);
10171
+ }
10172
+ function hashSkillRoot(absPath) {
10173
+ let lst;
10174
+ try {
10175
+ lst = fs22.lstatSync(absPath);
10176
+ } catch {
10177
+ return { exists: false, contentHash: "", fileCount: 0 };
10178
+ }
10179
+ if (lst.isSymbolicLink()) return { exists: false, contentHash: "", fileCount: 0 };
10180
+ if (lst.isFile()) {
10181
+ try {
10182
+ return { exists: true, contentHash: sha256Bytes(fs22.readFileSync(absPath)), fileCount: 1 };
10183
+ } catch {
10184
+ return { exists: false, contentHash: "", fileCount: 0 };
10185
+ }
10186
+ }
10187
+ if (lst.isDirectory()) {
10188
+ const entries = walkDir(absPath);
10189
+ const contentHash = crypto4.createHash("sha256").update(entries.join("\n")).digest("hex");
10190
+ return { exists: true, contentHash, fileCount: entries.length };
10191
+ }
10192
+ return { exists: false, contentHash: "", fileCount: 0 };
10193
+ }
10194
+ function getRootKey(absPath) {
10195
+ return crypto4.createHash("sha256").update(absPath).digest("hex").slice(0, 16);
10196
+ }
10197
+ function readSkillPinsSafe() {
10198
+ const filePath = getPinsFilePath();
10199
+ try {
10200
+ const raw = fs22.readFileSync(filePath, "utf-8");
10201
+ if (!raw.trim()) return { ok: false, reason: "corrupt", detail: "empty file" };
10202
+ const parsed = JSON.parse(raw);
10203
+ if (!parsed.roots || typeof parsed.roots !== "object" || Array.isArray(parsed.roots)) {
10204
+ return { ok: false, reason: "corrupt", detail: "invalid structure: missing roots object" };
10205
+ }
10206
+ return { ok: true, pins: { roots: parsed.roots } };
10207
+ } catch (err2) {
10208
+ if (err2.code === "ENOENT") return { ok: false, reason: "missing" };
10209
+ return { ok: false, reason: "corrupt", detail: String(err2) };
10210
+ }
10211
+ }
10212
+ function readSkillPins() {
10213
+ const result = readSkillPinsSafe();
10214
+ if (result.ok) return result.pins;
10215
+ if (result.reason === "missing") return { roots: {} };
10216
+ throw new Error(`[node9] skill pin file is corrupt: ${result.detail}`);
10217
+ }
10218
+ function writeSkillPins(data) {
10219
+ const filePath = getPinsFilePath();
10220
+ fs22.mkdirSync(path24.dirname(filePath), { recursive: true });
10221
+ const tmp = `${filePath}.${crypto4.randomBytes(6).toString("hex")}.tmp`;
10222
+ fs22.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
10223
+ fs22.renameSync(tmp, filePath);
10224
+ }
10225
+ function removePin(rootKey) {
10226
+ const pins = readSkillPins();
10227
+ delete pins.roots[rootKey];
10228
+ writeSkillPins(pins);
10229
+ }
10230
+ function clearAllPins() {
10231
+ writeSkillPins({ roots: {} });
10232
+ }
10233
+ function verifyAndPinRoots(roots) {
10234
+ const pinsRead = readSkillPinsSafe();
10235
+ if (!pinsRead.ok && pinsRead.reason === "corrupt") {
10236
+ return { kind: "corrupt", detail: pinsRead.detail };
10237
+ }
10238
+ const pins = pinsRead.ok ? pinsRead.pins : { roots: {} };
10239
+ let mutated = false;
10240
+ for (const rootPath of new Set(roots)) {
10241
+ const rootKey = getRootKey(rootPath);
10242
+ const current = hashSkillRoot(rootPath);
10243
+ const existing = pins.roots[rootKey];
10244
+ if (!existing) {
10245
+ pins.roots[rootKey] = {
10246
+ rootPath,
10247
+ exists: current.exists,
10248
+ contentHash: current.contentHash,
10249
+ fileCount: current.fileCount,
10250
+ pinnedAt: (/* @__PURE__ */ new Date()).toISOString()
10251
+ };
10252
+ mutated = true;
10253
+ continue;
10254
+ }
10255
+ if (existing.exists !== current.exists || existing.contentHash !== current.contentHash) {
10256
+ let summary;
10257
+ if (existing.exists && !current.exists) summary = `vanished: ${rootPath}`;
10258
+ else if (!existing.exists && current.exists) summary = `appeared: ${rootPath}`;
10259
+ else summary = `changed: ${rootPath}`;
10260
+ return { kind: "drift", changedRootKey: rootKey, changedRootPath: rootPath, summary };
10261
+ }
10262
+ }
10263
+ if (mutated) writeSkillPins(pins);
10264
+ return { kind: "verified" };
10265
+ }
10266
+ function defaultSkillRoots(_cwd) {
10267
+ const marketplaces = path24.join(os18.homedir(), ".claude", "plugins", "marketplaces");
10268
+ const roots = [];
10269
+ let registries;
10270
+ try {
10271
+ registries = fs22.readdirSync(marketplaces, { withFileTypes: true });
10272
+ } catch {
10273
+ return [];
10274
+ }
10275
+ for (const registry of registries) {
10276
+ if (!registry.isDirectory()) continue;
10277
+ const pluginsDir = path24.join(marketplaces, registry.name, "plugins");
10278
+ let plugins;
10279
+ try {
10280
+ plugins = fs22.readdirSync(pluginsDir, { withFileTypes: true });
10281
+ } catch {
10282
+ continue;
10283
+ }
10284
+ for (const plugin of plugins) {
10285
+ if (!plugin.isDirectory()) continue;
10286
+ roots.push(path24.join(pluginsDir, plugin.name));
10287
+ }
10288
+ }
10289
+ return roots;
10290
+ }
10291
+ function resolveUserSkillRoot(entry, cwd) {
10292
+ if (!entry) return null;
10293
+ if (entry.startsWith("~/") || entry === "~") return path24.join(os18.homedir(), entry.slice(1));
10294
+ if (path24.isAbsolute(entry)) return entry;
10295
+ if (!cwd || !path24.isAbsolute(cwd)) return null;
10296
+ return path24.join(cwd, entry);
10297
+ }
10298
+
10299
+ // src/cli/commands/check.ts
10300
+ function sanitize2(value) {
10301
+ return value.replace(/[\x00-\x1F\x7F]/g, "");
10302
+ }
10303
+ function registerCheckCommand(program2) {
10304
+ program2.command("check").description("Hook handler \u2014 evaluates a tool call before execution").argument("[data]", "JSON string of the tool call").action(async (data) => {
10305
+ const processPayload = async (raw) => {
10306
+ try {
10307
+ if (!raw || raw.trim() === "") process.exit(0);
10308
+ let payload = JSON.parse(raw);
10309
+ try {
10310
+ payload = JSON.parse(raw);
10311
+ } catch (err2) {
10312
+ const tempConfig = getConfig();
10313
+ if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
10314
+ const logPath = path25.join(os19.homedir(), ".node9", "hook-debug.log");
10315
+ const errMsg = err2 instanceof Error ? err2.message : String(err2);
10316
+ fs23.appendFileSync(
10317
+ logPath,
10318
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
10319
+ RAW: ${raw}
10320
+ `
10321
+ );
10322
+ }
10323
+ process.exit(0);
10324
+ }
10325
+ const config = getConfig(payload.cwd || void 0);
10326
+ if (config.settings.autoStartDaemon && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON) {
10327
+ try {
10328
+ const scriptPath = process.argv[1];
10329
+ if (typeof scriptPath !== "string" || !path25.isAbsolute(scriptPath))
10330
+ throw new Error("node9: argv[1] is not an absolute path");
10331
+ const resolvedScript = fs23.realpathSync(scriptPath);
10332
+ const packageDist = fs23.realpathSync(path25.resolve(__dirname, "../.."));
10333
+ if (!resolvedScript.startsWith(packageDist + path25.sep) && resolvedScript !== packageDist)
10334
+ throw new Error(
10335
+ `node9: daemon spawn aborted \u2014 argv[1] (${resolvedScript}) is outside package dist (${packageDist})`
10336
+ );
10337
+ const safeEnv = { ...process.env };
10338
+ for (const key of [
10339
+ "NODE_OPTIONS",
10340
+ "LD_PRELOAD",
10341
+ "LD_LIBRARY_PATH",
10342
+ "DYLD_INSERT_LIBRARIES",
10343
+ "NODE_PATH",
10344
+ "ELECTRON_RUN_AS_NODE"
9350
10345
  ]) {
9351
10346
  delete safeEnv[key];
9352
10347
  }
@@ -9357,10 +10352,10 @@ RAW: ${raw}
9357
10352
  });
9358
10353
  d.unref();
9359
10354
  } catch (spawnErr) {
9360
- const logPath = path22.join(os16.homedir(), ".node9", "hook-debug.log");
10355
+ const logPath = path25.join(os19.homedir(), ".node9", "hook-debug.log");
9361
10356
  const msg = spawnErr instanceof Error ? spawnErr.message : String(spawnErr);
9362
10357
  try {
9363
- fs20.appendFileSync(
10358
+ fs23.appendFileSync(
9364
10359
  logPath,
9365
10360
  `[${(/* @__PURE__ */ new Date()).toISOString()}] daemon-autostart-failed: ${msg}
9366
10361
  `
@@ -9370,10 +10365,10 @@ RAW: ${raw}
9370
10365
  }
9371
10366
  }
9372
10367
  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}
10368
+ const logPath = path25.join(os19.homedir(), ".node9", "hook-debug.log");
10369
+ if (!fs23.existsSync(path25.dirname(logPath)))
10370
+ fs23.mkdirSync(path25.dirname(logPath), { recursive: true });
10371
+ fs23.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
9377
10372
  `);
9378
10373
  }
9379
10374
  const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
@@ -9386,8 +10381,8 @@ RAW: ${raw}
9386
10381
  const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
9387
10382
  let ttyFd = null;
9388
10383
  try {
9389
- ttyFd = fs20.openSync("/dev/tty", "w");
9390
- const writeTty = (line) => fs20.writeSync(ttyFd, line + "\n");
10384
+ ttyFd = fs23.openSync("/dev/tty", "w");
10385
+ const writeTty = (line) => fs23.writeSync(ttyFd, line + "\n");
9391
10386
  if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
9392
10387
  writeTty(chalk5.bgRed.white.bold(`
9393
10388
  \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
@@ -9406,7 +10401,7 @@ RAW: ${raw}
9406
10401
  } finally {
9407
10402
  if (ttyFd !== null)
9408
10403
  try {
9409
- fs20.closeSync(ttyFd);
10404
+ fs23.closeSync(ttyFd);
9410
10405
  } catch {
9411
10406
  }
9412
10407
  }
@@ -9435,10 +10430,131 @@ RAW: ${raw}
9435
10430
  return;
9436
10431
  }
9437
10432
  const meta = { agent, mcpServer };
10433
+ const skillPinCfg = config.policy.skillPinning;
10434
+ const rawSessionId = typeof payload.session_id === "string" ? payload.session_id : "";
10435
+ const safeSessionId = /^[A-Za-z0-9_\-]{1,128}$/.test(rawSessionId) ? rawSessionId : "";
10436
+ if (skillPinCfg.enabled && safeSessionId) {
10437
+ try {
10438
+ const sessionsDir = path25.join(os19.homedir(), ".node9", "skill-sessions");
10439
+ const flagPath = path25.join(sessionsDir, `${safeSessionId}.json`);
10440
+ let flag = null;
10441
+ try {
10442
+ flag = JSON.parse(fs23.readFileSync(flagPath, "utf-8"));
10443
+ } catch {
10444
+ }
10445
+ const writeFlag = (data2) => {
10446
+ try {
10447
+ fs23.mkdirSync(sessionsDir, { recursive: true });
10448
+ fs23.writeFileSync(
10449
+ flagPath,
10450
+ JSON.stringify({ ...data2, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
10451
+ { mode: 384 }
10452
+ );
10453
+ } catch {
10454
+ }
10455
+ };
10456
+ const sendSkillWarn = (detail, recoveryCmd) => {
10457
+ let ttyFd = null;
10458
+ try {
10459
+ ttyFd = fs23.openSync("/dev/tty", "w");
10460
+ const w = (line) => fs23.writeSync(ttyFd, line + "\n");
10461
+ w(chalk5.yellow(`
10462
+ \u26A0\uFE0F Node9: installed skill drift detected`));
10463
+ w(chalk5.gray(` ${detail}`));
10464
+ w(
10465
+ chalk5.gray(
10466
+ ` If you updated a plugin, acknowledge the change to clear this warning.`
10467
+ )
10468
+ );
10469
+ if (recoveryCmd) w(chalk5.green(` \u{1F4A1} Run: ${recoveryCmd}`));
10470
+ w("");
10471
+ } catch {
10472
+ } finally {
10473
+ if (ttyFd !== null)
10474
+ try {
10475
+ fs23.closeSync(ttyFd);
10476
+ } catch {
10477
+ }
10478
+ }
10479
+ };
10480
+ if (flag && flag.state === "quarantined" && skillPinCfg.mode === "block") {
10481
+ sendBlock(
10482
+ `Node9: session quarantined \u2014 installed skill changed. Open a separate terminal and run: node9 skill pin list (to see what changed) then: node9 skill pin update <rootKey> (to acknowledge). If you updated a plugin intentionally, this is expected.`,
10483
+ {
10484
+ blockedByLabel: "Skill Pin Quarantine",
10485
+ recoveryCommand: "node9 skill pin list"
10486
+ }
10487
+ );
10488
+ return;
10489
+ }
10490
+ if (!flag || flag.state !== "verified" && flag.state !== "warned") {
10491
+ const absoluteCwd = typeof payload.cwd === "string" && path25.isAbsolute(payload.cwd) ? payload.cwd : void 0;
10492
+ const extraRoots = skillPinCfg.roots;
10493
+ const resolvedExtra = extraRoots.map((r) => resolveUserSkillRoot(r, absoluteCwd)).filter((r) => typeof r === "string");
10494
+ const roots = [...defaultSkillRoots(absoluteCwd), ...resolvedExtra];
10495
+ const result2 = verifyAndPinRoots(roots);
10496
+ if (result2.kind === "corrupt") {
10497
+ if (skillPinCfg.mode === "block") {
10498
+ writeFlag({
10499
+ state: "quarantined",
10500
+ detail: `pin file corrupt: ${result2.detail}`
10501
+ });
10502
+ sendBlock("Node9: skill pin file is corrupt \u2014 fail-closed.", {
10503
+ blockedByLabel: "Skill Pin Quarantine",
10504
+ recoveryCommand: "node9 skill pin reset"
10505
+ });
10506
+ return;
10507
+ }
10508
+ writeFlag({ state: "warned", detail: `pin file corrupt: ${result2.detail}` });
10509
+ sendSkillWarn(
10510
+ `Skill pin file is corrupt: ${result2.detail}`,
10511
+ "node9 skill pin reset"
10512
+ );
10513
+ } else if (result2.kind === "drift") {
10514
+ if (skillPinCfg.mode === "block") {
10515
+ writeFlag({ state: "quarantined", detail: result2.summary });
10516
+ sendBlock(
10517
+ `Node9: installed skill changed \u2014 ${result2.summary}. If you updated a plugin, open a separate terminal and run: node9 skill pin update ${result2.changedRootKey}`,
10518
+ {
10519
+ blockedByLabel: "Skill Pin Quarantine",
10520
+ recoveryCommand: `node9 skill pin update ${result2.changedRootKey}`
10521
+ }
10522
+ );
10523
+ return;
10524
+ }
10525
+ writeFlag({ state: "warned", detail: result2.summary });
10526
+ sendSkillWarn(result2.summary, `node9 skill pin update ${result2.changedRootKey}`);
10527
+ } else {
10528
+ writeFlag({ state: "verified" });
10529
+ }
10530
+ try {
10531
+ const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1e3;
10532
+ for (const name of fs23.readdirSync(sessionsDir)) {
10533
+ const p = path25.join(sessionsDir, name);
10534
+ try {
10535
+ if (fs23.statSync(p).mtimeMs < cutoff) fs23.unlinkSync(p);
10536
+ } catch {
10537
+ }
10538
+ }
10539
+ } catch {
10540
+ }
10541
+ }
10542
+ } catch (err2) {
10543
+ if (process.env.NODE9_DEBUG === "1") {
10544
+ try {
10545
+ const dbg = path25.join(os19.homedir(), ".node9", "hook-debug.log");
10546
+ const msg = err2 instanceof Error ? err2.message : String(err2);
10547
+ fs23.appendFileSync(dbg, `[${(/* @__PURE__ */ new Date()).toISOString()}] SKILL_PIN_ERROR: ${msg}
10548
+ `);
10549
+ } catch {
10550
+ }
10551
+ }
10552
+ }
10553
+ }
9438
10554
  if (shouldSnapshot(toolName, toolInput, config)) {
9439
10555
  await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
9440
10556
  }
9441
- const safeCwdForAuth = typeof payload.cwd === "string" && path22.isAbsolute(payload.cwd) ? payload.cwd : void 0;
10557
+ const safeCwdForAuth = typeof payload.cwd === "string" && path25.isAbsolute(payload.cwd) ? payload.cwd : void 0;
9442
10558
  const result = await authorizeHeadless(toolName, toolInput, meta, {
9443
10559
  cwd: safeCwdForAuth
9444
10560
  });
@@ -9450,12 +10566,12 @@ RAW: ${raw}
9450
10566
  }
9451
10567
  if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
9452
10568
  try {
9453
- const tty = fs20.openSync("/dev/tty", "w");
9454
- fs20.writeSync(
10569
+ const tty = fs23.openSync("/dev/tty", "w");
10570
+ fs23.writeSync(
9455
10571
  tty,
9456
10572
  chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
9457
10573
  );
9458
- fs20.closeSync(tty);
10574
+ fs23.closeSync(tty);
9459
10575
  } catch {
9460
10576
  }
9461
10577
  const daemonReady = await autoStartDaemonAndWait();
@@ -9482,9 +10598,9 @@ RAW: ${raw}
9482
10598
  });
9483
10599
  } catch (err2) {
9484
10600
  if (process.env.NODE9_DEBUG === "1") {
9485
- const logPath = path22.join(os16.homedir(), ".node9", "hook-debug.log");
10601
+ const logPath = path25.join(os19.homedir(), ".node9", "hook-debug.log");
9486
10602
  const errMsg = err2 instanceof Error ? err2.message : String(err2);
9487
- fs20.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
10603
+ fs23.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
9488
10604
  `);
9489
10605
  }
9490
10606
  process.exit(0);
@@ -9521,9 +10637,9 @@ RAW: ${raw}
9521
10637
  init_audit();
9522
10638
  init_config();
9523
10639
  init_policy();
9524
- import fs21 from "fs";
9525
- import path23 from "path";
9526
- import os17 from "os";
10640
+ import fs24 from "fs";
10641
+ import path26 from "path";
10642
+ import os20 from "os";
9527
10643
  init_daemon();
9528
10644
 
9529
10645
  // src/utils/cp-mv-parser.ts
@@ -9596,10 +10712,10 @@ function registerLogCommand(program2) {
9596
10712
  decision: "allowed",
9597
10713
  source: "post-hook"
9598
10714
  };
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");
10715
+ const logPath = path26.join(os20.homedir(), ".node9", "audit.log");
10716
+ if (!fs24.existsSync(path26.dirname(logPath)))
10717
+ fs24.mkdirSync(path26.dirname(logPath), { recursive: true });
10718
+ fs24.appendFileSync(logPath, JSON.stringify(entry) + "\n");
9603
10719
  if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
9604
10720
  const command = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
9605
10721
  if (command) {
@@ -9632,7 +10748,7 @@ function registerLogCommand(program2) {
9632
10748
  }
9633
10749
  }
9634
10750
  }
9635
- const safeCwd = typeof payload.cwd === "string" && path23.isAbsolute(payload.cwd) ? payload.cwd : void 0;
10751
+ const safeCwd = typeof payload.cwd === "string" && path26.isAbsolute(payload.cwd) ? payload.cwd : void 0;
9636
10752
  const config = getConfig(safeCwd);
9637
10753
  if (shouldSnapshot(tool, {}, config)) {
9638
10754
  await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
@@ -9641,9 +10757,9 @@ function registerLogCommand(program2) {
9641
10757
  const msg = err2 instanceof Error ? err2.message : String(err2);
9642
10758
  process.stderr.write(`[Node9] audit log error: ${msg}
9643
10759
  `);
9644
- const debugPath = path23.join(os17.homedir(), ".node9", "hook-debug.log");
10760
+ const debugPath = path26.join(os20.homedir(), ".node9", "hook-debug.log");
9645
10761
  try {
9646
- fs21.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
10762
+ fs24.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
9647
10763
  `);
9648
10764
  } catch {
9649
10765
  }
@@ -9673,10 +10789,10 @@ init_config();
9673
10789
  import chalk6 from "chalk";
9674
10790
 
9675
10791
  // src/utils/https-fetch.ts
9676
- import https from "https";
10792
+ import https2 from "https";
9677
10793
  function httpsFetch(url) {
9678
10794
  return new Promise((resolve, reject) => {
9679
- https.get(url, (res) => {
10795
+ https2.get(url, (res) => {
9680
10796
  if (res.statusCode !== 200) {
9681
10797
  reject(new Error(`HTTP ${String(res.statusCode)} for ${url}`));
9682
10798
  res.resume();
@@ -10044,13 +11160,13 @@ function registerConfigShowCommand(program2) {
10044
11160
  // src/cli/commands/doctor.ts
10045
11161
  init_daemon();
10046
11162
  import chalk7 from "chalk";
10047
- import fs22 from "fs";
10048
- import path24 from "path";
10049
- import os18 from "os";
11163
+ import fs25 from "fs";
11164
+ import path27 from "path";
11165
+ import os21 from "os";
10050
11166
  import { execSync as execSync2 } from "child_process";
10051
11167
  function registerDoctorCommand(program2, version2) {
10052
11168
  program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
10053
- const homeDir2 = os18.homedir();
11169
+ const homeDir2 = os21.homedir();
10054
11170
  let failures = 0;
10055
11171
  function pass(msg) {
10056
11172
  console.log(chalk7.green(" \u2705 ") + msg);
@@ -10099,10 +11215,10 @@ function registerDoctorCommand(program2, version2) {
10099
11215
  );
10100
11216
  }
10101
11217
  section("Configuration");
10102
- const globalConfigPath = path24.join(homeDir2, ".node9", "config.json");
10103
- if (fs22.existsSync(globalConfigPath)) {
11218
+ const globalConfigPath = path27.join(homeDir2, ".node9", "config.json");
11219
+ if (fs25.existsSync(globalConfigPath)) {
10104
11220
  try {
10105
- JSON.parse(fs22.readFileSync(globalConfigPath, "utf-8"));
11221
+ JSON.parse(fs25.readFileSync(globalConfigPath, "utf-8"));
10106
11222
  pass("~/.node9/config.json found and valid");
10107
11223
  } catch {
10108
11224
  fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
@@ -10110,10 +11226,10 @@ function registerDoctorCommand(program2, version2) {
10110
11226
  } else {
10111
11227
  warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
10112
11228
  }
10113
- const projectConfigPath = path24.join(process.cwd(), "node9.config.json");
10114
- if (fs22.existsSync(projectConfigPath)) {
11229
+ const projectConfigPath = path27.join(process.cwd(), "node9.config.json");
11230
+ if (fs25.existsSync(projectConfigPath)) {
10115
11231
  try {
10116
- JSON.parse(fs22.readFileSync(projectConfigPath, "utf-8"));
11232
+ JSON.parse(fs25.readFileSync(projectConfigPath, "utf-8"));
10117
11233
  pass("node9.config.json found and valid (project)");
10118
11234
  } catch {
10119
11235
  fail(
@@ -10122,8 +11238,8 @@ function registerDoctorCommand(program2, version2) {
10122
11238
  );
10123
11239
  }
10124
11240
  }
10125
- const credsPath = path24.join(homeDir2, ".node9", "credentials.json");
10126
- if (fs22.existsSync(credsPath)) {
11241
+ const credsPath = path27.join(homeDir2, ".node9", "credentials.json");
11242
+ if (fs25.existsSync(credsPath)) {
10127
11243
  pass("Cloud credentials found (~/.node9/credentials.json)");
10128
11244
  } else {
10129
11245
  warn(
@@ -10132,10 +11248,10 @@ function registerDoctorCommand(program2, version2) {
10132
11248
  );
10133
11249
  }
10134
11250
  section("Agent Hooks");
10135
- const claudeSettingsPath = path24.join(homeDir2, ".claude", "settings.json");
10136
- if (fs22.existsSync(claudeSettingsPath)) {
11251
+ const claudeSettingsPath = path27.join(homeDir2, ".claude", "settings.json");
11252
+ if (fs25.existsSync(claudeSettingsPath)) {
10137
11253
  try {
10138
- const cs = JSON.parse(fs22.readFileSync(claudeSettingsPath, "utf-8"));
11254
+ const cs = JSON.parse(fs25.readFileSync(claudeSettingsPath, "utf-8"));
10139
11255
  const hasHook = cs.hooks?.PreToolUse?.some(
10140
11256
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
10141
11257
  );
@@ -10151,10 +11267,10 @@ function registerDoctorCommand(program2, version2) {
10151
11267
  } else {
10152
11268
  warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
10153
11269
  }
10154
- const geminiSettingsPath = path24.join(homeDir2, ".gemini", "settings.json");
10155
- if (fs22.existsSync(geminiSettingsPath)) {
11270
+ const geminiSettingsPath = path27.join(homeDir2, ".gemini", "settings.json");
11271
+ if (fs25.existsSync(geminiSettingsPath)) {
10156
11272
  try {
10157
- const gs = JSON.parse(fs22.readFileSync(geminiSettingsPath, "utf-8"));
11273
+ const gs = JSON.parse(fs25.readFileSync(geminiSettingsPath, "utf-8"));
10158
11274
  const hasHook = gs.hooks?.BeforeTool?.some(
10159
11275
  (m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
10160
11276
  );
@@ -10170,10 +11286,10 @@ function registerDoctorCommand(program2, version2) {
10170
11286
  } else {
10171
11287
  warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
10172
11288
  }
10173
- const cursorHooksPath = path24.join(homeDir2, ".cursor", "hooks.json");
10174
- if (fs22.existsSync(cursorHooksPath)) {
11289
+ const cursorHooksPath = path27.join(homeDir2, ".cursor", "hooks.json");
11290
+ if (fs25.existsSync(cursorHooksPath)) {
10175
11291
  try {
10176
- const cur = JSON.parse(fs22.readFileSync(cursorHooksPath, "utf-8"));
11292
+ const cur = JSON.parse(fs25.readFileSync(cursorHooksPath, "utf-8"));
10177
11293
  const hasHook = cur.hooks?.preToolUse?.some(
10178
11294
  (h) => h.command?.includes("node9") || h.command?.includes("cli.js")
10179
11295
  );
@@ -10211,9 +11327,9 @@ function registerDoctorCommand(program2, version2) {
10211
11327
 
10212
11328
  // src/cli/commands/audit.ts
10213
11329
  import chalk8 from "chalk";
10214
- import fs23 from "fs";
10215
- import path25 from "path";
10216
- import os19 from "os";
11330
+ import fs26 from "fs";
11331
+ import path28 from "path";
11332
+ import os22 from "os";
10217
11333
  function formatRelativeTime(timestamp) {
10218
11334
  const diff = Date.now() - new Date(timestamp).getTime();
10219
11335
  const sec = Math.floor(diff / 1e3);
@@ -10226,14 +11342,14 @@ function formatRelativeTime(timestamp) {
10226
11342
  }
10227
11343
  function registerAuditCommand(program2) {
10228
11344
  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)) {
11345
+ const logPath = path28.join(os22.homedir(), ".node9", "audit.log");
11346
+ if (!fs26.existsSync(logPath)) {
10231
11347
  console.log(
10232
11348
  chalk8.yellow("No audit logs found. Run node9 with an agent to generate entries.")
10233
11349
  );
10234
11350
  return;
10235
11351
  }
10236
- const raw = fs23.readFileSync(logPath, "utf-8");
11352
+ const raw = fs26.readFileSync(logPath, "utf-8");
10237
11353
  const lines = raw.split("\n").filter((l) => l.trim() !== "");
10238
11354
  let entries = lines.flatMap((line) => {
10239
11355
  try {
@@ -10287,9 +11403,9 @@ function registerAuditCommand(program2) {
10287
11403
 
10288
11404
  // src/cli/commands/report.ts
10289
11405
  import chalk9 from "chalk";
10290
- import fs24 from "fs";
10291
- import path26 from "path";
10292
- import os20 from "os";
11406
+ import fs27 from "fs";
11407
+ import path29 from "path";
11408
+ import os23 from "os";
10293
11409
  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
11410
  function buildTestTimestamps(allEntries) {
10295
11411
  const testTs = /* @__PURE__ */ new Set();
@@ -10336,8 +11452,8 @@ function getDateRange(period) {
10336
11452
  }
10337
11453
  }
10338
11454
  function parseAuditLog(logPath) {
10339
- if (!fs24.existsSync(logPath)) return [];
10340
- const raw = fs24.readFileSync(logPath, "utf-8");
11455
+ if (!fs27.existsSync(logPath)) return [];
11456
+ const raw = fs27.readFileSync(logPath, "utf-8");
10341
11457
  return raw.split("\n").flatMap((line) => {
10342
11458
  if (!line.trim()) return [];
10343
11459
  try {
@@ -10363,9 +11479,9 @@ function colorBar(value, max, width) {
10363
11479
  const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
10364
11480
  return chalk9.cyan(s.slice(0, filled)) + chalk9.dim(s.slice(filled));
10365
11481
  }
10366
- function pct(num2, total) {
11482
+ function pct(num3, total) {
10367
11483
  if (total === 0) return "\u2013";
10368
- return Math.round(num2 / total * 100) + "%";
11484
+ return Math.round(num3 / total * 100) + "%";
10369
11485
  }
10370
11486
  function fmtDate(d) {
10371
11487
  const date = typeof d === "string" ? /* @__PURE__ */ new Date(d + "T12:00:00") : d;
@@ -10406,11 +11522,11 @@ function loadClaudeCost(start, end) {
10406
11522
  inputTokens: 0,
10407
11523
  cacheReadTokens: 0
10408
11524
  };
10409
- const projectsDir = path26.join(os20.homedir(), ".claude", "projects");
10410
- if (!fs24.existsSync(projectsDir)) return empty;
11525
+ const projectsDir = path29.join(os23.homedir(), ".claude", "projects");
11526
+ if (!fs27.existsSync(projectsDir)) return empty;
10411
11527
  let dirs;
10412
11528
  try {
10413
- dirs = fs24.readdirSync(projectsDir);
11529
+ dirs = fs27.readdirSync(projectsDir);
10414
11530
  } catch {
10415
11531
  return empty;
10416
11532
  }
@@ -10420,18 +11536,18 @@ function loadClaudeCost(start, end) {
10420
11536
  const byDay = /* @__PURE__ */ new Map();
10421
11537
  const byModel = /* @__PURE__ */ new Map();
10422
11538
  for (const proj of dirs) {
10423
- const projPath = path26.join(projectsDir, proj);
11539
+ const projPath = path29.join(projectsDir, proj);
10424
11540
  let files;
10425
11541
  try {
10426
- const stat = fs24.statSync(projPath);
11542
+ const stat = fs27.statSync(projPath);
10427
11543
  if (!stat.isDirectory()) continue;
10428
- files = fs24.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
11544
+ files = fs27.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
10429
11545
  } catch {
10430
11546
  continue;
10431
11547
  }
10432
11548
  for (const file of files) {
10433
11549
  try {
10434
- const raw = fs24.readFileSync(path26.join(projPath, file), "utf-8");
11550
+ const raw = fs27.readFileSync(path29.join(projPath, file), "utf-8");
10435
11551
  for (const line of raw.split("\n")) {
10436
11552
  if (!line.trim()) continue;
10437
11553
  let entry;
@@ -10474,7 +11590,7 @@ function registerReportCommand(program2) {
10474
11590
  const period = ["today", "7d", "30d", "month"].includes(
10475
11591
  options.period
10476
11592
  ) ? options.period : "7d";
10477
- const logPath = path26.join(os20.homedir(), ".node9", "audit.log");
11593
+ const logPath = path29.join(os23.homedir(), ".node9", "audit.log");
10478
11594
  const allEntries = parseAuditLog(logPath);
10479
11595
  if (allEntries.length === 0) {
10480
11596
  console.log(
@@ -10725,19 +11841,60 @@ init_daemon2();
10725
11841
  init_daemon();
10726
11842
  import chalk10 from "chalk";
10727
11843
  import { spawn as spawn7 } from "child_process";
11844
+ var VALID_ACTIONS = "start | stop | restart | status | install | uninstall";
10728
11845
  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(
11846
+ 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
11847
  "-w, --watch",
10731
11848
  "Start daemon + open browser, stay alive permanently (Flight Recorder mode)"
10732
11849
  ).action(
10733
11850
  async (action, options) => {
10734
11851
  const cmd = (action ?? "start").toLowerCase();
11852
+ if (cmd === "install") {
11853
+ const result = installDaemonService();
11854
+ if (!result.ok) {
11855
+ console.error(chalk10.red(`\u2717 ${result.reason}`));
11856
+ process.exit(1);
11857
+ }
11858
+ if (result.alreadyInstalled) {
11859
+ console.log(chalk10.green(`\u2713 Daemon service reinstalled (${result.platform})`));
11860
+ } else {
11861
+ console.log(chalk10.green(`\u2713 Daemon installed as login service (${result.platform})`));
11862
+ console.log(chalk10.gray(" The daemon will now start automatically on login."));
11863
+ }
11864
+ process.exit(0);
11865
+ }
11866
+ if (cmd === "uninstall") {
11867
+ const result = uninstallDaemonService();
11868
+ if (!result.ok) {
11869
+ console.error(chalk10.red(`\u2717 ${result.reason}`));
11870
+ process.exit(1);
11871
+ }
11872
+ console.log(chalk10.green(`\u2713 Daemon service removed (${result.platform})`));
11873
+ console.log(chalk10.gray(" The daemon will no longer start automatically on login."));
11874
+ console.log(chalk10.gray(" To stop the running daemon: node9 daemon stop"));
11875
+ process.exit(0);
11876
+ }
10735
11877
  if (cmd === "stop") return stopDaemon();
11878
+ if (cmd === "restart") {
11879
+ stopDaemon();
11880
+ await new Promise((r) => setTimeout(r, 500));
11881
+ const child = spawn7(process.execPath, [process.argv[1], "daemon"], {
11882
+ detached: true,
11883
+ stdio: "ignore",
11884
+ env: { ...process.env, NODE9_AUTO_STARTED: "1", NODE9_BROWSER_OPENED: "1" }
11885
+ });
11886
+ child.unref();
11887
+ if (child.pid) {
11888
+ console.log(chalk10.green(`\u2713 Daemon restarted (PID ${child.pid})`));
11889
+ } else {
11890
+ console.error(chalk10.red("\u2717 Failed to restart daemon \u2014 spawn returned no PID"));
11891
+ process.exit(1);
11892
+ }
11893
+ process.exit(0);
11894
+ }
10736
11895
  if (cmd === "status") return daemonStatus();
10737
11896
  if (cmd !== "start" && action !== void 0) {
10738
- console.error(
10739
- chalk10.red(`Unknown daemon action: "${action}". Use: start | stop | status`)
10740
- );
11897
+ console.error(chalk10.red(`Unknown daemon action: "${action}". Use: ${VALID_ACTIONS}`));
10741
11898
  process.exit(1);
10742
11899
  }
10743
11900
  if (options.watch) {
@@ -10788,12 +11945,12 @@ function registerDaemonCommand(program2) {
10788
11945
  init_core();
10789
11946
  init_daemon();
10790
11947
  import chalk11 from "chalk";
10791
- import fs25 from "fs";
10792
- import path27 from "path";
10793
- import os21 from "os";
11948
+ import fs28 from "fs";
11949
+ import path30 from "path";
11950
+ import os24 from "os";
10794
11951
  function readJson2(filePath) {
10795
11952
  try {
10796
- if (fs25.existsSync(filePath)) return JSON.parse(fs25.readFileSync(filePath, "utf-8"));
11953
+ if (fs28.existsSync(filePath)) return JSON.parse(fs28.readFileSync(filePath, "utf-8"));
10797
11954
  } catch {
10798
11955
  }
10799
11956
  return null;
@@ -10858,28 +12015,28 @@ function registerStatusCommand(program2) {
10858
12015
  console.log("");
10859
12016
  const modeLabel = settings.mode === "audit" ? chalk11.blue("audit") : settings.mode === "strict" ? chalk11.red("strict") : chalk11.white("standard");
10860
12017
  console.log(` Mode: ${modeLabel}`);
10861
- const projectConfig = path27.join(process.cwd(), "node9.config.json");
10862
- const globalConfig = path27.join(os21.homedir(), ".node9", "config.json");
12018
+ const projectConfig = path30.join(process.cwd(), "node9.config.json");
12019
+ const globalConfig = path30.join(os24.homedir(), ".node9", "config.json");
10863
12020
  console.log(
10864
- ` Local: ${fs25.existsSync(projectConfig) ? chalk11.green("Active (node9.config.json)") : chalk11.gray("Not present")}`
12021
+ ` Local: ${fs28.existsSync(projectConfig) ? chalk11.green("Active (node9.config.json)") : chalk11.gray("Not present")}`
10865
12022
  );
10866
12023
  console.log(
10867
- ` Global: ${fs25.existsSync(globalConfig) ? chalk11.green("Active (~/.node9/config.json)") : chalk11.gray("Not present")}`
12024
+ ` Global: ${fs28.existsSync(globalConfig) ? chalk11.green("Active (~/.node9/config.json)") : chalk11.gray("Not present")}`
10868
12025
  );
10869
12026
  if (mergedConfig.policy.sandboxPaths.length > 0) {
10870
12027
  console.log(
10871
12028
  ` Sandbox: ${chalk11.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
10872
12029
  );
10873
12030
  }
10874
- const homeDir2 = os21.homedir();
12031
+ const homeDir2 = os24.homedir();
10875
12032
  const claudeSettings = readJson2(
10876
- path27.join(homeDir2, ".claude", "settings.json")
12033
+ path30.join(homeDir2, ".claude", "settings.json")
10877
12034
  );
10878
- const claudeConfig = readJson2(path27.join(homeDir2, ".claude.json"));
12035
+ const claudeConfig = readJson2(path30.join(homeDir2, ".claude.json"));
10879
12036
  const geminiSettings = readJson2(
10880
- path27.join(homeDir2, ".gemini", "settings.json")
12037
+ path30.join(homeDir2, ".gemini", "settings.json")
10881
12038
  );
10882
- const cursorConfig = readJson2(path27.join(homeDir2, ".cursor", "mcp.json"));
12039
+ const cursorConfig = readJson2(path30.join(homeDir2, ".cursor", "mcp.json"));
10883
12040
  const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
10884
12041
  if (agentFound) {
10885
12042
  console.log("");
@@ -10939,11 +12096,12 @@ function registerStatusCommand(program2) {
10939
12096
  // src/cli/commands/init.ts
10940
12097
  init_core();
10941
12098
  import chalk12 from "chalk";
10942
- import fs26 from "fs";
10943
- import path28 from "path";
10944
- import os22 from "os";
10945
- import https2 from "https";
12099
+ import fs29 from "fs";
12100
+ import path31 from "path";
12101
+ import os25 from "os";
12102
+ import https3 from "https";
10946
12103
  init_shields();
12104
+ init_service();
10947
12105
  var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
10948
12106
  function fireTelemetryPing(agents) {
10949
12107
  try {
@@ -10953,7 +12111,7 @@ function fireTelemetryPing(agents) {
10953
12111
  os: process.platform,
10954
12112
  node9_version: process.env.npm_package_version ?? "unknown"
10955
12113
  });
10956
- const req = https2.request(
12114
+ const req = https3.request(
10957
12115
  {
10958
12116
  hostname: "api.node9.ai",
10959
12117
  path: "/api/v1/telemetry",
@@ -11000,15 +12158,15 @@ function registerInitCommand(program2) {
11000
12158
  }
11001
12159
  console.log("");
11002
12160
  }
11003
- const configPath = path28.join(os22.homedir(), ".node9", "config.json");
11004
- if (fs26.existsSync(configPath) && !options.force) {
12161
+ const configPath = path31.join(os25.homedir(), ".node9", "config.json");
12162
+ if (fs29.existsSync(configPath) && !options.force) {
11005
12163
  try {
11006
- const existing = JSON.parse(fs26.readFileSync(configPath, "utf-8"));
12164
+ const existing = JSON.parse(fs29.readFileSync(configPath, "utf-8"));
11007
12165
  const settings = existing.settings ?? {};
11008
12166
  if (settings.mode !== chosenMode) {
11009
12167
  settings.mode = chosenMode;
11010
12168
  existing.settings = settings;
11011
- fs26.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
12169
+ fs29.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
11012
12170
  console.log(chalk12.green(`\u2705 Mode updated: ${chosenMode}`));
11013
12171
  } else {
11014
12172
  console.log(chalk12.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
@@ -11021,9 +12179,9 @@ function registerInitCommand(program2) {
11021
12179
  ...DEFAULT_CONFIG,
11022
12180
  settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
11023
12181
  };
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");
12182
+ const dir = path31.dirname(configPath);
12183
+ if (!fs29.existsSync(dir)) fs29.mkdirSync(dir, { recursive: true });
12184
+ fs29.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
11027
12185
  console.log(chalk12.green(`\u2705 Config created: ${configPath}`));
11028
12186
  console.log(chalk12.gray(` Mode: ${chosenMode}`));
11029
12187
  }
@@ -11035,9 +12193,13 @@ function registerInitCommand(program2) {
11035
12193
  );
11036
12194
  if (found.length === 0) {
11037
12195
  console.log(
11038
- chalk12.gray("No AI agents detected. Install Claude Code, Gemini CLI, Cursor, or Codex")
12196
+ chalk12.gray(
12197
+ "No AI agents detected. Install Claude Code, Gemini CLI, Cursor, Windsurf, VSCode, or Codex"
12198
+ )
12199
+ );
12200
+ console.log(
12201
+ chalk12.gray("then run: node9 agents add <claude|gemini|cursor|windsurf|vscode|codex>")
11039
12202
  );
11040
- console.log(chalk12.gray("then run: node9 addto <claude|gemini|cursor|codex>"));
11041
12203
  return;
11042
12204
  }
11043
12205
  console.log(chalk12.bold("Detected agents:"));
@@ -11051,6 +12213,32 @@ function registerInitCommand(program2) {
11051
12213
  else if (agent === "gemini") await setupGemini();
11052
12214
  else if (agent === "cursor") await setupCursor();
11053
12215
  else if (agent === "codex") await setupCodex();
12216
+ else if (agent === "windsurf") await setupWindsurf();
12217
+ else if (agent === "vscode") await setupVSCode();
12218
+ console.log("");
12219
+ }
12220
+ if ((process.platform === "darwin" || process.platform === "linux") && process.stdout.isTTY) {
12221
+ const alreadyInstalled = isDaemonServiceInstalled();
12222
+ if (!alreadyInstalled) {
12223
+ const { confirm: confirm3 } = await import("@inquirer/prompts");
12224
+ const installService = await confirm3({
12225
+ message: "Install daemon as a login service? (starts automatically on login)",
12226
+ default: true
12227
+ });
12228
+ if (installService) {
12229
+ const result = installDaemonService();
12230
+ if (result.ok) {
12231
+ console.log(
12232
+ chalk12.green(` \u2713 Daemon installed as login service (${result.platform})`)
12233
+ );
12234
+ } else {
12235
+ console.log(chalk12.yellow(` \u26A0\uFE0F Could not install service: ${result.reason}`));
12236
+ console.log(chalk12.gray(" You can try again later with: node9 daemon install"));
12237
+ }
12238
+ }
12239
+ } else {
12240
+ console.log(chalk12.green(" \u2713 Daemon login service already installed"));
12241
+ }
11054
12242
  console.log("");
11055
12243
  }
11056
12244
  {
@@ -11077,7 +12265,7 @@ function registerInitCommand(program2) {
11077
12265
  }
11078
12266
 
11079
12267
  // src/cli/commands/undo.ts
11080
- import path29 from "path";
12268
+ import path32 from "path";
11081
12269
  import chalk14 from "chalk";
11082
12270
 
11083
12271
  // src/tui/undo-navigator.ts
@@ -11236,7 +12424,7 @@ function findMatchingCwd(startDir, history) {
11236
12424
  let dir = startDir;
11237
12425
  while (true) {
11238
12426
  if (cwds.has(dir)) return dir;
11239
- const parent = path29.dirname(dir);
12427
+ const parent = path32.dirname(dir);
11240
12428
  if (parent === dir) return null;
11241
12429
  dir = parent;
11242
12430
  }
@@ -11365,7 +12553,7 @@ function registerUndoCommand(program2) {
11365
12553
  // src/cli/commands/watch.ts
11366
12554
  init_daemon();
11367
12555
  import chalk15 from "chalk";
11368
- import { spawn as spawn8, spawnSync as spawnSync5 } from "child_process";
12556
+ import { spawn as spawn8, spawnSync as spawnSync6 } from "child_process";
11369
12557
  function registerWatchCommand(program2) {
11370
12558
  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
12559
  let port = DAEMON_PORT;
@@ -11411,7 +12599,7 @@ function registerWatchCommand(program2) {
11411
12599
  "\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
11412
12600
  )
11413
12601
  );
11414
- const result = spawnSync5(cmd, args, {
12602
+ const result = spawnSync6(cmd, args, {
11415
12603
  stdio: "inherit",
11416
12604
  env: { ...process.env, NODE9_WATCH_MODE: "1" }
11417
12605
  });
@@ -11432,12 +12620,12 @@ import { execa as execa2 } from "execa";
11432
12620
  init_provenance();
11433
12621
 
11434
12622
  // src/mcp-pin.ts
11435
- import fs27 from "fs";
11436
- import path30 from "path";
11437
- import os23 from "os";
11438
- import crypto4 from "crypto";
11439
- function getPinsFilePath() {
11440
- return path30.join(os23.homedir(), ".node9", "mcp-pins.json");
12623
+ import fs30 from "fs";
12624
+ import path33 from "path";
12625
+ import os26 from "os";
12626
+ import crypto5 from "crypto";
12627
+ function getPinsFilePath2() {
12628
+ return path33.join(os26.homedir(), ".node9", "mcp-pins.json");
11441
12629
  }
11442
12630
  function hashToolDefinitions(tools) {
11443
12631
  const sorted = [...tools].sort((a, b) => {
@@ -11446,15 +12634,15 @@ function hashToolDefinitions(tools) {
11446
12634
  return nameA.localeCompare(nameB);
11447
12635
  });
11448
12636
  const canonical = JSON.stringify(sorted);
11449
- return crypto4.createHash("sha256").update(canonical).digest("hex");
12637
+ return crypto5.createHash("sha256").update(canonical).digest("hex");
11450
12638
  }
11451
12639
  function getServerKey(upstreamCommand) {
11452
- return crypto4.createHash("sha256").update(upstreamCommand).digest("hex").slice(0, 16);
12640
+ return crypto5.createHash("sha256").update(upstreamCommand).digest("hex").slice(0, 16);
11453
12641
  }
11454
12642
  function readMcpPinsSafe() {
11455
- const filePath = getPinsFilePath();
12643
+ const filePath = getPinsFilePath2();
11456
12644
  try {
11457
- const raw = fs27.readFileSync(filePath, "utf-8");
12645
+ const raw = fs30.readFileSync(filePath, "utf-8");
11458
12646
  if (!raw.trim()) {
11459
12647
  return { ok: false, reason: "corrupt", detail: "empty file" };
11460
12648
  }
@@ -11477,11 +12665,11 @@ function readMcpPins() {
11477
12665
  throw new Error(`[node9] MCP pin file is corrupt: ${result.detail}`);
11478
12666
  }
11479
12667
  function writeMcpPins(data) {
11480
- const filePath = getPinsFilePath();
11481
- fs27.mkdirSync(path30.dirname(filePath), { recursive: true });
11482
- 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);
12668
+ const filePath = getPinsFilePath2();
12669
+ fs30.mkdirSync(path33.dirname(filePath), { recursive: true });
12670
+ const tmp = `${filePath}.${crypto5.randomBytes(6).toString("hex")}.tmp`;
12671
+ fs30.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
12672
+ fs30.renameSync(tmp, filePath);
11485
12673
  }
11486
12674
  function checkPin(serverKey, currentHash) {
11487
12675
  const result = readMcpPinsSafe();
@@ -11504,12 +12692,12 @@ function updatePin(serverKey, label, toolsHash, toolNames) {
11504
12692
  };
11505
12693
  writeMcpPins(pins);
11506
12694
  }
11507
- function removePin(serverKey) {
12695
+ function removePin2(serverKey) {
11508
12696
  const pins = readMcpPins();
11509
12697
  delete pins.servers[serverKey];
11510
12698
  writeMcpPins(pins);
11511
12699
  }
11512
- function clearAllPins() {
12700
+ function clearAllPins2() {
11513
12701
  writeMcpPins({ servers: {} });
11514
12702
  }
11515
12703
 
@@ -11853,9 +13041,9 @@ function registerMcpGatewayCommand(program2) {
11853
13041
 
11854
13042
  // src/mcp-server/index.ts
11855
13043
  import readline4 from "readline";
11856
- import fs28 from "fs";
11857
- import os24 from "os";
11858
- import path31 from "path";
13044
+ import fs31 from "fs";
13045
+ import os27 from "os";
13046
+ import path34 from "path";
11859
13047
  init_core();
11860
13048
  init_daemon();
11861
13049
  init_shields();
@@ -12030,13 +13218,13 @@ function handleStatus() {
12030
13218
  lines.push(`Active shields: ${activeShields.length > 0 ? activeShields.join(", ") : "none"}`);
12031
13219
  lines.push(`Smart rules: ${config.policy.smartRules.length} loaded`);
12032
13220
  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");
13221
+ const projectConfig = path34.join(process.cwd(), "node9.config.json");
13222
+ const globalConfig = path34.join(os27.homedir(), ".node9", "config.json");
12035
13223
  lines.push(
12036
- `Project config (node9.config.json): ${fs28.existsSync(projectConfig) ? "present" : "not found"}`
13224
+ `Project config (node9.config.json): ${fs31.existsSync(projectConfig) ? "present" : "not found"}`
12037
13225
  );
12038
13226
  lines.push(
12039
- `Global config (~/.node9/config.json): ${fs28.existsSync(globalConfig) ? "present" : "not found"}`
13227
+ `Global config (~/.node9/config.json): ${fs31.existsSync(globalConfig) ? "present" : "not found"}`
12040
13228
  );
12041
13229
  return lines.join("\n");
12042
13230
  }
@@ -12110,21 +13298,21 @@ function handleShieldDisable(args) {
12110
13298
  writeActiveShields(active.filter((s) => s !== name));
12111
13299
  return `Shield "${name}" disabled.`;
12112
13300
  }
12113
- var GLOBAL_CONFIG_PATH2 = path31.join(os24.homedir(), ".node9", "config.json");
13301
+ var GLOBAL_CONFIG_PATH2 = path34.join(os27.homedir(), ".node9", "config.json");
12114
13302
  var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
12115
13303
  function readGlobalConfigRaw() {
12116
13304
  try {
12117
- if (fs28.existsSync(GLOBAL_CONFIG_PATH2)) {
12118
- return JSON.parse(fs28.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
13305
+ if (fs31.existsSync(GLOBAL_CONFIG_PATH2)) {
13306
+ return JSON.parse(fs31.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
12119
13307
  }
12120
13308
  } catch {
12121
13309
  }
12122
13310
  return {};
12123
13311
  }
12124
13312
  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");
13313
+ const dir = path34.dirname(GLOBAL_CONFIG_PATH2);
13314
+ if (!fs31.existsSync(dir)) fs31.mkdirSync(dir, { recursive: true });
13315
+ fs31.writeFileSync(GLOBAL_CONFIG_PATH2, JSON.stringify(data, null, 2) + "\n");
12128
13316
  }
12129
13317
  function handleApproverList() {
12130
13318
  const config = getConfig();
@@ -12167,9 +13355,9 @@ function handleApproverSet(args) {
12167
13355
  }
12168
13356
  function handleAuditGet(args) {
12169
13357
  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);
13358
+ const auditPath = path34.join(os27.homedir(), ".node9", "audit.log");
13359
+ if (!fs31.existsSync(auditPath)) return "No audit log found.";
13360
+ const lines = fs31.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
12173
13361
  const recent = lines.slice(-limit);
12174
13362
  const entries = recent.map((line) => {
12175
13363
  try {
@@ -12467,7 +13655,7 @@ function registerMcpPinCommand(program2) {
12467
13655
  process.exit(1);
12468
13656
  }
12469
13657
  const label = pins.servers[serverKey].label;
12470
- removePin(serverKey);
13658
+ removePin2(serverKey);
12471
13659
  console.log(chalk18.green(`
12472
13660
  \u{1F513} Pin removed for ${chalk18.cyan(serverKey)}`));
12473
13661
  console.log(chalk18.gray(` Server: ${label}`));
@@ -12480,94 +13668,1115 @@ function registerMcpPinCommand(program2) {
12480
13668
  return;
12481
13669
  }
12482
13670
  const count = result.ok ? Object.keys(result.pins.servers).length : "?";
12483
- clearAllPins();
13671
+ clearAllPins2();
12484
13672
  console.log(chalk18.green(`
12485
13673
  \u{1F513} Cleared ${count} MCP pin(s).`));
12486
13674
  console.log(chalk18.gray(" Next connection to each server will re-pin.\n"));
12487
13675
  });
12488
13676
  }
12489
13677
 
12490
- // src/cli.ts
12491
- var { version } = JSON.parse(
12492
- fs31.readFileSync(path34.join(__dirname, "../package.json"), "utf-8")
12493
- );
12494
- var program = new Command();
12495
- program.name("node9").description("The Sudo Command for AI Agents").version(version);
12496
- 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 });
12501
- const profileName = options.profile || "default";
12502
- let existingCreds = {};
12503
- try {
12504
- if (fs31.existsSync(credPath)) {
12505
- const raw = JSON.parse(fs31.readFileSync(credPath, "utf-8"));
12506
- if (raw.apiKey) {
12507
- existingCreds = {
12508
- default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
12509
- };
12510
- } else {
12511
- existingCreds = raw;
12512
- }
13678
+ // src/cli/commands/sync.ts
13679
+ init_sync();
13680
+ import chalk19 from "chalk";
13681
+ function registerSyncCommand(program2) {
13682
+ const policy = program2.command("policy").description("Manage cloud policy rules");
13683
+ policy.command("sync").description("Sync cloud policy rules to local cache (~/.node9/rules-cache.json)").action(async () => {
13684
+ process.stdout.write(chalk19.cyan("Syncing cloud policy rules\u2026"));
13685
+ const result = await runCloudSync();
13686
+ process.stdout.write("\n");
13687
+ if (!result.ok) {
13688
+ console.error(chalk19.red(`\u2717 ${result.reason}`));
13689
+ process.exit(1);
12513
13690
  }
12514
- } catch {
12515
- }
12516
- existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
12517
- fs31.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
12518
- if (profileName === "default") {
12519
- const configPath = path34.join(os27.homedir(), ".node9", "config.json");
12520
- let config = {};
12521
- try {
12522
- if (fs31.existsSync(configPath))
12523
- config = JSON.parse(fs31.readFileSync(configPath, "utf-8"));
12524
- } catch {
13691
+ console.log(
13692
+ chalk19.green(`\u2713 Synced ${result.rules} rule${result.rules === 1 ? "" : "s"} from cloud`)
13693
+ );
13694
+ console.log(chalk19.gray(` Cached at: ${result.fetchedAt}`));
13695
+ console.log(chalk19.gray(` File: ~/.node9/rules-cache.json`));
13696
+ });
13697
+ policy.command("show").description("List all cloud policy rules in the local cache").action(() => {
13698
+ const status = getCloudSyncStatus();
13699
+ if (!status.cached) {
13700
+ console.log(chalk19.yellow("\n No cloud rules cached \u2014 run: node9 policy sync\n"));
13701
+ return;
12525
13702
  }
12526
- if (!config.settings || typeof config.settings !== "object") config.settings = {};
12527
- const s = config.settings;
12528
- const approvers = s.approvers || {
12529
- native: true,
12530
- browser: true,
12531
- cloud: true,
12532
- terminal: true
12533
- };
12534
- if (options.local) {
12535
- approvers.cloud = false;
13703
+ const rules = getCloudRules() ?? [];
13704
+ const age = Math.round((Date.now() - new Date(status.fetchedAt).getTime()) / 6e4);
13705
+ console.log(
13706
+ chalk19.bold(`
13707
+ Cloud policy rules`) + chalk19.gray(
13708
+ ` (${rules.length} rule${rules.length === 1 ? "" : "s"}, synced ${age}m ago)
13709
+ `
13710
+ )
13711
+ );
13712
+ if (rules.length === 0) {
13713
+ console.log(chalk19.gray(" No rules defined in cloud policy.\n"));
13714
+ return;
12536
13715
  }
12537
- 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 });
12541
- }
12542
- 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`));
13716
+ for (const rule of rules) {
13717
+ const r = rule;
13718
+ const verdictColor = r.verdict === "block" ? chalk19.red : r.verdict === "allow" ? chalk19.green : chalk19.yellow;
13719
+ console.log(
13720
+ ` ${verdictColor(
13721
+ String(r.verdict ?? "unknown").toUpperCase().padEnd(6)
13722
+ )} ${chalk19.white(String(r.name ?? "(unnamed)"))}`
13723
+ );
13724
+ if (r.reason) console.log(chalk19.gray(` ${String(r.reason)}`));
13725
+ }
13726
+ console.log("");
13727
+ });
13728
+ policy.command("status").description("Show current cloud policy cache status").action(() => {
13729
+ const s = getCloudSyncStatus();
13730
+ if (!s.cached) {
13731
+ console.log(chalk19.yellow("\n No cache yet \u2014 run: node9 policy sync\n"));
13732
+ } else {
13733
+ const age = Math.round((Date.now() - new Date(s.fetchedAt).getTime()) / 6e4);
13734
+ console.log(`
13735
+ Rules : ${chalk19.green(String(s.rules))} cloud rules loaded`);
13736
+ console.log(
13737
+ ` Synced : ${chalk19.gray(`${age} minute${age === 1 ? "" : "s"} ago`)} (${s.fetchedAt})
13738
+ `
13739
+ );
13740
+ }
13741
+ });
13742
+ }
13743
+
13744
+ // src/cli/commands/agents.ts
13745
+ import chalk20 from "chalk";
13746
+ var SETUP_FN = {
13747
+ claude: setupClaude,
13748
+ gemini: setupGemini,
13749
+ cursor: setupCursor,
13750
+ codex: setupCodex,
13751
+ windsurf: setupWindsurf,
13752
+ vscode: setupVSCode
13753
+ };
13754
+ var TEARDOWN_FN = {
13755
+ claude: teardownClaude,
13756
+ gemini: teardownGemini,
13757
+ cursor: teardownCursor,
13758
+ codex: teardownCodex,
13759
+ windsurf: teardownWindsurf,
13760
+ vscode: teardownVSCode
13761
+ };
13762
+ var AGENT_NAMES = Object.keys(SETUP_FN);
13763
+ function registerAgentsCommand(program2) {
13764
+ const agents = program2.command("agents").description("List and manage AI agent integrations");
13765
+ agents.command("list").description("Show all supported agents and their Node9 status").action(() => {
13766
+ const statuses = getAgentsStatus();
13767
+ const anyInstalled = statuses.some((s) => s.installed);
13768
+ console.log("");
13769
+ console.log(` ${"Agent".padEnd(14)}${"Installed".padEnd(11)}${"Wired".padEnd(8)}Mode`);
13770
+ console.log(" " + "\u2500".repeat(44));
13771
+ for (const s of statuses) {
13772
+ const installed = s.installed ? chalk20.green("\u2713") : chalk20.gray("\u2717");
13773
+ const wired = !s.installed ? chalk20.gray("\u2014") : s.wired ? chalk20.green("\u2713") : chalk20.yellow("\u2717");
13774
+ const mode = s.mode ? chalk20.gray(s.mode) : chalk20.gray("\u2014");
13775
+ const hint = s.installed && !s.wired ? chalk20.gray(` \u2190 node9 agents add ${s.name}`) : "";
13776
+ console.log(` ${s.label.padEnd(14)}${installed} ${wired} ${mode}${hint}`);
13777
+ }
13778
+ console.log("");
13779
+ if (!anyInstalled) {
13780
+ console.log(
13781
+ 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")
13782
+ );
13783
+ return;
13784
+ }
13785
+ const unwired = statuses.filter((s) => s.installed && !s.wired);
13786
+ if (unwired.length > 0) {
13787
+ console.log(
13788
+ chalk20.yellow(` ${unwired.length} agent(s) not yet wired. Run: `) + chalk20.white(`node9 agents add ${unwired[0].name}`) + "\n"
13789
+ );
13790
+ }
13791
+ });
13792
+ agents.command("add").description("Wire Node9 into an agent").argument("<agent>", `Agent to wire: ${AGENT_NAMES.join(" | ")}`).action(async (agent) => {
13793
+ const name = agent.toLowerCase();
13794
+ const fn = SETUP_FN[name];
13795
+ if (!fn) {
13796
+ console.error(chalk20.red(`Unknown agent: "${agent}". Supported: ${AGENT_NAMES.join(", ")}`));
13797
+ process.exit(1);
13798
+ }
13799
+ await fn();
13800
+ });
13801
+ agents.command("remove").description("Remove Node9 from an agent").argument("<agent>", `Agent to unwire: ${AGENT_NAMES.join(" | ")}`).action((agent) => {
13802
+ const name = agent.toLowerCase();
13803
+ const fn = TEARDOWN_FN[name];
13804
+ if (!fn) {
13805
+ console.error(chalk20.red(`Unknown agent: "${agent}". Supported: ${AGENT_NAMES.join(", ")}`));
13806
+ process.exit(1);
13807
+ }
13808
+ console.log(chalk20.cyan(`
13809
+ \u{1F6E1}\uFE0F Node9: removing from ${name}...
13810
+ `));
13811
+ fn();
13812
+ console.log(chalk20.gray("\n Restart the agent for changes to take effect."));
13813
+ });
13814
+ }
13815
+
13816
+ // src/cli/commands/scan.ts
13817
+ init_shields();
13818
+ init_config();
13819
+ init_policy();
13820
+ init_dlp();
13821
+ import chalk21 from "chalk";
13822
+ import fs32 from "fs";
13823
+ import path35 from "path";
13824
+ import os28 from "os";
13825
+ var CLAUDE_PRICING2 = {
13826
+ "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
13827
+ "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
13828
+ "claude-opus-4": { i: 15e-6, o: 75e-6, cw: 1875e-8, cr: 15e-7 },
13829
+ "claude-sonnet-4-6": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13830
+ "claude-sonnet-4-5": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13831
+ "claude-sonnet-4": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13832
+ "claude-3-7-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13833
+ "claude-3-5-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
13834
+ "claude-haiku-4-5": { i: 1e-6, o: 5e-6, cw: 125e-8, cr: 1e-7 },
13835
+ "claude-3-5-haiku": { i: 8e-7, o: 4e-6, cw: 1e-6, cr: 8e-8 }
13836
+ };
13837
+ function claudeModelPrice2(model) {
13838
+ const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
13839
+ for (const [key, p] of Object.entries(CLAUDE_PRICING2)) {
13840
+ if (base === key || base.startsWith(key)) return p;
13841
+ }
13842
+ return null;
13843
+ }
13844
+ function num2(n) {
13845
+ return n.toLocaleString();
13846
+ }
13847
+ function fmtCost2(usd) {
13848
+ if (usd < 1e-3) return "< $0.001";
13849
+ if (usd < 1) return "$" + usd.toFixed(4);
13850
+ return "$" + usd.toFixed(2);
13851
+ }
13852
+ function fmtTs(ts) {
13853
+ try {
13854
+ return new Date(ts).toLocaleDateString("en-US", {
13855
+ month: "short",
13856
+ day: "numeric",
13857
+ year: "numeric"
13858
+ });
13859
+ } catch {
13860
+ return ts.slice(0, 10);
13861
+ }
13862
+ }
13863
+ function preview(input, max) {
13864
+ const cmd = input.command ?? input.query ?? input.file_path ?? JSON.stringify(input);
13865
+ const s = String(cmd).replace(/\s+/g, " ").trim();
13866
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
13867
+ }
13868
+ function buildRuleSources() {
13869
+ const sources = [];
13870
+ for (const [shieldName, shield] of Object.entries(SHIELDS)) {
13871
+ for (const rule of shield.smartRules) {
13872
+ sources.push({ shieldName, shieldLabel: shieldName, rule });
13873
+ }
13874
+ }
13875
+ try {
13876
+ const config = getConfig();
13877
+ for (const rule of config.policy.smartRules) {
13878
+ if (!rule.name) continue;
13879
+ if (rule.name.startsWith("shield:")) continue;
13880
+ const isCloud = rule.name.startsWith("cloud:");
13881
+ sources.push({
13882
+ shieldName: isCloud ? "cloud" : "custom",
13883
+ shieldLabel: isCloud ? "Cloud Policy" : "Your Rules",
13884
+ rule
13885
+ });
13886
+ }
13887
+ } catch {
13888
+ }
13889
+ return sources;
13890
+ }
13891
+ function scanClaudeHistory(startDate) {
13892
+ const projectsDir = path35.join(os28.homedir(), ".claude", "projects");
13893
+ const result = {
13894
+ filesScanned: 0,
13895
+ sessions: 0,
13896
+ totalToolCalls: 0,
13897
+ bashCalls: 0,
13898
+ findings: [],
13899
+ dlpFindings: [],
13900
+ totalCostUSD: 0,
13901
+ firstDate: null,
13902
+ lastDate: null
13903
+ };
13904
+ if (!fs32.existsSync(projectsDir)) return result;
13905
+ let projDirs;
13906
+ try {
13907
+ projDirs = fs32.readdirSync(projectsDir);
13908
+ } catch {
13909
+ return result;
13910
+ }
13911
+ const ruleSources = buildRuleSources();
13912
+ for (const proj of projDirs) {
13913
+ const projPath = path35.join(projectsDir, proj);
13914
+ try {
13915
+ if (!fs32.statSync(projPath).isDirectory()) continue;
13916
+ } catch {
13917
+ continue;
13918
+ }
13919
+ const projLabel = decodeURIComponent(proj).replace(os28.homedir(), "~").slice(0, 40);
13920
+ let files;
13921
+ try {
13922
+ files = fs32.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
13923
+ } catch {
13924
+ continue;
13925
+ }
13926
+ for (const file of files) {
13927
+ result.filesScanned++;
13928
+ result.sessions++;
13929
+ let raw;
13930
+ try {
13931
+ raw = fs32.readFileSync(path35.join(projPath, file), "utf-8");
13932
+ } catch {
13933
+ continue;
13934
+ }
13935
+ for (const line of raw.split("\n")) {
13936
+ if (!line.trim()) continue;
13937
+ let entry;
13938
+ try {
13939
+ entry = JSON.parse(line);
13940
+ } catch {
13941
+ continue;
13942
+ }
13943
+ if (entry.type !== "assistant") continue;
13944
+ if (startDate && entry.timestamp) {
13945
+ if (new Date(entry.timestamp) < startDate) continue;
13946
+ }
13947
+ if (entry.timestamp) {
13948
+ if (!result.firstDate || entry.timestamp < result.firstDate)
13949
+ result.firstDate = entry.timestamp;
13950
+ if (!result.lastDate || entry.timestamp > result.lastDate)
13951
+ result.lastDate = entry.timestamp;
13952
+ }
13953
+ const usage = entry.message?.usage;
13954
+ const model = entry.message?.model;
13955
+ if (usage && model) {
13956
+ const p = claudeModelPrice2(model);
13957
+ if (p) {
13958
+ 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;
13959
+ }
13960
+ }
13961
+ const content = entry.message?.content;
13962
+ if (!Array.isArray(content)) continue;
13963
+ for (const block of content) {
13964
+ if (block.type !== "tool_use") continue;
13965
+ result.totalToolCalls++;
13966
+ const toolName = block.name ?? "";
13967
+ const toolNameLower = toolName.toLowerCase();
13968
+ const input = block.input ?? {};
13969
+ if (toolNameLower === "bash" || toolNameLower === "execute_bash") {
13970
+ result.bashCalls++;
13971
+ }
13972
+ const dlpMatch = scanArgs(input);
13973
+ if (dlpMatch) {
13974
+ const isDupe = result.dlpFindings.some(
13975
+ (f) => f.patternName === dlpMatch.patternName && f.redactedSample === dlpMatch.redactedSample && f.project === projLabel
13976
+ );
13977
+ if (!isDupe) {
13978
+ result.dlpFindings.push({
13979
+ patternName: dlpMatch.patternName,
13980
+ redactedSample: dlpMatch.redactedSample,
13981
+ toolName,
13982
+ timestamp: entry.timestamp ?? "",
13983
+ project: projLabel
13984
+ });
13985
+ }
13986
+ }
13987
+ for (const source of ruleSources) {
13988
+ const { rule } = source;
13989
+ if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
13990
+ if (!evaluateSmartConditions(input, rule)) continue;
13991
+ const inputPreview = preview(input, 120);
13992
+ const isDupe = result.findings.some(
13993
+ (f) => f.source.rule.name === rule.name && preview(f.input, 120) === inputPreview && f.project === projLabel
13994
+ );
13995
+ if (!isDupe) {
13996
+ result.findings.push({
13997
+ source,
13998
+ toolName,
13999
+ input,
14000
+ timestamp: entry.timestamp ?? "",
14001
+ project: projLabel
14002
+ });
14003
+ }
14004
+ break;
14005
+ }
14006
+ }
14007
+ }
14008
+ }
14009
+ }
14010
+ return result;
14011
+ }
14012
+ function registerScanCommand(program2) {
14013
+ 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) => {
14014
+ const topN = Math.max(1, parseInt(options.top, 10) || 5);
14015
+ const startDate = options.all ? null : (() => {
14016
+ const d = /* @__PURE__ */ new Date();
14017
+ d.setDate(d.getDate() - (parseInt(options.days, 10) || 90));
14018
+ d.setHours(0, 0, 0, 0);
14019
+ return d;
14020
+ })();
14021
+ console.log("");
14022
+ console.log(chalk21.cyan.bold("\u{1F50D} node9 scan") + chalk21.dim(" \u2014 what would node9 catch?"));
14023
+ console.log("");
14024
+ const projectsDir = path35.join(os28.homedir(), ".claude", "projects");
14025
+ if (!fs32.existsSync(projectsDir)) {
14026
+ console.log(chalk21.yellow(" No Claude history found at ~/.claude/projects/"));
14027
+ console.log(chalk21.gray(" Install Claude Code, run a few sessions, then try again.\n"));
14028
+ return;
14029
+ }
14030
+ process.stdout.write(chalk21.dim(" Scanning\u2026"));
14031
+ const scan = scanClaudeHistory(startDate);
14032
+ process.stdout.write("\r" + " ".repeat(20) + "\r");
14033
+ if (scan.filesScanned === 0) {
14034
+ console.log(chalk21.yellow(" No JSONL session files found.\n"));
14035
+ return;
14036
+ }
14037
+ const rangeLabel = options.all ? chalk21.dim("all time") : chalk21.dim(`last ${options.days ?? 90} days`);
14038
+ const dateRange = scan.firstDate && scan.lastDate ? chalk21.dim(` ${fmtTs(scan.firstDate)} \u2013 ${fmtTs(scan.lastDate)}`) : "";
14039
+ console.log(
14040
+ " " + 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
14041
+ );
14042
+ console.log("");
14043
+ const byShield = /* @__PURE__ */ new Map();
14044
+ for (const f of scan.findings) {
14045
+ const key = f.source.shieldName;
14046
+ const entry = byShield.get(key) ?? { label: f.source.shieldLabel, findings: [] };
14047
+ entry.findings.push(f);
14048
+ byShield.set(key, entry);
14049
+ }
14050
+ const totalFindings = scan.findings.length;
14051
+ if (totalFindings === 0 && scan.dlpFindings.length === 0) {
14052
+ console.log(chalk21.green(" \u2705 No findings across all shields and rules."));
14053
+ console.log(chalk21.dim(" node9 is still worth running \u2014 it monitors in real time.\n"));
14054
+ } else {
14055
+ if (totalFindings > 0) {
14056
+ console.log(
14057
+ " " + chalk21.bold("If node9 had been installed:") + " " + chalk21.yellow.bold(
14058
+ `${num2(totalFindings)} command${totalFindings !== 1 ? "s" : ""} flagged for review`
14059
+ )
14060
+ );
14061
+ console.log("");
14062
+ const sorted = [...byShield.entries()].sort(
14063
+ (a, b) => b[1].findings.length - a[1].findings.length
14064
+ );
14065
+ for (const [shieldName, { label, findings }] of sorted) {
14066
+ const count = findings.length;
14067
+ const isUserRule = shieldName === "custom" || shieldName === "cloud";
14068
+ const shieldBadge = isUserRule ? chalk21.magenta(label) : chalk21.cyan(label);
14069
+ console.log(" " + chalk21.dim("\u2500".repeat(70)));
14070
+ console.log(
14071
+ " " + shieldBadge + chalk21.dim(" \xB7 ") + chalk21.yellow(`${num2(count)} finding${count !== 1 ? "s" : ""}`) + (isUserRule ? "" : chalk21.dim(` \u2192 node9 shield enable ${shieldName}`))
14072
+ );
14073
+ const byRule = /* @__PURE__ */ new Map();
14074
+ for (const f of findings) {
14075
+ const ruleKey = f.source.rule.name ?? "unnamed";
14076
+ const arr = byRule.get(ruleKey) ?? [];
14077
+ arr.push(f);
14078
+ byRule.set(ruleKey, arr);
14079
+ }
14080
+ for (const [, ruleFindings] of byRule) {
14081
+ const rule = ruleFindings[0].source.rule;
14082
+ const ruleCount = ruleFindings.length;
14083
+ const countBadge = ruleCount > 1 ? chalk21.white(` \xD7${ruleCount}`) : "";
14084
+ const shortName = (rule.name ?? "unnamed").replace(/^shield:[^:]+:/, "");
14085
+ console.log(
14086
+ " " + chalk21.white(shortName) + countBadge + (rule.reason ? chalk21.dim(` \u2014 ${rule.reason}`) : "")
14087
+ );
14088
+ const shown = ruleFindings.slice(0, topN);
14089
+ for (const f of shown) {
14090
+ const ts = f.timestamp ? chalk21.dim(fmtTs(f.timestamp) + " ") : "";
14091
+ const proj = chalk21.dim(f.project.slice(0, 22).padEnd(22) + " ");
14092
+ const cmd = chalk21.gray(preview(f.input, 55));
14093
+ console.log(` ${ts}${proj}${cmd}`);
14094
+ }
14095
+ if (ruleFindings.length > topN) {
14096
+ console.log(
14097
+ chalk21.dim(
14098
+ ` \u2026 and ${ruleFindings.length - topN} more (--top ${ruleFindings.length})`
14099
+ )
14100
+ );
14101
+ }
14102
+ }
14103
+ console.log("");
14104
+ }
14105
+ }
14106
+ if (scan.dlpFindings.length > 0) {
14107
+ console.log(" " + chalk21.dim("\u2500".repeat(70)));
14108
+ console.log(
14109
+ " " + chalk21.red.bold("Secrets / DLP") + chalk21.dim(" \xB7 ") + chalk21.red(
14110
+ `${num2(scan.dlpFindings.length)} potential secret leak${scan.dlpFindings.length !== 1 ? "s" : ""}`
14111
+ )
14112
+ );
14113
+ const shownDlp = scan.dlpFindings.slice(0, topN);
14114
+ for (const f of shownDlp) {
14115
+ const ts = f.timestamp ? chalk21.dim(fmtTs(f.timestamp) + " ") : "";
14116
+ const proj = chalk21.dim(f.project.slice(0, 22).padEnd(22) + " ");
14117
+ console.log(
14118
+ ` ${ts}${proj}` + chalk21.yellow(f.patternName) + chalk21.dim(" ") + chalk21.gray(f.redactedSample)
14119
+ );
14120
+ }
14121
+ if (scan.dlpFindings.length > topN) {
14122
+ console.log(
14123
+ chalk21.dim(
14124
+ ` \u2026 and ${scan.dlpFindings.length - topN} more (--top ${scan.dlpFindings.length})`
14125
+ )
14126
+ );
14127
+ }
14128
+ console.log("");
14129
+ }
14130
+ }
14131
+ if (scan.totalCostUSD > 0) {
14132
+ console.log(
14133
+ " " + chalk21.bold("Claude spend:") + " " + chalk21.yellow(fmtCost2(scan.totalCostUSD)) + chalk21.dim(" (for per-period breakdown: node9 report)")
14134
+ );
14135
+ console.log("");
14136
+ }
14137
+ const auditLog = path35.join(os28.homedir(), ".node9", "audit.log");
14138
+ if (fs32.existsSync(auditLog)) {
14139
+ console.log(chalk21.green(" \u2705 node9 is active \u2014 future sessions are protected."));
14140
+ console.log(
14141
+ chalk21.dim(" Run ") + chalk21.cyan("node9 report") + chalk21.dim(" to see live stats.")
14142
+ );
14143
+ } else {
14144
+ console.log(chalk21.yellow.bold(" \u26A1 node9 was not running during these sessions."));
14145
+ console.log(
14146
+ " " + chalk21.white("Run ") + chalk21.cyan("node9 init") + chalk21.white(" to start protecting your AI agents.")
14147
+ );
14148
+ }
14149
+ console.log("");
14150
+ });
14151
+ }
14152
+
14153
+ // src/cli/commands/sessions.ts
14154
+ import chalk22 from "chalk";
14155
+ import fs33 from "fs";
14156
+ import path36 from "path";
14157
+ import os29 from "os";
14158
+ var CLAUDE_PRICING3 = {
14159
+ "claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
14160
+ "claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
14161
+ "claude-opus-4": { i: 15e-6, o: 75e-6, cw: 1875e-8, cr: 15e-7 },
14162
+ "claude-sonnet-4-6": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
14163
+ "claude-sonnet-4-5": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
14164
+ "claude-sonnet-4": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
14165
+ "claude-3-7-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
14166
+ "claude-3-5-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
14167
+ "claude-haiku-4-5": { i: 1e-6, o: 5e-6, cw: 125e-8, cr: 1e-7 },
14168
+ "claude-3-5-haiku": { i: 8e-7, o: 4e-6, cw: 1e-6, cr: 8e-8 }
14169
+ };
14170
+ function modelPrice(model) {
14171
+ const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
14172
+ for (const [key, p] of Object.entries(CLAUDE_PRICING3)) {
14173
+ if (base === key || base.startsWith(key)) return p;
14174
+ }
14175
+ return null;
14176
+ }
14177
+ function encodeProjectPath(projectPath) {
14178
+ return projectPath.replace(/\//g, "-");
14179
+ }
14180
+ function sessionJsonlPath(projectPath, sessionId) {
14181
+ const encoded = encodeProjectPath(projectPath);
14182
+ return path36.join(os29.homedir(), ".claude", "projects", encoded, `${sessionId}.jsonl`);
14183
+ }
14184
+ function projectLabel(projectPath) {
14185
+ return projectPath.replace(os29.homedir(), "~");
14186
+ }
14187
+ function parseHistoryLines(lines) {
14188
+ const entries = [];
14189
+ for (const line of lines) {
14190
+ if (!line.trim()) continue;
14191
+ try {
14192
+ const obj = JSON.parse(line);
14193
+ if (typeof obj["display"] === "string" && (typeof obj["timestamp"] === "string" || typeof obj["timestamp"] === "number") && typeof obj["project"] === "string" && typeof obj["sessionId"] === "string") {
14194
+ const ts = typeof obj["timestamp"] === "number" ? new Date(obj["timestamp"]).toISOString() : obj["timestamp"];
14195
+ entries.push({
14196
+ display: obj["display"],
14197
+ timestamp: ts,
14198
+ project: obj["project"],
14199
+ sessionId: obj["sessionId"]
14200
+ });
14201
+ }
14202
+ } catch {
14203
+ }
14204
+ }
14205
+ return entries;
14206
+ }
14207
+ function parseSessionLines(lines) {
14208
+ const toolCalls = [];
14209
+ let costUSD = 0;
14210
+ let hasSnapshot = false;
14211
+ const modifiedFiles = [];
14212
+ const seenFiles = /* @__PURE__ */ new Set();
14213
+ for (const line of lines) {
14214
+ if (!line.trim()) continue;
14215
+ let entry;
14216
+ try {
14217
+ entry = JSON.parse(line);
14218
+ } catch {
14219
+ continue;
14220
+ }
14221
+ if (entry.type === "file-history-snapshot") {
14222
+ hasSnapshot = true;
14223
+ continue;
14224
+ }
14225
+ if (entry.type !== "assistant") continue;
14226
+ const usage = entry.message?.usage;
14227
+ const model = entry.message?.model;
14228
+ if (usage && model) {
14229
+ const p = modelPrice(model);
14230
+ if (p) {
14231
+ 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;
14232
+ }
14233
+ }
14234
+ const content = entry.message?.content;
14235
+ if (!Array.isArray(content)) continue;
14236
+ for (const block of content) {
14237
+ if (block.type !== "tool_use") continue;
14238
+ const tool = block.name ?? "";
14239
+ const input = block.input ?? {};
14240
+ toolCalls.push({ tool, input, timestamp: entry.timestamp ?? "" });
14241
+ const toolLower = tool.toLowerCase();
14242
+ if (toolLower === "write" || toolLower === "edit" || toolLower === "notebookedit") {
14243
+ const fp = input.file_path ?? input.path;
14244
+ if (typeof fp === "string" && !seenFiles.has(fp)) {
14245
+ seenFiles.add(fp);
14246
+ modifiedFiles.push(fp);
14247
+ }
14248
+ }
14249
+ }
14250
+ }
14251
+ return { toolCalls, costUSD, hasSnapshot, modifiedFiles };
14252
+ }
14253
+ function loadAuditEntries(auditPath) {
14254
+ const aPath = auditPath ?? path36.join(os29.homedir(), ".node9", "audit.log");
14255
+ let raw;
14256
+ try {
14257
+ raw = fs33.readFileSync(aPath, "utf-8");
14258
+ } catch {
14259
+ return [];
14260
+ }
14261
+ const entries = [];
14262
+ for (const line of raw.split("\n")) {
14263
+ if (!line.trim()) continue;
14264
+ try {
14265
+ const e = JSON.parse(line);
14266
+ if (!e.ts || !e.tool || !e.decision) continue;
14267
+ if (e.decision === "allow" || e.decision === "allowed") continue;
14268
+ entries.push(e);
14269
+ } catch {
14270
+ }
14271
+ }
14272
+ return entries;
14273
+ }
14274
+ function auditEntriesInWindow(entries, windowStart, windowEnd) {
14275
+ const start = new Date(windowStart).getTime();
14276
+ const end = new Date(windowEnd).getTime();
14277
+ const result = [];
14278
+ for (const e of entries) {
14279
+ const t = new Date(e.ts).getTime();
14280
+ if (t < start || t > end) continue;
14281
+ result.push({
14282
+ tool: e.tool,
14283
+ args: e.args,
14284
+ argsHash: e.argsHash,
14285
+ timestamp: e.ts,
14286
+ decision: e.decision,
14287
+ checkedBy: e.checkedBy
14288
+ });
14289
+ }
14290
+ return result;
14291
+ }
14292
+ function buildSessions(days, historyPath) {
14293
+ const hPath = historyPath ?? path36.join(os29.homedir(), ".claude", "history.jsonl");
14294
+ let historyRaw;
14295
+ try {
14296
+ historyRaw = fs33.readFileSync(hPath, "utf-8");
14297
+ } catch {
14298
+ return [];
14299
+ }
14300
+ const cutoff = days !== null ? (() => {
14301
+ const d = /* @__PURE__ */ new Date();
14302
+ d.setDate(d.getDate() - days);
14303
+ d.setHours(0, 0, 0, 0);
14304
+ return d;
14305
+ })() : null;
14306
+ const entries = parseHistoryLines(historyRaw.split("\n"));
14307
+ const bySession = /* @__PURE__ */ new Map();
14308
+ for (const e of entries) {
14309
+ if (cutoff && new Date(e.timestamp) < cutoff) continue;
14310
+ const existing = bySession.get(e.sessionId);
14311
+ if (!existing || e.timestamp < existing.timestamp) {
14312
+ bySession.set(e.sessionId, e);
14313
+ }
14314
+ }
14315
+ const allAuditEntries = loadAuditEntries();
14316
+ const summaries = [];
14317
+ for (const entry of bySession.values()) {
14318
+ const jsonlFile = sessionJsonlPath(entry.project, entry.sessionId);
14319
+ let sessionLines = [];
14320
+ try {
14321
+ sessionLines = fs33.readFileSync(jsonlFile, "utf-8").split("\n");
14322
+ } catch {
14323
+ }
14324
+ const { toolCalls, costUSD, hasSnapshot, modifiedFiles } = parseSessionLines(sessionLines);
14325
+ const windowStart = entry.timestamp;
14326
+ const lastToolTs = toolCalls.length > 0 ? toolCalls[toolCalls.length - 1].timestamp : "";
14327
+ const windowEnd = new Date(
14328
+ Math.max(new Date(windowStart).getTime(), lastToolTs ? new Date(lastToolTs).getTime() : 0) + 5 * 60 * 1e3
14329
+ // 5 min buffer
14330
+ ).toISOString();
14331
+ const blockedCalls = auditEntriesInWindow(allAuditEntries, windowStart, windowEnd);
14332
+ summaries.push({
14333
+ sessionId: entry.sessionId,
14334
+ project: entry.project,
14335
+ projectLabel: projectLabel(entry.project),
14336
+ firstPrompt: entry.display,
14337
+ startTime: entry.timestamp,
14338
+ toolCalls,
14339
+ blockedCalls,
14340
+ costUSD,
14341
+ hasSnapshot,
14342
+ modifiedFiles
14343
+ });
14344
+ }
14345
+ summaries.sort((a, b) => a.startTime > b.startTime ? -1 : 1);
14346
+ return summaries;
14347
+ }
14348
+ function fmtCost3(usd) {
14349
+ if (usd === 0) return "";
14350
+ if (usd < 1e-3) return "< $0.001";
14351
+ if (usd < 1) return "$" + usd.toFixed(3);
14352
+ return "$" + usd.toFixed(2);
14353
+ }
14354
+ function fmtDate2(iso) {
14355
+ try {
14356
+ return new Date(iso).toLocaleDateString("en-US", { month: "short", day: "numeric" });
14357
+ } catch {
14358
+ return iso.slice(0, 10);
14359
+ }
14360
+ }
14361
+ function fmtTime(iso) {
14362
+ try {
14363
+ return new Date(iso).toLocaleTimeString("en-US", {
14364
+ hour: "2-digit",
14365
+ minute: "2-digit",
14366
+ hour12: false
14367
+ });
14368
+ } catch {
14369
+ return iso.slice(11, 16);
14370
+ }
14371
+ }
14372
+ function fmtDateTime(iso) {
14373
+ try {
14374
+ return new Date(iso).toLocaleString("en-US", {
14375
+ month: "short",
14376
+ day: "numeric",
14377
+ year: "numeric",
14378
+ hour: "2-digit",
14379
+ minute: "2-digit",
14380
+ hour12: false
14381
+ });
14382
+ } catch {
14383
+ return iso;
14384
+ }
14385
+ }
14386
+ function truncate(s, max) {
14387
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
14388
+ }
14389
+ function toolInputSummary(tool, input) {
14390
+ const fp = input.file_path ?? input.path;
14391
+ if (typeof fp === "string") return fp;
14392
+ const cmd = input.command;
14393
+ if (typeof cmd === "string") return truncate(cmd.replace(/\s+/g, " "), 60);
14394
+ const q = input.query ?? input.url;
14395
+ if (typeof q === "string") return truncate(String(q), 60);
14396
+ return "";
14397
+ }
14398
+ function toolColor(tool) {
14399
+ const t = tool.toLowerCase();
14400
+ if (t === "bash" || t === "execute_bash") return chalk22.red;
14401
+ if (t === "write") return chalk22.green;
14402
+ if (t === "edit" || t === "notebookedit") return chalk22.yellow;
14403
+ if (t === "read") return chalk22.cyan;
14404
+ return chalk22.gray;
14405
+ }
14406
+ function barStr2(value, max, width) {
14407
+ if (max === 0 || width <= 0) return "\u2591".repeat(width);
14408
+ const filled = Math.max(1, Math.round(value / max * width));
14409
+ return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
14410
+ }
14411
+ function colorBar2(value, max, width) {
14412
+ const s = barStr2(value, max, width);
14413
+ const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
14414
+ return chalk22.cyan(s.slice(0, filled)) + chalk22.dim(s.slice(filled));
14415
+ }
14416
+ function renderSummary(summaries) {
14417
+ const totalTools = summaries.reduce((n, s) => n + s.toolCalls.length, 0);
14418
+ const totalCost = summaries.reduce((n, s) => n + s.costUSD, 0);
14419
+ const totalFiles = summaries.reduce((n, s) => n + s.modifiedFiles.length, 0);
14420
+ const totalBlocked = summaries.reduce((n, s) => n + s.blockedCalls.length, 0);
14421
+ const snapshots = summaries.filter((s) => s.hasSnapshot).length;
14422
+ const avgCost = summaries.length > 0 ? totalCost / summaries.length : 0;
14423
+ const toolCounts = /* @__PURE__ */ new Map();
14424
+ for (const s of summaries) {
14425
+ for (const tc of s.toolCalls) {
14426
+ const key = tc.tool.toLowerCase();
14427
+ toolCounts.set(key, (toolCounts.get(key) ?? 0) + 1);
14428
+ }
14429
+ }
14430
+ const groups = { Bash: 0, Read: 0, Write: 0, Edit: 0, Other: 0 };
14431
+ for (const [tool, count] of toolCounts) {
14432
+ if (tool === "bash" || tool === "execute_bash") groups["Bash"] += count;
14433
+ else if (tool === "read") groups["Read"] += count;
14434
+ else if (tool === "write") groups["Write"] += count;
14435
+ else if (tool === "edit" || tool === "notebookedit") groups["Edit"] += count;
14436
+ else groups["Other"] += count;
14437
+ }
14438
+ const projCosts = /* @__PURE__ */ new Map();
14439
+ for (const s of summaries) {
14440
+ projCosts.set(s.projectLabel, (projCosts.get(s.projectLabel) ?? 0) + s.costUSD);
14441
+ }
14442
+ const topProjects = [...projCosts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
14443
+ const W = 20;
14444
+ console.log(chalk22.dim(" " + "\u2500".repeat(70)));
14445
+ console.log(
14446
+ " " + 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") : "")
14447
+ );
14448
+ console.log(
14449
+ " " + chalk22.dim("avg ") + chalk22.white(fmtCost3(avgCost).padEnd(10)) + chalk22.dim("/session ") + chalk22.green(String(snapshots)) + chalk22.dim(` of ${summaries.length} sessions had snapshots`)
14450
+ );
14451
+ console.log("");
14452
+ console.log(" " + chalk22.dim("Tool breakdown:"));
14453
+ const maxGroup = Math.max(...Object.values(groups));
14454
+ for (const [label, count] of Object.entries(groups)) {
14455
+ if (count === 0) continue;
14456
+ const pct2 = totalTools > 0 ? Math.round(count / totalTools * 100) : 0;
14457
+ console.log(
14458
+ " " + label.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " + chalk22.white(String(count).padStart(4)) + chalk22.dim(` (${String(pct2)}%)`)
14459
+ );
14460
+ }
14461
+ console.log("");
14462
+ if (topProjects.length > 1) {
14463
+ console.log(" " + chalk22.dim("Cost by project:"));
14464
+ const maxProjCost = topProjects[0][1];
14465
+ for (const [proj, cost] of topProjects) {
14466
+ console.log(
14467
+ " " + proj.slice(0, 28).padEnd(28) + " " + colorBar2(cost, maxProjCost, W) + " " + chalk22.yellow(fmtCost3(cost))
14468
+ );
14469
+ }
14470
+ console.log("");
14471
+ }
14472
+ console.log(chalk22.dim(" " + "\u2500".repeat(70)));
14473
+ console.log("");
14474
+ }
14475
+ function renderList(summaries, totalCost) {
14476
+ if (summaries.length === 0) {
14477
+ console.log(chalk22.yellow(" No sessions found in the requested range.\n"));
14478
+ return;
14479
+ }
14480
+ const totalLabel = totalCost > 0 ? chalk22.dim(" ~" + fmtCost3(totalCost) + " total") : "";
14481
+ console.log(
14482
+ " " + chalk22.white(String(summaries.length)) + chalk22.dim(` session${summaries.length !== 1 ? "s" : ""}`) + totalLabel
14483
+ );
14484
+ console.log("");
14485
+ let lastGroup = "";
14486
+ for (const s of summaries) {
14487
+ const group = fmtDate2(s.startTime) + " " + s.projectLabel;
14488
+ if (group !== lastGroup) {
14489
+ console.log(
14490
+ chalk22.dim(" \u2500\u2500\u2500 ") + chalk22.bold(fmtDate2(s.startTime)) + chalk22.dim(" " + s.projectLabel)
14491
+ );
14492
+ lastGroup = group;
14493
+ }
14494
+ const timeStr = chalk22.dim(fmtTime(s.startTime));
14495
+ const prompt = chalk22.white(truncate(s.firstPrompt.replace(/\n/g, " "), 50).padEnd(50));
14496
+ const tools = s.toolCalls.length > 0 ? chalk22.dim(String(s.toolCalls.length).padStart(3) + " tools") : chalk22.dim(" 0 tools");
14497
+ const cost = s.costUSD > 0 ? chalk22.dim(" " + fmtCost3(s.costUSD).padEnd(8)) : " ";
14498
+ const blocked = s.blockedCalls.length > 0 ? chalk22.red(" \u{1F6D1} " + String(s.blockedCalls.length)) : "";
14499
+ const snap = s.hasSnapshot ? chalk22.green(" \u{1F4F8}") : "";
14500
+ const sid = chalk22.dim(" " + s.sessionId.slice(0, 8));
14501
+ console.log(` ${timeStr} ${prompt} ${tools}${cost}${blocked}${snap}${sid}`);
14502
+ }
14503
+ console.log("");
14504
+ console.log(
14505
+ chalk22.dim(" Run") + " " + chalk22.cyan("node9 sessions --detail <session-id>") + chalk22.dim(" for full tool trace.")
14506
+ );
14507
+ console.log("");
14508
+ }
14509
+ function renderDetail(s) {
14510
+ console.log("");
14511
+ console.log(chalk22.bold(" Session ") + chalk22.dim(s.sessionId));
14512
+ console.log(
14513
+ chalk22.bold(" Prompt ") + chalk22.white(s.firstPrompt.replace(/\n/g, " ").slice(0, 120))
14514
+ );
14515
+ console.log(chalk22.bold(" Project ") + chalk22.white(s.projectLabel));
14516
+ console.log(chalk22.bold(" When ") + chalk22.white(fmtDateTime(s.startTime)));
14517
+ if (s.costUSD > 0)
14518
+ console.log(chalk22.bold(" Cost ") + chalk22.yellow("~" + fmtCost3(s.costUSD)));
14519
+ console.log(
14520
+ chalk22.bold(" Snapshot ") + (s.hasSnapshot ? chalk22.green("\u2713 taken") : chalk22.dim("none"))
14521
+ );
14522
+ console.log("");
14523
+ if (s.toolCalls.length === 0 && s.blockedCalls.length === 0) {
14524
+ console.log(chalk22.dim(" No tool calls recorded.\n"));
14525
+ return;
14526
+ }
14527
+ const timeline = [
14528
+ ...s.toolCalls.map((tc) => ({ kind: "tool", tc })),
14529
+ ...s.blockedCalls.map((bc) => ({ kind: "blocked", bc }))
14530
+ ].sort((a, b) => {
14531
+ const ta = a.kind === "tool" ? a.tc.timestamp : a.bc.timestamp;
14532
+ const tb = b.kind === "tool" ? b.tc.timestamp : b.bc.timestamp;
14533
+ return ta < tb ? -1 : ta > tb ? 1 : 0;
14534
+ });
14535
+ const headerParts = [`Tool calls (${s.toolCalls.length})`];
14536
+ if (s.blockedCalls.length > 0)
14537
+ headerParts.push(chalk22.red(`${s.blockedCalls.length} blocked by node9`));
14538
+ console.log(chalk22.bold(" " + headerParts.join(" \xB7 ")));
14539
+ console.log("");
14540
+ for (const entry of timeline) {
14541
+ if (entry.kind === "tool") {
14542
+ const tc = entry.tc;
14543
+ const colorFn = toolColor(tc.tool);
14544
+ const toolPad = colorFn(tc.tool.padEnd(16));
14545
+ const detail = chalk22.gray(truncate(toolInputSummary(tc.tool, tc.input), 70));
14546
+ const ts = tc.timestamp ? chalk22.dim(fmtTime(tc.timestamp) + " ") : " ";
14547
+ console.log(` ${ts}${toolPad} ${detail}`);
14548
+ } else {
14549
+ const bc = entry.bc;
14550
+ const ts = bc.timestamp ? chalk22.dim(fmtTime(bc.timestamp) + " ") : " ";
14551
+ const label = chalk22.red("\u{1F6D1} BLOCKED".padEnd(16));
14552
+ const toolName = chalk22.red(bc.tool.padEnd(10));
14553
+ const argsSummary = bc.args ? chalk22.gray(truncate(toolInputSummary(bc.tool, bc.args), 40)) : chalk22.dim("[args not logged]");
14554
+ const reason = bc.checkedBy ? chalk22.dim(" \u2190 " + bc.checkedBy) : "";
14555
+ console.log(` ${ts}${label} ${toolName} ${argsSummary}${reason}`);
14556
+ }
14557
+ }
14558
+ console.log("");
14559
+ if (s.modifiedFiles.length > 0) {
14560
+ console.log(chalk22.bold(` Files modified (${s.modifiedFiles.length}):`));
14561
+ for (const f of s.modifiedFiles) {
14562
+ console.log(" " + chalk22.yellow(f));
14563
+ }
14564
+ console.log("");
14565
+ }
14566
+ }
14567
+ function registerSessionsCommand(program2) {
14568
+ 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) => {
14569
+ console.log("");
14570
+ console.log(chalk22.cyan.bold("\u{1F4CB} node9 sessions") + chalk22.dim(" \u2014 what your AI agent did"));
14571
+ console.log("");
14572
+ const historyPath = path36.join(os29.homedir(), ".claude", "history.jsonl");
14573
+ if (!fs33.existsSync(historyPath)) {
14574
+ console.log(chalk22.yellow(" No Claude session history found at ~/.claude/history.jsonl"));
14575
+ console.log(chalk22.gray(" Install Claude Code, run a few sessions, then try again.\n"));
14576
+ return;
14577
+ }
14578
+ const days = options.detail || options.all ? null : Math.max(1, parseInt(options.days, 10) || 7);
14579
+ const rangeLabel = options.detail ? "all time" : options.all ? "all time" : `last ${String(days)} days`;
14580
+ console.log(chalk22.dim(" " + rangeLabel));
14581
+ console.log("");
14582
+ process.stdout.write(chalk22.dim(" Loading\u2026"));
14583
+ const summaries = buildSessions(days);
14584
+ process.stdout.write("\r" + " ".repeat(20) + "\r");
14585
+ if (options.detail) {
14586
+ const target = summaries.find(
14587
+ (s) => s.sessionId === options.detail || s.sessionId.startsWith(options.detail)
14588
+ );
14589
+ if (!target) {
14590
+ console.log(chalk22.red(` Session not found: ${options.detail}`));
14591
+ console.log(chalk22.dim(" Run `node9 sessions` to list recent sessions.\n"));
14592
+ return;
14593
+ }
14594
+ renderDetail(target);
14595
+ return;
14596
+ }
14597
+ const totalCost = summaries.reduce((sum, s) => sum + s.costUSD, 0);
14598
+ if (summaries.length > 0) renderSummary(summaries);
14599
+ renderList(summaries, totalCost);
14600
+ });
14601
+ }
14602
+
14603
+ // src/cli/commands/skill-pin.ts
14604
+ import chalk23 from "chalk";
14605
+ import fs34 from "fs";
14606
+ import os30 from "os";
14607
+ import path37 from "path";
14608
+ function wipeSkillSessions() {
14609
+ try {
14610
+ fs34.rmSync(path37.join(os30.homedir(), ".node9", "skill-sessions"), {
14611
+ recursive: true,
14612
+ force: true
14613
+ });
14614
+ } catch {
14615
+ }
14616
+ }
14617
+ function registerSkillPinCommand(program2) {
14618
+ const skillCmd = program2.command("skill").description("Manage skill pinning (supply chain & update drift defense, AST 02 + AST 07)");
14619
+ const pinSubCmd = skillCmd.command("pin").description("Manage pinned skill roots");
14620
+ pinSubCmd.command("list").description("Show all pinned skill roots and their content hashes").action(() => {
14621
+ const result = readSkillPinsSafe();
14622
+ if (!result.ok) {
14623
+ if (result.reason === "missing") {
14624
+ console.log(chalk23.gray("\nNo skill roots are pinned yet."));
14625
+ console.log(
14626
+ chalk23.gray("Pins are created automatically on the first tool call of each session.\n")
14627
+ );
14628
+ return;
14629
+ }
14630
+ console.error(chalk23.red(`
14631
+ \u274C Pin file is corrupt: ${result.detail}`));
14632
+ console.error(chalk23.yellow(" Run: node9 skill pin reset\n"));
14633
+ process.exit(1);
14634
+ }
14635
+ const entries = Object.entries(result.pins.roots);
14636
+ if (entries.length === 0) {
14637
+ console.log(chalk23.gray("\nNo skill roots are pinned yet.\n"));
14638
+ return;
14639
+ }
14640
+ console.log(chalk23.bold("\n\u{1F512} Pinned Skill Roots\n"));
14641
+ for (const [key, entry] of entries) {
14642
+ const missing = entry.exists ? "" : chalk23.yellow(" (not present at pin time)");
14643
+ console.log(` ${chalk23.cyan(key)} ${chalk23.gray(entry.rootPath)}${missing}`);
14644
+ console.log(` Files (${entry.fileCount})`);
14645
+ console.log(` Hash: ${chalk23.gray(entry.contentHash.slice(0, 16))}...`);
14646
+ console.log(` Pinned: ${chalk23.gray(entry.pinnedAt)}
14647
+ `);
14648
+ }
14649
+ });
14650
+ pinSubCmd.command("update <rootKey>").description("Remove a pin so the next session re-pins with current state").action((rootKey) => {
14651
+ let pins;
14652
+ try {
14653
+ pins = readSkillPins();
14654
+ } catch {
14655
+ console.error(chalk23.red("\n\u274C Pin file is corrupt."));
14656
+ console.error(chalk23.yellow(" Run: node9 skill pin reset\n"));
14657
+ process.exit(1);
14658
+ }
14659
+ if (!pins.roots[rootKey]) {
14660
+ console.error(chalk23.red(`
14661
+ \u274C No pin found for root key "${rootKey}"
14662
+ `));
14663
+ console.error(`Run ${chalk23.cyan("node9 skill pin list")} to see pinned roots.
14664
+ `);
14665
+ process.exit(1);
14666
+ }
14667
+ const rootPath = pins.roots[rootKey].rootPath;
14668
+ removePin(rootKey);
14669
+ wipeSkillSessions();
14670
+ console.log(chalk23.green(`
14671
+ \u{1F513} Pin removed for ${chalk23.cyan(rootKey)}`));
14672
+ console.log(chalk23.gray(` ${rootPath}`));
14673
+ console.log(chalk23.gray(" Next session will re-pin with current state.\n"));
14674
+ });
14675
+ pinSubCmd.command("reset").description("Clear all skill pins and wipe session verification flags").action(() => {
14676
+ const result = readSkillPinsSafe();
14677
+ if (!result.ok && result.reason === "missing") {
14678
+ wipeSkillSessions();
14679
+ console.log(chalk23.gray("\nNo pins to clear.\n"));
14680
+ return;
14681
+ }
14682
+ const count = result.ok ? Object.keys(result.pins.roots).length : "?";
14683
+ clearAllPins();
14684
+ wipeSkillSessions();
14685
+ console.log(chalk23.green(`
14686
+ \u{1F513} Cleared ${count} skill pin(s).`));
14687
+ console.log(chalk23.gray(" Next session will re-pin with current state.\n"));
14688
+ });
14689
+ }
14690
+
14691
+ // src/cli.ts
14692
+ var { version } = JSON.parse(
14693
+ fs37.readFileSync(path40.join(__dirname, "../package.json"), "utf-8")
14694
+ );
14695
+ var program = new Command();
14696
+ program.name("node9").description("The Sudo Command for AI Agents").version(version);
14697
+ 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) => {
14698
+ const DEFAULT_API_URL2 = "https://api.node9.ai/api/v1/intercept";
14699
+ const credPath = path40.join(os33.homedir(), ".node9", "credentials.json");
14700
+ if (!fs37.existsSync(path40.dirname(credPath)))
14701
+ fs37.mkdirSync(path40.dirname(credPath), { recursive: true });
14702
+ const profileName = options.profile || "default";
14703
+ let existingCreds = {};
14704
+ try {
14705
+ if (fs37.existsSync(credPath)) {
14706
+ const raw = JSON.parse(fs37.readFileSync(credPath, "utf-8"));
14707
+ if (raw.apiKey) {
14708
+ existingCreds = {
14709
+ default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL2 }
14710
+ };
14711
+ } else {
14712
+ existingCreds = raw;
14713
+ }
14714
+ }
14715
+ } catch {
14716
+ }
14717
+ existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL2 };
14718
+ fs37.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
14719
+ if (profileName === "default") {
14720
+ const configPath = path40.join(os33.homedir(), ".node9", "config.json");
14721
+ let config = {};
14722
+ try {
14723
+ if (fs37.existsSync(configPath))
14724
+ config = JSON.parse(fs37.readFileSync(configPath, "utf-8"));
14725
+ } catch {
14726
+ }
14727
+ if (!config.settings || typeof config.settings !== "object") config.settings = {};
14728
+ const s = config.settings;
14729
+ const approvers = s.approvers || {
14730
+ native: true,
14731
+ browser: true,
14732
+ cloud: true,
14733
+ terminal: true
14734
+ };
14735
+ if (options.local) {
14736
+ approvers.cloud = false;
14737
+ }
14738
+ s.approvers = approvers;
14739
+ if (!fs37.existsSync(path40.dirname(configPath)))
14740
+ fs37.mkdirSync(path40.dirname(configPath), { recursive: true });
14741
+ fs37.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
14742
+ }
14743
+ if (options.profile && profileName !== "default") {
14744
+ console.log(chalk25.green(`\u2705 Profile "${profileName}" saved`));
14745
+ console.log(chalk25.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
12545
14746
  } 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.`));
14747
+ console.log(chalk25.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
14748
+ console.log(chalk25.gray(` All decisions stay on this machine.`));
12548
14749
  } 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.`));
14750
+ console.log(chalk25.green(`\u2705 Logged in \u2014 agent mode`));
14751
+ console.log(chalk25.gray(` Team policy enforced for all calls via Node9 cloud.`));
12551
14752
  }
12552
14753
  });
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) => {
14754
+ 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
14755
  if (target === "gemini") return await setupGemini();
12555
14756
  if (target === "claude") return await setupClaude();
12556
14757
  if (target === "cursor") return await setupCursor();
14758
+ if (target === "windsurf") return await setupWindsurf();
14759
+ if (target === "vscode") return await setupVSCode();
12557
14760
  if (target === "hud") return setupHud();
12558
- console.error(chalk20.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
14761
+ console.error(
14762
+ chalk25.red(
14763
+ `Unknown target: "${target}". Supported: claude, gemini, cursor, windsurf, vscode, hud`
14764
+ )
14765
+ );
12559
14766
  process.exit(1);
12560
14767
  });
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) => {
14768
+ 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
14769
  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");
14770
+ console.log(chalk25.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
14771
+ console.log(" Usage: " + chalk25.white("node9 setup <target>") + "\n");
12565
14772
  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)");
14773
+ console.log(" " + chalk25.green("claude") + " \u2014 Claude Code (hook mode)");
14774
+ console.log(" " + chalk25.green("gemini") + " \u2014 Gemini CLI (hook mode)");
14775
+ console.log(" " + chalk25.green("cursor") + " \u2014 Cursor (MCP proxy)");
14776
+ console.log(" " + chalk25.green("windsurf") + " \u2014 Windsurf (MCP proxy)");
14777
+ console.log(" " + chalk25.green("vscode") + " \u2014 VSCode / Copilot (MCP proxy)");
12569
14778
  process.stdout.write(
12570
- " " + chalk20.green("hud") + " \u2014 Claude Code security statusline\n"
14779
+ " " + chalk25.green("hud") + " \u2014 Claude Code security statusline\n"
12571
14780
  );
12572
14781
  console.log("");
12573
14782
  return;
@@ -12576,93 +14785,108 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
12576
14785
  if (t === "gemini") return await setupGemini();
12577
14786
  if (t === "claude") return await setupClaude();
12578
14787
  if (t === "cursor") return await setupCursor();
14788
+ if (t === "windsurf") return await setupWindsurf();
14789
+ if (t === "vscode") return await setupVSCode();
12579
14790
  if (t === "hud") return setupHud();
12580
- console.error(chalk20.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
14791
+ console.error(
14792
+ chalk25.red(
14793
+ `Unknown target: "${target}". Supported: claude, gemini, cursor, windsurf, vscode, hud`
14794
+ )
14795
+ );
12581
14796
  process.exit(1);
12582
14797
  });
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) => {
14798
+ program.command("removefrom").description("Remove Node9 hooks from an AI agent configuration").addHelpText("after", "\n Supported targets: claude gemini cursor windsurf vscode hud").argument(
14799
+ "<target>",
14800
+ "The agent to remove from: claude | gemini | cursor | windsurf | vscode | hud"
14801
+ ).action((target) => {
12584
14802
  let fn;
12585
14803
  if (target === "claude") fn = teardownClaude;
12586
14804
  else if (target === "gemini") fn = teardownGemini;
12587
14805
  else if (target === "cursor") fn = teardownCursor;
14806
+ else if (target === "windsurf") fn = teardownWindsurf;
14807
+ else if (target === "vscode") fn = teardownVSCode;
12588
14808
  else if (target === "hud") fn = teardownHud;
12589
14809
  else {
12590
14810
  console.error(
12591
- chalk20.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`)
14811
+ chalk25.red(
14812
+ `Unknown target: "${target}". Supported: claude, gemini, cursor, windsurf, vscode, hud`
14813
+ )
12592
14814
  );
12593
14815
  process.exit(1);
12594
14816
  }
12595
- console.log(chalk20.cyan(`
14817
+ console.log(chalk25.cyan(`
12596
14818
  \u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
12597
14819
  `));
12598
14820
  try {
12599
14821
  fn();
12600
14822
  } catch (err2) {
12601
- console.error(chalk20.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
14823
+ console.error(chalk25.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
12602
14824
  process.exit(1);
12603
14825
  }
12604
- console.log(chalk20.gray("\n Restart the agent for changes to take effect."));
14826
+ console.log(chalk25.gray("\n Restart the agent for changes to take effect."));
12605
14827
  });
12606
14828
  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..."));
14829
+ console.log(chalk25.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
14830
+ console.log(chalk25.bold("Stopping daemon..."));
12609
14831
  try {
12610
14832
  stopDaemon();
12611
- console.log(chalk20.green(" \u2705 Daemon stopped"));
14833
+ console.log(chalk25.green(" \u2705 Daemon stopped"));
12612
14834
  } catch {
12613
- console.log(chalk20.blue(" \u2139\uFE0F Daemon was not running"));
14835
+ console.log(chalk25.blue(" \u2139\uFE0F Daemon was not running"));
12614
14836
  }
12615
- console.log(chalk20.bold("\nRemoving hooks..."));
14837
+ console.log(chalk25.bold("\nRemoving hooks..."));
12616
14838
  let teardownFailed = false;
12617
14839
  for (const [label, fn] of [
12618
14840
  ["Claude", teardownClaude],
12619
14841
  ["Gemini", teardownGemini],
12620
- ["Cursor", teardownCursor]
14842
+ ["Cursor", teardownCursor],
14843
+ ["Windsurf", teardownWindsurf],
14844
+ ["VSCode", teardownVSCode]
12621
14845
  ]) {
12622
14846
  try {
12623
14847
  fn();
12624
14848
  } catch (err2) {
12625
14849
  teardownFailed = true;
12626
14850
  console.error(
12627
- chalk20.red(
14851
+ chalk25.red(
12628
14852
  ` \u26A0\uFE0F Failed to remove ${label} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
12629
14853
  )
12630
14854
  );
12631
14855
  }
12632
14856
  }
12633
14857
  if (options.purge) {
12634
- const node9Dir = path34.join(os27.homedir(), ".node9");
12635
- if (fs31.existsSync(node9Dir)) {
14858
+ const node9Dir = path40.join(os33.homedir(), ".node9");
14859
+ if (fs37.existsSync(node9Dir)) {
12636
14860
  const confirmed = await confirm2({
12637
14861
  message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
12638
14862
  default: false
12639
14863
  });
12640
14864
  if (confirmed) {
12641
- fs31.rmSync(node9Dir, { recursive: true });
12642
- if (fs31.existsSync(node9Dir)) {
14865
+ fs37.rmSync(node9Dir, { recursive: true });
14866
+ if (fs37.existsSync(node9Dir)) {
12643
14867
  console.error(
12644
- chalk20.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
14868
+ chalk25.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
12645
14869
  );
12646
14870
  } else {
12647
- console.log(chalk20.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
14871
+ console.log(chalk25.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
12648
14872
  }
12649
14873
  } else {
12650
- console.log(chalk20.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
14874
+ console.log(chalk25.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
12651
14875
  }
12652
14876
  } else {
12653
- console.log(chalk20.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
14877
+ console.log(chalk25.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
12654
14878
  }
12655
14879
  } else {
12656
14880
  console.log(
12657
- chalk20.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
14881
+ chalk25.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
12658
14882
  );
12659
14883
  }
12660
14884
  if (teardownFailed) {
12661
- console.error(chalk20.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
14885
+ console.error(chalk25.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
12662
14886
  process.exit(1);
12663
14887
  }
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"));
14888
+ console.log(chalk25.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
14889
+ console.log(chalk25.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
12666
14890
  });
12667
14891
  registerDoctorCommand(program, version);
12668
14892
  program.command("explain").description(
@@ -12675,7 +14899,7 @@ program.command("explain").description(
12675
14899
  try {
12676
14900
  args = JSON.parse(trimmed);
12677
14901
  } catch {
12678
- console.error(chalk20.red(`
14902
+ console.error(chalk25.red(`
12679
14903
  \u274C Invalid JSON: ${trimmed}
12680
14904
  `));
12681
14905
  process.exit(1);
@@ -12686,54 +14910,54 @@ program.command("explain").description(
12686
14910
  }
12687
14911
  const result = await explainPolicy(tool, args);
12688
14912
  console.log("");
12689
- console.log(chalk20.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
14913
+ console.log(chalk25.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
12690
14914
  console.log("");
12691
- console.log(` ${chalk20.bold("Tool:")} ${chalk20.white(result.tool)}`);
14915
+ console.log(` ${chalk25.bold("Tool:")} ${chalk25.white(result.tool)}`);
12692
14916
  if (argsRaw) {
12693
- const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
12694
- console.log(` ${chalk20.bold("Input:")} ${chalk20.gray(preview)}`);
14917
+ const preview2 = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
14918
+ console.log(` ${chalk25.bold("Input:")} ${chalk25.gray(preview2)}`);
12695
14919
  }
12696
14920
  console.log("");
12697
- console.log(chalk20.bold("Config Sources (Waterfall):"));
14921
+ console.log(chalk25.bold("Config Sources (Waterfall):"));
12698
14922
  for (const tier of result.waterfall) {
12699
- const num2 = chalk20.gray(` ${tier.tier}.`);
14923
+ const num3 = chalk25.gray(` ${tier.tier}.`);
12700
14924
  const label = tier.label.padEnd(16);
12701
14925
  let statusStr;
12702
14926
  if (tier.tier === 1) {
12703
- statusStr = chalk20.gray(tier.note ?? "");
14927
+ statusStr = chalk25.gray(tier.note ?? "");
12704
14928
  } 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 : "");
14929
+ const loc = tier.path ? chalk25.gray(tier.path) : "";
14930
+ const note = tier.note ? chalk25.gray(`(${tier.note})`) : "";
14931
+ statusStr = chalk25.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
12708
14932
  } else {
12709
- statusStr = chalk20.gray("\u25CB " + (tier.note ?? "not found"));
14933
+ statusStr = chalk25.gray("\u25CB " + (tier.note ?? "not found"));
12710
14934
  }
12711
- console.log(`${num2} ${chalk20.white(label)} ${statusStr}`);
14935
+ console.log(`${num3} ${chalk25.white(label)} ${statusStr}`);
12712
14936
  }
12713
14937
  console.log("");
12714
- console.log(chalk20.bold("Policy Evaluation:"));
14938
+ console.log(chalk25.bold("Policy Evaluation:"));
12715
14939
  for (const step of result.steps) {
12716
14940
  const isFinal = step.isFinal;
12717
14941
  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 ");
14942
+ if (step.outcome === "allow") icon = chalk25.green(" \u2705");
14943
+ else if (step.outcome === "review") icon = chalk25.red(" \u{1F534}");
14944
+ else if (step.outcome === "skip") icon = chalk25.gray(" \u2500 ");
14945
+ else icon = chalk25.gray(" \u25CB ");
12722
14946
  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") : "";
14947
+ const nameStr = isFinal ? chalk25.white.bold(name) : chalk25.white(name);
14948
+ const detail = isFinal ? chalk25.white(step.detail) : chalk25.gray(step.detail);
14949
+ const arrow = isFinal ? chalk25.yellow(" \u2190 STOP") : "";
12726
14950
  console.log(`${icon} ${nameStr} ${detail}${arrow}`);
12727
14951
  }
12728
14952
  console.log("");
12729
14953
  if (result.decision === "allow") {
12730
- console.log(chalk20.green.bold(" Decision: \u2705 ALLOW") + chalk20.gray(" \u2014 no approval needed"));
14954
+ console.log(chalk25.green.bold(" Decision: \u2705 ALLOW") + chalk25.gray(" \u2014 no approval needed"));
12731
14955
  } else {
12732
14956
  console.log(
12733
- chalk20.red.bold(" Decision: \u{1F534} REVIEW") + chalk20.gray(" \u2014 human approval required")
14957
+ chalk25.red.bold(" Decision: \u{1F534} REVIEW") + chalk25.gray(" \u2014 human approval required")
12734
14958
  );
12735
14959
  if (result.blockedByLabel) {
12736
- console.log(chalk20.gray(` Reason: ${result.blockedByLabel}`));
14960
+ console.log(chalk25.gray(` Reason: ${result.blockedByLabel}`));
12737
14961
  }
12738
14962
  }
12739
14963
  console.log("");
@@ -12748,7 +14972,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
12748
14972
  try {
12749
14973
  await startTail2(options);
12750
14974
  } catch (err2) {
12751
- console.error(chalk20.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
14975
+ console.error(chalk25.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
12752
14976
  process.exit(1);
12753
14977
  }
12754
14978
  });
@@ -12756,6 +14980,7 @@ registerWatchCommand(program);
12756
14980
  registerMcpGatewayCommand(program);
12757
14981
  registerMcpServerCommand(program);
12758
14982
  registerMcpPinCommand(program);
14983
+ registerSkillPinCommand(program);
12759
14984
  registerCheckCommand(program);
12760
14985
  registerLogCommand(program);
12761
14986
  program.command("hud").description("Render node9 security statusline (spawned by Claude Code statusLine)").addHelpText(
@@ -12780,14 +15005,14 @@ Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
12780
15005
  Run "node9 addto claude" to register it as the statusLine.`
12781
15006
  ).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
15007
  if (subcommand === "debug") {
12783
- const flagFile = path34.join(os27.homedir(), ".node9", "hud-debug");
15008
+ const flagFile = path40.join(os33.homedir(), ".node9", "hud-debug");
12784
15009
  if (state === "on") {
12785
- fs31.mkdirSync(path34.dirname(flagFile), { recursive: true });
12786
- fs31.writeFileSync(flagFile, "");
15010
+ fs37.mkdirSync(path40.dirname(flagFile), { recursive: true });
15011
+ fs37.writeFileSync(flagFile, "");
12787
15012
  console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
12788
15013
  console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
12789
15014
  } else if (state === "off") {
12790
- if (fs31.existsSync(flagFile)) fs31.unlinkSync(flagFile);
15015
+ if (fs37.existsSync(flagFile)) fs37.unlinkSync(flagFile);
12791
15016
  console.log("HUD debug logging disabled.");
12792
15017
  } else {
12793
15018
  console.error("Usage: node9 hud debug on|off");
@@ -12802,7 +15027,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
12802
15027
  const ms = parseDuration(options.duration);
12803
15028
  if (ms === null) {
12804
15029
  console.error(
12805
- chalk20.red(`
15030
+ chalk25.red(`
12806
15031
  \u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
12807
15032
  `)
12808
15033
  );
@@ -12810,20 +15035,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
12810
15035
  }
12811
15036
  pauseNode9(ms, options.duration);
12812
15037
  const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
12813
- console.log(chalk20.yellow(`
15038
+ console.log(chalk25.yellow(`
12814
15039
  \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.
15040
+ console.log(chalk25.gray(` All tool calls will be allowed without review.`));
15041
+ console.log(chalk25.gray(` Run "node9 resume" to re-enable early.
12817
15042
  `));
12818
15043
  });
12819
15044
  program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
12820
15045
  const { paused } = checkPause();
12821
15046
  if (!paused) {
12822
- console.log(chalk20.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
15047
+ console.log(chalk25.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
12823
15048
  return;
12824
15049
  }
12825
15050
  resumeNode9();
12826
- console.log(chalk20.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
15051
+ console.log(chalk25.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
12827
15052
  });
12828
15053
  var HOOK_BASED_AGENTS = {
12829
15054
  claude: "claude",
@@ -12836,15 +15061,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
12836
15061
  if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
12837
15062
  const target = HOOK_BASED_AGENTS[firstArg2];
12838
15063
  console.error(
12839
- chalk20.yellow(`
15064
+ chalk25.yellow(`
12840
15065
  \u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
12841
15066
  );
12842
- console.error(chalk20.white(`
15067
+ console.error(chalk25.white(`
12843
15068
  "${target}" uses its own hook system. Use:`));
12844
15069
  console.error(
12845
- chalk20.green(` node9 addto ${target} `) + chalk20.gray("# one-time setup")
15070
+ chalk25.green(` node9 addto ${target} `) + chalk25.gray("# one-time setup")
12846
15071
  );
12847
- console.error(chalk20.green(` ${target} `) + chalk20.gray("# run normally"));
15072
+ console.error(chalk25.green(` ${target} `) + chalk25.gray("# run normally"));
12848
15073
  process.exit(1);
12849
15074
  }
12850
15075
  const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
@@ -12861,7 +15086,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
12861
15086
  }
12862
15087
  );
12863
15088
  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..."));
15089
+ console.error(chalk25.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
12865
15090
  const daemonReady = await autoStartDaemonAndWait();
12866
15091
  if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
12867
15092
  }
@@ -12874,12 +15099,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
12874
15099
  }
12875
15100
  if (!result.approved) {
12876
15101
  console.error(
12877
- chalk20.red(`
15102
+ chalk25.red(`
12878
15103
  \u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
12879
15104
  );
12880
15105
  process.exit(1);
12881
15106
  }
12882
- console.error(chalk20.green("\n\u2705 Approved \u2014 running command...\n"));
15107
+ console.error(chalk25.green("\n\u2705 Approved \u2014 running command...\n"));
12883
15108
  await runProxy(fullCommand);
12884
15109
  } else {
12885
15110
  program.help();
@@ -12889,14 +15114,18 @@ registerUndoCommand(program);
12889
15114
  registerShieldCommand(program);
12890
15115
  registerConfigShowCommand(program);
12891
15116
  registerTrustCommand(program);
15117
+ registerSyncCommand(program);
15118
+ registerAgentsCommand(program);
15119
+ registerScanCommand(program);
15120
+ registerSessionsCommand(program);
12892
15121
  if (process.argv[2] !== "daemon") {
12893
15122
  process.on("unhandledRejection", (reason) => {
12894
15123
  const isCheckHook = process.argv[2] === "check";
12895
15124
  if (isCheckHook) {
12896
15125
  if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
12897
- const logPath = path34.join(os27.homedir(), ".node9", "hook-debug.log");
15126
+ const logPath = path40.join(os33.homedir(), ".node9", "hook-debug.log");
12898
15127
  const msg = reason instanceof Error ? reason.message : String(reason);
12899
- fs31.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
15128
+ fs37.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
12900
15129
  `);
12901
15130
  }
12902
15131
  process.exit(0);