@node9/proxy 1.7.1 → 1.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -250,6 +250,70 @@ import fs2 from "fs";
250
250
  import path2 from "path";
251
251
  import os2 from "os";
252
252
  import crypto from "crypto";
253
+ function validateShieldDefinition(raw, filePath) {
254
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
255
+ process.stderr.write(`[node9] Shield file is not an object: ${filePath}
256
+ `);
257
+ return null;
258
+ }
259
+ const r = raw;
260
+ if (typeof r.name !== "string" || !r.name) {
261
+ process.stderr.write(`[node9] Shield file missing 'name': ${filePath}
262
+ `);
263
+ return null;
264
+ }
265
+ if (typeof r.description !== "string") {
266
+ process.stderr.write(`[node9] Shield file missing 'description': ${filePath}
267
+ `);
268
+ return null;
269
+ }
270
+ if (!Array.isArray(r.aliases)) {
271
+ process.stderr.write(`[node9] Shield file missing 'aliases' array: ${filePath}
272
+ `);
273
+ return null;
274
+ }
275
+ if (!Array.isArray(r.smartRules)) {
276
+ process.stderr.write(`[node9] Shield file missing 'smartRules' array: ${filePath}
277
+ `);
278
+ return null;
279
+ }
280
+ if (!Array.isArray(r.dangerousWords)) {
281
+ process.stderr.write(`[node9] Shield file missing 'dangerousWords' array: ${filePath}
282
+ `);
283
+ return null;
284
+ }
285
+ return r;
286
+ }
287
+ function loadShieldsFromDir(dir, label) {
288
+ const result = {};
289
+ let entries;
290
+ try {
291
+ entries = fs2.readdirSync(dir).filter((f) => f.endsWith(".json"));
292
+ } catch (err2) {
293
+ if (err2.code !== "ENOENT") {
294
+ process.stderr.write(`[node9] Could not read ${label} shields dir ${dir}: ${String(err2)}
295
+ `);
296
+ }
297
+ return result;
298
+ }
299
+ for (const file of entries) {
300
+ const filePath = path2.join(dir, file);
301
+ try {
302
+ const raw = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
303
+ const shield = validateShieldDefinition(raw, filePath);
304
+ if (shield) result[shield.name] = shield;
305
+ } catch (err2) {
306
+ process.stderr.write(`[node9] Failed to load ${label} shield ${file}: ${String(err2)}
307
+ `);
308
+ }
309
+ }
310
+ return result;
311
+ }
312
+ function buildSHIELDS() {
313
+ const builtins = loadShieldsFromDir(BUILTIN_DIR, "builtin");
314
+ const userShields = loadShieldsFromDir(USER_SHIELDS_DIR, "user");
315
+ return { ...builtins, ...userShields };
316
+ }
253
317
  function resolveShieldName(input) {
254
318
  const lower = input.toLowerCase();
255
319
  if (SHIELDS[lower]) return lower;
@@ -356,255 +420,30 @@ function resolveShieldRule(shieldName, identifier) {
356
420
  }
357
421
  return null;
358
422
  }
359
- var SHIELDS, SHIELDS_STATE_FILE;
423
+ function installShield(name, shieldJson) {
424
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
425
+ throw new Error(
426
+ `Invalid shield name '${name}': only alphanumeric characters, hyphens, and underscores are allowed`
427
+ );
428
+ }
429
+ const shield = validateShieldDefinition(shieldJson, `<downloaded:${name}>`);
430
+ if (!shield) throw new Error(`Downloaded shield '${name}' failed validation`);
431
+ if (shield.name !== name) {
432
+ throw new Error(`Shield name mismatch: file declares '${shield.name}' but expected '${name}'`);
433
+ }
434
+ fs2.mkdirSync(USER_SHIELDS_DIR, { recursive: true });
435
+ const filePath = path2.join(USER_SHIELDS_DIR, `${name}.json`);
436
+ const tmp = `${filePath}.${crypto.randomBytes(6).toString("hex")}.tmp`;
437
+ fs2.writeFileSync(tmp, JSON.stringify(shieldJson, null, 2), { mode: 384 });
438
+ fs2.renameSync(tmp, filePath);
439
+ }
440
+ var BUILTIN_DIR, USER_SHIELDS_DIR, SHIELDS, SHIELDS_STATE_FILE;
360
441
  var init_shields = __esm({
361
442
  "src/shields.ts"() {
362
443
  "use strict";
363
- SHIELDS = {
364
- postgres: {
365
- name: "postgres",
366
- description: "Protects PostgreSQL databases from destructive AI operations",
367
- aliases: ["pg", "postgresql"],
368
- smartRules: [
369
- {
370
- name: "shield:postgres:block-drop-table",
371
- tool: "*",
372
- conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
373
- verdict: "block",
374
- reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
375
- },
376
- {
377
- name: "shield:postgres:block-truncate",
378
- tool: "*",
379
- conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
380
- verdict: "block",
381
- reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
382
- },
383
- {
384
- name: "shield:postgres:block-drop-column",
385
- tool: "*",
386
- conditions: [
387
- { field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
388
- ],
389
- verdict: "block",
390
- reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
391
- },
392
- {
393
- name: "shield:postgres:review-grant-revoke",
394
- tool: "*",
395
- conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
396
- verdict: "review",
397
- reason: "Permission changes require human approval (Postgres shield)"
398
- }
399
- ],
400
- dangerousWords: ["dropdb", "pg_dropcluster"]
401
- },
402
- github: {
403
- name: "github",
404
- description: "Protects GitHub repositories from destructive AI operations",
405
- aliases: ["git"],
406
- smartRules: [
407
- {
408
- // Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
409
- // This rule adds coverage for `git push --delete` which the built-in does not match.
410
- name: "shield:github:review-delete-branch-remote",
411
- tool: "bash",
412
- conditions: [
413
- {
414
- field: "command",
415
- op: "matches",
416
- value: "git\\s+push\\s+.*--delete",
417
- flags: "i"
418
- }
419
- ],
420
- verdict: "review",
421
- reason: "Remote branch deletion requires human approval (GitHub shield)"
422
- },
423
- {
424
- name: "shield:github:block-delete-repo",
425
- tool: "*",
426
- conditions: [
427
- { field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
428
- ],
429
- verdict: "block",
430
- reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
431
- }
432
- ],
433
- dangerousWords: []
434
- },
435
- aws: {
436
- name: "aws",
437
- description: "Protects AWS infrastructure from destructive AI operations",
438
- aliases: ["amazon"],
439
- smartRules: [
440
- {
441
- name: "shield:aws:block-delete-s3-bucket",
442
- tool: "*",
443
- conditions: [
444
- {
445
- field: "command",
446
- op: "matches",
447
- value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
448
- flags: "i"
449
- }
450
- ],
451
- verdict: "block",
452
- reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
453
- },
454
- {
455
- name: "shield:aws:review-iam-changes",
456
- tool: "*",
457
- conditions: [
458
- {
459
- field: "command",
460
- op: "matches",
461
- value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
462
- flags: "i"
463
- }
464
- ],
465
- verdict: "review",
466
- reason: "IAM changes require human approval (AWS shield)"
467
- },
468
- {
469
- name: "shield:aws:block-ec2-terminate",
470
- tool: "*",
471
- conditions: [
472
- {
473
- field: "command",
474
- op: "matches",
475
- value: "aws\\s+ec2\\s+terminate-instances",
476
- flags: "i"
477
- }
478
- ],
479
- verdict: "block",
480
- reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
481
- },
482
- {
483
- name: "shield:aws:review-rds-delete",
484
- tool: "*",
485
- conditions: [
486
- { field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
487
- ],
488
- verdict: "review",
489
- reason: "RDS deletion requires human approval (AWS shield)"
490
- }
491
- ],
492
- dangerousWords: []
493
- },
494
- "bash-safe": {
495
- name: "bash-safe",
496
- description: "Blocks high-risk bash patterns: pipe-to-shell, rm -rf /, disk overwrites, eval",
497
- aliases: ["bash", "shell"],
498
- smartRules: [
499
- {
500
- name: "shield:bash-safe:block-pipe-to-shell",
501
- tool: "bash",
502
- conditions: [
503
- {
504
- field: "command",
505
- op: "matches",
506
- value: "(curl|wget)\\s+[^|]*\\|\\s*(bash|sh|zsh|fish|python3?|ruby|perl|node)",
507
- flags: "i"
508
- }
509
- ],
510
- verdict: "block",
511
- reason: "Pipe-to-shell is a common supply-chain attack vector \u2014 blocked by bash-safe shield"
512
- },
513
- {
514
- name: "shield:bash-safe:block-obfuscated-exec",
515
- tool: "bash",
516
- conditions: [
517
- {
518
- field: "command",
519
- op: "matches",
520
- value: "base64\\s+(-d|--decode).*\\|\\s*(bash|sh|zsh)",
521
- flags: "i"
522
- }
523
- ],
524
- verdict: "block",
525
- reason: "Obfuscated execution via base64 decode \u2014 blocked by bash-safe shield"
526
- },
527
- {
528
- name: "shield:bash-safe:block-rm-root",
529
- tool: "bash",
530
- conditions: [
531
- {
532
- field: "command",
533
- op: "matches",
534
- value: "rm\\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)[a-zA-Z]*\\s+(\\/|~|\\$HOME|\\$\\{HOME\\})\\s*$",
535
- flags: "i"
536
- }
537
- ],
538
- verdict: "block",
539
- reason: "rm -rf of root or home directory is catastrophic \u2014 blocked by bash-safe shield"
540
- },
541
- {
542
- name: "shield:bash-safe:block-disk-overwrite",
543
- tool: "bash",
544
- conditions: [
545
- {
546
- field: "command",
547
- op: "matches",
548
- value: "dd\\s+.*of=\\/dev\\/(sd|nvme|hd|vd|xvd)",
549
- flags: "i"
550
- }
551
- ],
552
- verdict: "block",
553
- reason: "Writing directly to a block device is irreversible \u2014 blocked by bash-safe shield"
554
- },
555
- {
556
- name: "shield:bash-safe:review-eval",
557
- tool: "bash",
558
- conditions: [
559
- {
560
- field: "command",
561
- op: "matches",
562
- value: '\\beval\\s+[\\$`("]',
563
- flags: "i"
564
- }
565
- ],
566
- verdict: "review",
567
- reason: "eval of dynamic content requires human approval (bash-safe shield)"
568
- }
569
- ],
570
- dangerousWords: []
571
- },
572
- filesystem: {
573
- name: "filesystem",
574
- description: "Protects the local filesystem from dangerous AI operations",
575
- aliases: ["fs"],
576
- smartRules: [
577
- {
578
- name: "shield:filesystem:review-chmod-777",
579
- tool: "bash",
580
- conditions: [
581
- { field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
582
- ],
583
- verdict: "review",
584
- reason: "chmod 777 requires human approval (filesystem shield)"
585
- },
586
- {
587
- name: "shield:filesystem:review-write-etc",
588
- tool: "bash",
589
- conditions: [
590
- {
591
- field: "command",
592
- // Narrow to write-indicative operations to avoid approval fatigue on reads.
593
- // Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
594
- op: "matches",
595
- value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
596
- }
597
- ],
598
- verdict: "review",
599
- reason: "Writing to /etc requires human approval (filesystem shield)"
600
- }
601
- ],
602
- // dd removed: too common as a legitimate tool (disk imaging, file ops).
603
- // mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
604
- // wipefs retained: rarely legitimate in an agent context and not in built-ins.
605
- dangerousWords: ["wipefs"]
606
- }
607
- };
444
+ BUILTIN_DIR = path2.join(__dirname, "shields", "builtin");
445
+ USER_SHIELDS_DIR = path2.join(os2.homedir(), ".node9", "shields");
446
+ SHIELDS = buildSHIELDS();
608
447
  SHIELDS_STATE_FILE = path2.join(os2.homedir(), ".node9", "shields.json");
609
448
  }
610
449
  });
@@ -2581,7 +2420,7 @@ function isDaemonRunning() {
2581
2420
  return false;
2582
2421
  }
2583
2422
  }
2584
- async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly) {
2423
+ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched) {
2585
2424
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2586
2425
  const ctrl = new AbortController();
2587
2426
  const timer = setTimeout(() => ctrl.abort(), 5e3);
@@ -2602,7 +2441,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
2602
2441
  ...cwd && { cwd },
2603
2442
  ...recoveryCommand && { recoveryCommand },
2604
2443
  ...skipBackgroundAuth && { skipBackgroundAuth: true },
2605
- ...viewOnly && { viewOnly: true }
2444
+ ...viewOnly && { viewOnly: true },
2445
+ ...localSmartRuleMatched && { localSmartRuleMatched: true }
2606
2446
  }),
2607
2447
  signal: ctrl.signal
2608
2448
  });
@@ -3012,9 +2852,7 @@ end run`;
3012
2852
  "--text",
3013
2853
  pangoMessage,
3014
2854
  "--ok-label",
3015
- locked ? "Waiting..." : "Allow \u21B5",
3016
- "--timeout",
3017
- "300"
2855
+ locked ? "Waiting..." : "Allow \u21B5"
3018
2856
  ];
3019
2857
  if (!locked) {
3020
2858
  argsList.push("--cancel-label", "Block \u238B");
@@ -3292,6 +3130,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3292
3130
  let policyMatchedWord;
3293
3131
  let riskMetadata;
3294
3132
  let statefulRecoveryCommand;
3133
+ let localSmartRuleMatched = false;
3295
3134
  let taintWarning = null;
3296
3135
  if (isNetworkTool(toolName, args)) {
3297
3136
  const filePaths = extractFilePaths(toolName, args);
@@ -3435,6 +3274,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3435
3274
  explainableLabel = policyResult.blockedByLabel || "Local Config";
3436
3275
  policyMatchedField = policyResult.matchedField;
3437
3276
  policyMatchedWord = policyResult.matchedWord;
3277
+ if (policyResult.ruleName) localSmartRuleMatched = true;
3438
3278
  riskMetadata = computeRiskMetadata(
3439
3279
  args,
3440
3280
  policyResult.tier ?? 6,
@@ -3477,22 +3317,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3477
3317
  }
3478
3318
  let cloudRequestId = null;
3479
3319
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
3480
- if (cloudEnforced) {
3320
+ if (cloudEnforced && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
3481
3321
  try {
3482
3322
  const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
3483
3323
  if (!initResult.pending) {
3484
3324
  if (initResult.shadowMode) {
3485
3325
  return { approved: true, checkedBy: "cloud" };
3486
3326
  }
3487
- return {
3488
- approved: !!initResult.approved,
3489
- reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
3490
- checkedBy: initResult.approved ? "cloud" : void 0,
3491
- blockedBy: initResult.approved ? void 0 : "team-policy",
3492
- blockedByLabel: "Organization Policy (SaaS)"
3493
- };
3327
+ if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
3328
+ return {
3329
+ approved: !!initResult.approved,
3330
+ reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
3331
+ checkedBy: initResult.approved ? "cloud" : void 0,
3332
+ blockedBy: initResult.approved ? void 0 : "team-policy",
3333
+ blockedByLabel: "Organization Policy (SaaS)"
3334
+ };
3335
+ }
3336
+ }
3337
+ if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
3338
+ cloudRequestId = initResult.requestId || null;
3494
3339
  }
3495
- cloudRequestId = initResult.requestId || null;
3496
3340
  if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
3497
3341
  } catch {
3498
3342
  }
@@ -3538,7 +3382,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3538
3382
  riskMetadata,
3539
3383
  options?.activityId,
3540
3384
  options?.cwd,
3541
- statefulRecoveryCommand
3385
+ statefulRecoveryCommand,
3386
+ void 0,
3387
+ void 0,
3388
+ localSmartRuleMatched || options?.localSmartRuleMatched
3542
3389
  );
3543
3390
  daemonEntryId = entry.id;
3544
3391
  daemonAllowCount = entry.allowCount;
@@ -3546,7 +3393,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3546
3393
  }
3547
3394
  }
3548
3395
  }
3549
- if (cloudEnforced && cloudRequestId) {
3396
+ if (cloudEnforced && cloudRequestId && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
3550
3397
  racePromises.push(
3551
3398
  (async () => {
3552
3399
  try {
@@ -5825,6 +5672,12 @@ function estimateToolCost(tool, args) {
5825
5672
  const newStr = a.new_string ?? "";
5826
5673
  return String(newStr).length / BYTES_PER_TOKEN / 1e6 * OUTPUT_PRICE_PER_1M;
5827
5674
  }
5675
+ if (t === "bash" || t === "shell" || t === "run_shell_command" || t === "terminal_execute") {
5676
+ const command = String(a.command ?? a.cmd ?? a.input ?? "");
5677
+ if (command.length > 0) {
5678
+ return command.length / BYTES_PER_TOKEN / 1e6 * OUTPUT_PRICE_PER_1M;
5679
+ }
5680
+ }
5828
5681
  return void 0;
5829
5682
  }
5830
5683
  function broadcast(event, data) {
@@ -6213,7 +6066,8 @@ data: ${JSON.stringify(item.data)}
6213
6066
  viewOnly = false,
6214
6067
  fromCLI = false,
6215
6068
  activityId,
6216
- cwd
6069
+ cwd,
6070
+ localSmartRuleMatched = false
6217
6071
  } = JSON.parse(body);
6218
6072
  const id = fromCLI && typeof activityId === "string" && activityId || randomUUID4();
6219
6073
  const entry = {
@@ -6293,7 +6147,7 @@ data: ${JSON.stringify(item.data)}
6293
6147
  agent: typeof agent === "string" ? agent : void 0,
6294
6148
  mcpServer: typeof mcpServer === "string" ? mcpServer : void 0
6295
6149
  },
6296
- { calledFromDaemon: true }
6150
+ { calledFromDaemon: true, localSmartRuleMatched: !!localSmartRuleMatched }
6297
6151
  ).then((result) => {
6298
6152
  const e = pending.get(id);
6299
6153
  if (!e) return;
@@ -6898,7 +6752,7 @@ import fs25 from "fs";
6898
6752
  import os21 from "os";
6899
6753
  import path28 from "path";
6900
6754
  import readline5 from "readline";
6901
- import { spawn as spawn9, execSync as execSync3 } from "child_process";
6755
+ import { spawn as spawn10, execSync as execSync3 } from "child_process";
6902
6756
  function getIcon(tool) {
6903
6757
  const t = tool.toLowerCase();
6904
6758
  for (const [k, v] of Object.entries(ICONS)) {
@@ -6955,7 +6809,7 @@ async function ensureDaemon() {
6955
6809
  } catch {
6956
6810
  }
6957
6811
  console.log(chalk17.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
6958
- const child = spawn9(process.execPath, [process.argv[1], "daemon"], {
6812
+ const child = spawn10(process.execPath, [process.argv[1], "daemon"], {
6959
6813
  detached: true,
6960
6814
  stdio: "ignore",
6961
6815
  env: { ...process.env, NODE9_AUTO_STARTED: "1" }
@@ -7129,6 +6983,7 @@ async function startTail(options = {}) {
7129
6983
  return;
7130
6984
  }
7131
6985
  const connectionTime = Date.now();
6986
+ let initialReplayDone = false;
7132
6987
  const activityPending = /* @__PURE__ */ new Map();
7133
6988
  const orphanedResults = /* @__PURE__ */ new Map();
7134
6989
  let csrfToken = "";
@@ -7460,11 +7315,17 @@ async function startTail(options = {}) {
7460
7315
  return;
7461
7316
  }
7462
7317
  if (event === "activity") {
7318
+ const isReplayEvent = data.status && data.status !== "pending";
7319
+ if (isReplayEvent && !initialReplayDone) {
7320
+ renderResult(data, data);
7321
+ return;
7322
+ }
7463
7323
  if (!options.history && data.ts > 0 && data.ts < connectionTime) return;
7464
- if (data.status && data.status !== "pending") {
7324
+ if (isReplayEvent) {
7465
7325
  renderResult(data, data);
7466
7326
  return;
7467
7327
  }
7328
+ if (!initialReplayDone) initialReplayDone = true;
7468
7329
  const orphaned = orphanedResults.get(data.id);
7469
7330
  if (orphaned) {
7470
7331
  orphanedResults.delete(data.id);
@@ -7845,6 +7706,24 @@ function renderContextLine(stdin) {
7845
7706
  async function main() {
7846
7707
  try {
7847
7708
  const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
7709
+ if (fs26.existsSync(path29.join(os22.homedir(), ".node9", "hud-debug"))) {
7710
+ try {
7711
+ const logPath = path29.join(os22.homedir(), ".node9", "hud-debug.log");
7712
+ const MAX_LOG_SIZE = 10 * 1024 * 1024;
7713
+ let size = 0;
7714
+ try {
7715
+ size = fs26.statSync(logPath).size;
7716
+ } catch {
7717
+ }
7718
+ if (size < MAX_LOG_SIZE) {
7719
+ fs26.appendFileSync(
7720
+ logPath,
7721
+ JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
7722
+ );
7723
+ }
7724
+ } catch {
7725
+ }
7726
+ }
7848
7727
  if (!daemonStatus2) {
7849
7728
  renderOffline();
7850
7729
  return;
@@ -7957,7 +7836,7 @@ function isNode9Hook(cmd) {
7957
7836
  function teardownClaude() {
7958
7837
  const homeDir2 = os10.homedir();
7959
7838
  const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
7960
- const mcpPath = path14.join(homeDir2, ".claude.json");
7839
+ const mcpPath = path14.join(homeDir2, ".claude", ".mcp.json");
7961
7840
  let changed = false;
7962
7841
  const settings = readJson(hooksPath);
7963
7842
  if (settings?.hooks) {
@@ -7983,11 +7862,12 @@ function teardownClaude() {
7983
7862
  let mcpChanged = false;
7984
7863
  if (removeNode9McpServer(claudeConfig.mcpServers)) {
7985
7864
  mcpChanged = true;
7986
- console.log(chalk.green(" \u2705 Removed node9 MCP server entry from ~/.claude.json"));
7865
+ console.log(chalk.green(" \u2705 Removed node9 MCP server entry from ~/.claude/.mcp.json"));
7987
7866
  }
7988
7867
  for (const [name, server] of Object.entries(claudeConfig.mcpServers)) {
7989
- if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
7990
- const [originalCmd, ...originalArgs] = server.args;
7868
+ const args = server.args;
7869
+ if (server.command === "node9" && Array.isArray(args) && args[0] === "mcp" && args[1] === "--upstream" && typeof args[2] === "string") {
7870
+ const [originalCmd, ...originalArgs] = args[2].split(" ");
7991
7871
  claudeConfig.mcpServers[name] = {
7992
7872
  ...server,
7993
7873
  command: originalCmd,
@@ -7995,16 +7875,11 @@ function teardownClaude() {
7995
7875
  };
7996
7876
  mcpChanged = true;
7997
7877
  } else if (server.command === "node9") {
7998
- console.warn(
7999
- chalk.yellow(
8000
- ` \u26A0\uFE0F Cannot unwrap MCP server "${name}" in ~/.claude.json \u2014 args is empty. Remove it manually.`
8001
- )
8002
- );
8003
7878
  }
8004
7879
  }
8005
7880
  if (mcpChanged) {
8006
7881
  writeJson(mcpPath, claudeConfig);
8007
- console.log(chalk.green(" \u2705 Unwrapped MCP servers in ~/.claude.json"));
7882
+ console.log(chalk.green(" \u2705 Unwrapped MCP servers in ~/.claude/.mcp.json"));
8008
7883
  }
8009
7884
  }
8010
7885
  }
@@ -8033,8 +7908,9 @@ function teardownGemini() {
8033
7908
  console.log(chalk.green(" \u2705 Removed node9 MCP server entry from ~/.gemini/settings.json"));
8034
7909
  }
8035
7910
  for (const [name, server] of Object.entries(settings.mcpServers)) {
8036
- if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
8037
- const [originalCmd, ...originalArgs] = server.args;
7911
+ const args = server.args;
7912
+ if (server.command === "node9" && Array.isArray(args) && args[0] === "mcp" && args[1] === "--upstream" && typeof args[2] === "string") {
7913
+ const [originalCmd, ...originalArgs] = args[2].split(" ");
8038
7914
  settings.mcpServers[name] = {
8039
7915
  ...server,
8040
7916
  command: originalCmd,
@@ -8065,8 +7941,9 @@ function teardownCursor() {
8065
7941
  console.log(chalk.green(" \u2705 Removed node9 MCP server entry from ~/.cursor/mcp.json"));
8066
7942
  }
8067
7943
  for (const [name, server] of Object.entries(mcpConfig.mcpServers)) {
8068
- if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
8069
- const [originalCmd, ...originalArgs] = server.args;
7944
+ const args = server.args;
7945
+ if (server.command === "node9" && Array.isArray(args) && args[0] === "mcp" && args[1] === "--upstream" && typeof args[2] === "string") {
7946
+ const [originalCmd, ...originalArgs] = args[2].split(" ");
8070
7947
  mcpConfig.mcpServers[name] = {
8071
7948
  ...server,
8072
7949
  command: originalCmd,
@@ -8084,7 +7961,7 @@ function teardownCursor() {
8084
7961
  }
8085
7962
  async function setupClaude() {
8086
7963
  const homeDir2 = os10.homedir();
8087
- const mcpPath = path14.join(homeDir2, ".claude.json");
7964
+ const mcpPath = path14.join(homeDir2, ".claude", ".mcp.json");
8088
7965
  const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
8089
7966
  const claudeConfig = readJson(mcpPath) ?? {};
8090
7967
  const settings = readJson(hooksPath) ?? {};
@@ -8099,7 +7976,7 @@ async function setupClaude() {
8099
7976
  if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
8100
7977
  settings.hooks.PreToolUse.push({
8101
7978
  matcher: ".*",
8102
- hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
7979
+ hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 600 }]
8103
7980
  });
8104
7981
  console.log(chalk.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
8105
7982
  hooksChanged = true;
@@ -8125,6 +8002,15 @@ async function setupClaude() {
8125
8002
  console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
8126
8003
  anythingChanged = true;
8127
8004
  }
8005
+ const hudCommand = fullPathCommand("hud");
8006
+ const statusLineObj = { type: "command", command: hudCommand };
8007
+ const existingStatusLine = settings.statusLine;
8008
+ const existingStatusCommand = typeof existingStatusLine === "object" ? existingStatusLine?.command : existingStatusLine;
8009
+ if (existingStatusCommand !== hudCommand) {
8010
+ settings.statusLine = statusLineObj;
8011
+ hooksChanged = true;
8012
+ anythingChanged = true;
8013
+ }
8128
8014
  if (hooksChanged) {
8129
8015
  writeJson(hooksPath, settings);
8130
8016
  console.log("");
@@ -8132,20 +8018,24 @@ async function setupClaude() {
8132
8018
  const serversToWrap = [];
8133
8019
  for (const [name, server] of Object.entries(servers)) {
8134
8020
  if (!server.command || server.command === "node9") continue;
8135
- const parts = [server.command, ...server.args ?? []];
8136
- serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
8021
+ const upstream = [server.command, ...server.args ?? []].join(" ");
8022
+ serversToWrap.push({ name, upstream });
8137
8023
  }
8138
8024
  if (serversToWrap.length > 0) {
8139
8025
  console.log(chalk.bold("The following existing entries will be modified:\n"));
8140
8026
  console.log(chalk.white(` ${mcpPath}`));
8141
- for (const { name, originalCmd } of serversToWrap) {
8142
- console.log(chalk.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
8027
+ for (const { name, upstream } of serversToWrap) {
8028
+ console.log(chalk.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
8143
8029
  }
8144
8030
  console.log("");
8145
8031
  const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
8146
8032
  if (proceed) {
8147
- for (const { name, parts } of serversToWrap) {
8148
- servers[name] = { ...servers[name], command: "node9", args: parts };
8033
+ for (const { name, upstream } of serversToWrap) {
8034
+ servers[name] = {
8035
+ ...servers[name],
8036
+ command: "node9",
8037
+ args: ["mcp", "--upstream", upstream]
8038
+ };
8149
8039
  }
8150
8040
  claudeConfig.mcpServers = servers;
8151
8041
  writeJson(mcpPath, claudeConfig);
@@ -8225,20 +8115,24 @@ async function setupGemini() {
8225
8115
  const serversToWrap = [];
8226
8116
  for (const [name, server] of Object.entries(servers)) {
8227
8117
  if (!server.command || server.command === "node9") continue;
8228
- const parts = [server.command, ...server.args ?? []];
8229
- serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
8118
+ const upstream = [server.command, ...server.args ?? []].join(" ");
8119
+ serversToWrap.push({ name, upstream });
8230
8120
  }
8231
8121
  if (serversToWrap.length > 0) {
8232
8122
  console.log(chalk.bold("The following existing entries will be modified:\n"));
8233
8123
  console.log(chalk.white(` ${settingsPath} (mcpServers)`));
8234
- for (const { name, originalCmd } of serversToWrap) {
8235
- console.log(chalk.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
8124
+ for (const { name, upstream } of serversToWrap) {
8125
+ console.log(chalk.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
8236
8126
  }
8237
8127
  console.log("");
8238
8128
  const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
8239
8129
  if (proceed) {
8240
- for (const { name, parts } of serversToWrap) {
8241
- servers[name] = { ...servers[name], command: "node9", args: parts };
8130
+ for (const { name, upstream } of serversToWrap) {
8131
+ servers[name] = {
8132
+ ...servers[name],
8133
+ command: "node9",
8134
+ args: ["mcp", "--upstream", upstream]
8135
+ };
8242
8136
  }
8243
8137
  settings.mcpServers = servers;
8244
8138
  writeJson(settingsPath, settings);
@@ -8297,20 +8191,24 @@ async function setupCursor() {
8297
8191
  const serversToWrap = [];
8298
8192
  for (const [name, server] of Object.entries(servers)) {
8299
8193
  if (!server.command || server.command === "node9") continue;
8300
- const parts = [server.command, ...server.args ?? []];
8301
- serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
8194
+ const upstream = [server.command, ...server.args ?? []].join(" ");
8195
+ serversToWrap.push({ name, upstream });
8302
8196
  }
8303
8197
  if (serversToWrap.length > 0) {
8304
8198
  console.log(chalk.bold("The following existing entries will be modified:\n"));
8305
8199
  console.log(chalk.white(` ${mcpPath}`));
8306
- for (const { name, originalCmd } of serversToWrap) {
8307
- console.log(chalk.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
8200
+ for (const { name, upstream } of serversToWrap) {
8201
+ console.log(chalk.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
8308
8202
  }
8309
8203
  console.log("");
8310
8204
  const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
8311
8205
  if (proceed) {
8312
- for (const { name, parts } of serversToWrap) {
8313
- servers[name] = { ...servers[name], command: "node9", args: parts };
8206
+ for (const { name, upstream } of serversToWrap) {
8207
+ servers[name] = {
8208
+ ...servers[name],
8209
+ command: "node9",
8210
+ args: ["mcp", "--upstream", upstream]
8211
+ };
8314
8212
  }
8315
8213
  mcpConfig.mcpServers = servers;
8316
8214
  writeJson(mcpPath, mcpConfig);
@@ -8373,20 +8271,24 @@ async function setupCodex() {
8373
8271
  const serversToWrap = [];
8374
8272
  for (const [name, server] of Object.entries(servers)) {
8375
8273
  if (!server.command || server.command === "node9") continue;
8376
- const parts = [server.command, ...server.args ?? []];
8377
- serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
8274
+ const upstream = [server.command, ...server.args ?? []].join(" ");
8275
+ serversToWrap.push({ name, upstream });
8378
8276
  }
8379
8277
  if (serversToWrap.length > 0) {
8380
8278
  console.log(chalk.bold("The following existing entries will be modified:\n"));
8381
8279
  console.log(chalk.white(` ${configPath}`));
8382
- for (const { name, originalCmd } of serversToWrap) {
8383
- console.log(chalk.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
8280
+ for (const { name, upstream } of serversToWrap) {
8281
+ console.log(chalk.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
8384
8282
  }
8385
8283
  console.log("");
8386
8284
  const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
8387
8285
  if (proceed) {
8388
- for (const { name, parts } of serversToWrap) {
8389
- servers[name] = { ...servers[name], command: "node9", args: parts };
8286
+ for (const { name, upstream } of serversToWrap) {
8287
+ servers[name] = {
8288
+ ...servers[name],
8289
+ command: "node9",
8290
+ args: ["mcp", "--upstream", upstream]
8291
+ };
8390
8292
  }
8391
8293
  config.mcp_servers = servers;
8392
8294
  writeToml(configPath, config);
@@ -8575,18 +8477,20 @@ async function runProxy(targetCommand) {
8575
8477
  const cmd = commandParts[0];
8576
8478
  const args = commandParts.slice(1);
8577
8479
  let executable = cmd;
8480
+ let useShell = false;
8578
8481
  try {
8579
8482
  const { stdout } = await execa("which", [cmd]);
8580
8483
  if (stdout) executable = stdout.trim();
8581
8484
  } catch {
8485
+ useShell = true;
8582
8486
  }
8583
8487
  console.error(chalk4.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
8584
- const child = spawn3(executable, args, {
8488
+ const spawnEnv = { ...process.env, FORCE_COLOR: "1" };
8489
+ const child = useShell ? spawn3("/bin/bash", ["-c", targetCommand], {
8585
8490
  stdio: ["pipe", "pipe", "inherit"],
8586
- // We control STDIN and STDOUT
8587
8491
  shell: false,
8588
- env: { ...process.env, FORCE_COLOR: "1" }
8589
- });
8492
+ env: spawnEnv
8493
+ }) : spawn3(executable, args, { stdio: ["pipe", "pipe", "inherit"], shell: false, env: spawnEnv });
8590
8494
  const agentIn = readline.createInterface({ input: process.stdin, terminal: false });
8591
8495
  agentIn.on("line", async (line) => {
8592
8496
  let message;
@@ -8699,6 +8603,7 @@ init_config();
8699
8603
  init_policy();
8700
8604
  import chalk5 from "chalk";
8701
8605
  import fs18 from "fs";
8606
+ import { spawn as spawn6 } from "child_process";
8702
8607
  import path20 from "path";
8703
8608
  import os14 from "os";
8704
8609
 
@@ -9078,6 +8983,37 @@ RAW: ${raw}
9078
8983
  process.exit(0);
9079
8984
  }
9080
8985
  const config = getConfig(payload.cwd || void 0);
8986
+ if (config.settings.autoStartDaemon && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON) {
8987
+ try {
8988
+ const scriptPath = process.argv[1];
8989
+ if (typeof scriptPath !== "string" || !path20.isAbsolute(scriptPath))
8990
+ throw new Error("node9: argv[1] is not an absolute path");
8991
+ const resolvedScript = fs18.realpathSync(scriptPath);
8992
+ const expectedCli = fs18.realpathSync(path20.resolve(__dirname, "../../cli.js"));
8993
+ if (resolvedScript !== expectedCli)
8994
+ throw new Error(
8995
+ "node9: daemon spawn aborted \u2014 argv[1] does not resolve to the node9 CLI"
8996
+ );
8997
+ const safeEnv = { ...process.env };
8998
+ for (const key of [
8999
+ "NODE_OPTIONS",
9000
+ "LD_PRELOAD",
9001
+ "LD_LIBRARY_PATH",
9002
+ "DYLD_INSERT_LIBRARIES",
9003
+ "NODE_PATH",
9004
+ "ELECTRON_RUN_AS_NODE"
9005
+ ]) {
9006
+ delete safeEnv[key];
9007
+ }
9008
+ const d = spawn6(process.execPath, [scriptPath, "daemon"], {
9009
+ detached: true,
9010
+ stdio: "ignore",
9011
+ env: { ...safeEnv, NODE9_AUTO_STARTED: "1", NODE9_BROWSER_OPENED: "1" }
9012
+ });
9013
+ d.unref();
9014
+ } catch {
9015
+ }
9016
+ }
9081
9017
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
9082
9018
  const logPath = path20.join(os14.homedir(), ".node9", "hook-debug.log");
9083
9019
  if (!fs18.existsSync(path20.dirname(logPath)))
@@ -9136,7 +9072,7 @@ RAW: ${raw}
9136
9072
  }
9137
9073
  }) + "\n"
9138
9074
  );
9139
- process.exit(0);
9075
+ process.exit(2);
9140
9076
  };
9141
9077
  if (!toolName) {
9142
9078
  sendBlock("Node9: unrecognised hook payload \u2014 tool name missing.");
@@ -9371,6 +9307,27 @@ init_shields();
9371
9307
  init_audit();
9372
9308
  init_config();
9373
9309
  import chalk6 from "chalk";
9310
+
9311
+ // src/utils/https-fetch.ts
9312
+ import https from "https";
9313
+ function httpsFetch(url) {
9314
+ return new Promise((resolve, reject) => {
9315
+ https.get(url, (res) => {
9316
+ if (res.statusCode !== 200) {
9317
+ reject(new Error(`HTTP ${String(res.statusCode)} for ${url}`));
9318
+ res.resume();
9319
+ return;
9320
+ }
9321
+ const chunks = [];
9322
+ res.on("data", (chunk) => chunks.push(chunk));
9323
+ res.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
9324
+ res.on("error", reject);
9325
+ }).on("error", reject);
9326
+ });
9327
+ }
9328
+
9329
+ // src/cli/commands/shield.ts
9330
+ var COMMUNITY_INDEX_URL = "https://raw.githubusercontent.com/node9ai/node9-proxy/main/shields/community/index.json";
9374
9331
  function registerShieldCommand(program2) {
9375
9332
  const shieldCmd = program2.command("shield").description("Manage pre-packaged security shield templates");
9376
9333
  shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
@@ -9430,7 +9387,32 @@ function registerShieldCommand(program2) {
9430
9387
  \u{1F6E1}\uFE0F Shield "${name}" disabled.
9431
9388
  `));
9432
9389
  });
9433
- shieldCmd.command("list").description("Show all available shields").action(() => {
9390
+ shieldCmd.command("list").description("Show available shields (add --community to browse the marketplace)").option("--community", "List shields available from the community marketplace").action((opts) => {
9391
+ if (opts.community) {
9392
+ console.log(chalk6.bold("\n\u{1F6E1}\uFE0F Community Shield Marketplace\n"));
9393
+ console.log(chalk6.gray(" Fetching index\u2026\n"));
9394
+ httpsFetch(COMMUNITY_INDEX_URL).then((body) => {
9395
+ const entries = JSON.parse(body);
9396
+ const installed = new Set(listShields().map((s) => s.name));
9397
+ for (const e of entries) {
9398
+ const tag = installed.has(e.name) ? chalk6.green("installed") : chalk6.gray("available");
9399
+ console.log(
9400
+ ` ${tag} ${chalk6.cyan(e.name.padEnd(12))} ${e.description} ${chalk6.gray(`by ${e.author}`)}`
9401
+ );
9402
+ }
9403
+ console.log("");
9404
+ console.log(
9405
+ chalk6.gray(` Install a shield: ${chalk6.cyan("node9 shield install <name>")}
9406
+ `)
9407
+ );
9408
+ }).catch((err2) => {
9409
+ console.error(chalk6.red(`
9410
+ \u274C Could not fetch community index: ${String(err2)}
9411
+ `));
9412
+ process.exit(1);
9413
+ });
9414
+ return;
9415
+ }
9434
9416
  const active = new Set(readActiveShields());
9435
9417
  console.log(chalk6.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
9436
9418
  for (const shield of listShields()) {
@@ -9440,6 +9422,10 @@ function registerShieldCommand(program2) {
9440
9422
  console.log(chalk6.gray(` aliases: ${shield.aliases.join(", ")}`));
9441
9423
  }
9442
9424
  console.log("");
9425
+ console.log(
9426
+ chalk6.gray(` Browse community shields: ${chalk6.cyan("node9 shield list --community")}
9427
+ `)
9428
+ );
9443
9429
  });
9444
9430
  shieldCmd.command("status").description("Show active shields and their individual rules with verdicts").action(() => {
9445
9431
  const active = readActiveShields();
@@ -9577,6 +9563,52 @@ function registerShieldCommand(program2) {
9577
9563
  `)
9578
9564
  );
9579
9565
  });
9566
+ shieldCmd.command("install <name>").description("Install a shield from the community marketplace into ~/.node9/shields/").action((name) => {
9567
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
9568
+ console.error(
9569
+ chalk6.red(
9570
+ `
9571
+ \u274C Invalid shield name: only alphanumeric characters, hyphens, and underscores are allowed
9572
+ `
9573
+ )
9574
+ );
9575
+ process.exit(1);
9576
+ }
9577
+ console.log(chalk6.bold(`
9578
+ \u{1F6E1}\uFE0F Installing shield "${name}"\u2026
9579
+ `));
9580
+ httpsFetch(COMMUNITY_INDEX_URL).then((indexBody) => {
9581
+ const entries = JSON.parse(indexBody);
9582
+ const entry = entries.find((e) => e.name === name);
9583
+ if (!entry) {
9584
+ const names = entries.map((e) => chalk6.cyan(e.name)).join(", ");
9585
+ console.error(
9586
+ chalk6.red(`\u274C Shield "${name}" not found in the community marketplace.
9587
+ `)
9588
+ );
9589
+ console.error(` Available: ${names}
9590
+ `);
9591
+ process.exit(1);
9592
+ }
9593
+ return httpsFetch(entry.url);
9594
+ }).then((shieldBody) => {
9595
+ const shieldJson = JSON.parse(shieldBody);
9596
+ installShield(name, shieldJson);
9597
+ console.log(
9598
+ chalk6.green(`\u2705 Shield "${name}" installed to ~/.node9/shields/${name}.json`)
9599
+ );
9600
+ console.log(
9601
+ chalk6.gray(` Activate it with: ${chalk6.cyan(`node9 shield enable ${name}`)}
9602
+ `)
9603
+ );
9604
+ appendConfigAudit({ event: "shield-install", shield: name });
9605
+ }).catch((err2) => {
9606
+ console.error(chalk6.red(`
9607
+ \u274C Install failed: ${String(err2)}
9608
+ `));
9609
+ process.exit(1);
9610
+ });
9611
+ });
9580
9612
  }
9581
9613
  function registerConfigShowCommand(program2) {
9582
9614
  program2.command("config show").description(
@@ -9893,7 +9925,7 @@ function registerAuditCommand(program2) {
9893
9925
  init_daemon2();
9894
9926
  init_daemon();
9895
9927
  import chalk9 from "chalk";
9896
- import { spawn as spawn6 } from "child_process";
9928
+ import { spawn as spawn7 } from "child_process";
9897
9929
  function registerDaemonCommand(program2) {
9898
9930
  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(
9899
9931
  "-w, --watch",
@@ -9924,7 +9956,7 @@ function registerDaemonCommand(program2) {
9924
9956
  console.log(chalk9.green(`\u{1F310} Opened browser: http://${DAEMON_HOST}:${DAEMON_PORT}/`));
9925
9957
  process.exit(0);
9926
9958
  }
9927
- const child = spawn6(process.execPath, [process.argv[1], "daemon"], {
9959
+ const child = spawn7(process.execPath, [process.argv[1], "daemon"], {
9928
9960
  detached: true,
9929
9961
  stdio: "ignore"
9930
9962
  });
@@ -9939,7 +9971,7 @@ function registerDaemonCommand(program2) {
9939
9971
  process.exit(0);
9940
9972
  }
9941
9973
  if (options.background) {
9942
- const child = spawn6(process.execPath, [process.argv[1], "daemon"], {
9974
+ const child = spawn7(process.execPath, [process.argv[1], "daemon"], {
9943
9975
  detached: true,
9944
9976
  stdio: "ignore"
9945
9977
  });
@@ -10111,7 +10143,7 @@ import chalk11 from "chalk";
10111
10143
  import fs23 from "fs";
10112
10144
  import path25 from "path";
10113
10145
  import os19 from "os";
10114
- import https from "https";
10146
+ import https2 from "https";
10115
10147
  init_shields();
10116
10148
  var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
10117
10149
  function fireTelemetryPing(agents) {
@@ -10122,7 +10154,7 @@ function fireTelemetryPing(agents) {
10122
10154
  os: process.platform,
10123
10155
  node9_version: process.env.npm_package_version ?? "unknown"
10124
10156
  });
10125
- const req = https.request(
10157
+ const req = https2.request(
10126
10158
  {
10127
10159
  hostname: "api.node9.ai",
10128
10160
  path: "/api/v1/telemetry",
@@ -10528,7 +10560,7 @@ function registerUndoCommand(program2) {
10528
10560
  // src/cli/commands/watch.ts
10529
10561
  init_daemon();
10530
10562
  import chalk14 from "chalk";
10531
- import { spawn as spawn7, spawnSync as spawnSync5 } from "child_process";
10563
+ import { spawn as spawn8, spawnSync as spawnSync5 } from "child_process";
10532
10564
  function registerWatchCommand(program2) {
10533
10565
  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) => {
10534
10566
  let port = DAEMON_PORT;
@@ -10544,7 +10576,7 @@ function registerWatchCommand(program2) {
10544
10576
  }
10545
10577
  } catch {
10546
10578
  console.error(chalk14.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
10547
- const child = spawn7(process.execPath, [process.argv[1], "daemon"], {
10579
+ const child = spawn8(process.execPath, [process.argv[1], "daemon"], {
10548
10580
  detached: true,
10549
10581
  stdio: "ignore",
10550
10582
  env: { ...process.env, NODE9_AUTO_STARTED: "1", NODE9_WATCH_MODE: "1" }
@@ -10590,7 +10622,7 @@ function registerWatchCommand(program2) {
10590
10622
  init_orchestrator();
10591
10623
  import readline3 from "readline";
10592
10624
  import chalk15 from "chalk";
10593
- import { spawn as spawn8 } from "child_process";
10625
+ import { spawn as spawn9 } from "child_process";
10594
10626
  import { execa as execa2 } from "execa";
10595
10627
  init_provenance();
10596
10628
  function sanitize4(value) {
@@ -10677,7 +10709,7 @@ async function runMcpGateway(upstreamCommand) {
10677
10709
  const safeEnv = Object.fromEntries(
10678
10710
  Object.entries(process.env).filter(([k]) => !UPSTREAM_INJECTOR_VARS.has(k))
10679
10711
  );
10680
- const child = spawn8(executable, cmdArgs, {
10712
+ const child = spawn9(executable, cmdArgs, {
10681
10713
  stdio: ["pipe", "pipe", "inherit"],
10682
10714
  // control stdin/stdout; inherit stderr
10683
10715
  shell: false,
@@ -10760,8 +10792,11 @@ async function runMcpGateway(upstreamCommand) {
10760
10792
  return;
10761
10793
  } finally {
10762
10794
  authPending = false;
10763
- agentIn.resume();
10764
- if (deferredStdinEnd) child.stdin.end();
10795
+ if (deferredStdinEnd) {
10796
+ child.stdin.end();
10797
+ } else {
10798
+ agentIn.resume();
10799
+ }
10765
10800
  if (deferredExitCode !== null) process.exit(deferredExitCode);
10766
10801
  }
10767
10802
  return;
@@ -11496,7 +11531,43 @@ registerMcpGatewayCommand(program);
11496
11531
  registerMcpServerCommand(program);
11497
11532
  registerCheckCommand(program);
11498
11533
  registerLogCommand(program);
11499
- program.command("hud").description("Render node9 security statusline (spawned by Claude Code statusLine)").action(async () => {
11534
+ program.command("hud").description("Render node9 security statusline (spawned by Claude Code statusLine)").addHelpText(
11535
+ "after",
11536
+ `
11537
+ Outputs up to 3 lines to stdout, then exits:
11538
+
11539
+ Line 1 \u2014 Security state (always shown):
11540
+ \u{1F6E1} node9 | <mode> [shields] | \u2705 allowed \u{1F6D1} blocked \u{1F6A8} dlp ~$cost
11541
+ Shows "offline" if the node9 daemon is not running.
11542
+
11543
+ Line 2 \u2014 Claude context & rate limits (shown when available):
11544
+ <model> \u2502 ctx \u2588\u2588\u2588\u2588\u2588\u2588\u2591\u2591\u2591\u2591 61% \u2502 5h \u2588\u2588\u2588\u2588\u2591\u2591\u2591\u2591\u2591\u2591 40% (2h 10m left)
11545
+ Only appears when Claude Code passes context_window / rate_limits data via stdin.
11546
+
11547
+ Line 3 \u2014 Environment counts (shown when non-zero):
11548
+ 2 CLAUDE.md | 5 rules | 4 MCPs | 3 hooks
11549
+ Counts CLAUDE.md files, rules/, MCP servers, and hook entries across user + project scope.
11550
+ Disable with: { "settings": { "hud": { "showEnvironmentCounts": false } } } in node9.config.json
11551
+
11552
+ Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
11553
+ Run "node9 addto claude" to register it as the statusLine.`
11554
+ ).argument("[subcommand]", 'Optional: "debug on" / "debug off" to toggle stdin logging').argument("[state]", 'on|off \u2014 used with "debug" subcommand').action(async (subcommand, state) => {
11555
+ if (subcommand === "debug") {
11556
+ const flagFile = path30.join(os23.homedir(), ".node9", "hud-debug");
11557
+ if (state === "on") {
11558
+ fs27.mkdirSync(path30.dirname(flagFile), { recursive: true });
11559
+ fs27.writeFileSync(flagFile, "");
11560
+ console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
11561
+ console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
11562
+ } else if (state === "off") {
11563
+ if (fs27.existsSync(flagFile)) fs27.unlinkSync(flagFile);
11564
+ console.log("HUD debug logging disabled.");
11565
+ } else {
11566
+ console.error("Usage: node9 hud debug on|off");
11567
+ process.exit(1);
11568
+ }
11569
+ return;
11570
+ }
11500
11571
  const { main: main2 } = await Promise.resolve().then(() => (init_hud(), hud_exports));
11501
11572
  await main2();
11502
11573
  });