@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.js CHANGED
@@ -268,6 +268,70 @@ var init_config_schema = __esm({
268
268
  });
269
269
 
270
270
  // src/shields.ts
271
+ function validateShieldDefinition(raw, filePath) {
272
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
273
+ process.stderr.write(`[node9] Shield file is not an object: ${filePath}
274
+ `);
275
+ return null;
276
+ }
277
+ const r = raw;
278
+ if (typeof r.name !== "string" || !r.name) {
279
+ process.stderr.write(`[node9] Shield file missing 'name': ${filePath}
280
+ `);
281
+ return null;
282
+ }
283
+ if (typeof r.description !== "string") {
284
+ process.stderr.write(`[node9] Shield file missing 'description': ${filePath}
285
+ `);
286
+ return null;
287
+ }
288
+ if (!Array.isArray(r.aliases)) {
289
+ process.stderr.write(`[node9] Shield file missing 'aliases' array: ${filePath}
290
+ `);
291
+ return null;
292
+ }
293
+ if (!Array.isArray(r.smartRules)) {
294
+ process.stderr.write(`[node9] Shield file missing 'smartRules' array: ${filePath}
295
+ `);
296
+ return null;
297
+ }
298
+ if (!Array.isArray(r.dangerousWords)) {
299
+ process.stderr.write(`[node9] Shield file missing 'dangerousWords' array: ${filePath}
300
+ `);
301
+ return null;
302
+ }
303
+ return r;
304
+ }
305
+ function loadShieldsFromDir(dir, label) {
306
+ const result = {};
307
+ let entries;
308
+ try {
309
+ entries = import_fs2.default.readdirSync(dir).filter((f) => f.endsWith(".json"));
310
+ } catch (err2) {
311
+ if (err2.code !== "ENOENT") {
312
+ process.stderr.write(`[node9] Could not read ${label} shields dir ${dir}: ${String(err2)}
313
+ `);
314
+ }
315
+ return result;
316
+ }
317
+ for (const file of entries) {
318
+ const filePath = import_path2.default.join(dir, file);
319
+ try {
320
+ const raw = JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
321
+ const shield = validateShieldDefinition(raw, filePath);
322
+ if (shield) result[shield.name] = shield;
323
+ } catch (err2) {
324
+ process.stderr.write(`[node9] Failed to load ${label} shield ${file}: ${String(err2)}
325
+ `);
326
+ }
327
+ }
328
+ return result;
329
+ }
330
+ function buildSHIELDS() {
331
+ const builtins = loadShieldsFromDir(BUILTIN_DIR, "builtin");
332
+ const userShields = loadShieldsFromDir(USER_SHIELDS_DIR, "user");
333
+ return { ...builtins, ...userShields };
334
+ }
271
335
  function resolveShieldName(input) {
272
336
  const lower = input.toLowerCase();
273
337
  if (SHIELDS[lower]) return lower;
@@ -374,7 +438,24 @@ function resolveShieldRule(shieldName, identifier) {
374
438
  }
375
439
  return null;
376
440
  }
377
- var import_fs2, import_path2, import_os2, import_crypto2, SHIELDS, SHIELDS_STATE_FILE;
441
+ function installShield(name, shieldJson) {
442
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
443
+ throw new Error(
444
+ `Invalid shield name '${name}': only alphanumeric characters, hyphens, and underscores are allowed`
445
+ );
446
+ }
447
+ const shield = validateShieldDefinition(shieldJson, `<downloaded:${name}>`);
448
+ if (!shield) throw new Error(`Downloaded shield '${name}' failed validation`);
449
+ if (shield.name !== name) {
450
+ throw new Error(`Shield name mismatch: file declares '${shield.name}' but expected '${name}'`);
451
+ }
452
+ import_fs2.default.mkdirSync(USER_SHIELDS_DIR, { recursive: true });
453
+ const filePath = import_path2.default.join(USER_SHIELDS_DIR, `${name}.json`);
454
+ const tmp = `${filePath}.${import_crypto2.default.randomBytes(6).toString("hex")}.tmp`;
455
+ import_fs2.default.writeFileSync(tmp, JSON.stringify(shieldJson, null, 2), { mode: 384 });
456
+ import_fs2.default.renameSync(tmp, filePath);
457
+ }
458
+ var import_fs2, import_path2, import_os2, import_crypto2, BUILTIN_DIR, USER_SHIELDS_DIR, SHIELDS, SHIELDS_STATE_FILE;
378
459
  var init_shields = __esm({
379
460
  "src/shields.ts"() {
380
461
  "use strict";
@@ -382,251 +463,9 @@ var init_shields = __esm({
382
463
  import_path2 = __toESM(require("path"));
383
464
  import_os2 = __toESM(require("os"));
384
465
  import_crypto2 = __toESM(require("crypto"));
385
- SHIELDS = {
386
- postgres: {
387
- name: "postgres",
388
- description: "Protects PostgreSQL databases from destructive AI operations",
389
- aliases: ["pg", "postgresql"],
390
- smartRules: [
391
- {
392
- name: "shield:postgres:block-drop-table",
393
- tool: "*",
394
- conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
395
- verdict: "block",
396
- reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
397
- },
398
- {
399
- name: "shield:postgres:block-truncate",
400
- tool: "*",
401
- conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
402
- verdict: "block",
403
- reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
404
- },
405
- {
406
- name: "shield:postgres:block-drop-column",
407
- tool: "*",
408
- conditions: [
409
- { field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
410
- ],
411
- verdict: "block",
412
- reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
413
- },
414
- {
415
- name: "shield:postgres:review-grant-revoke",
416
- tool: "*",
417
- conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
418
- verdict: "review",
419
- reason: "Permission changes require human approval (Postgres shield)"
420
- }
421
- ],
422
- dangerousWords: ["dropdb", "pg_dropcluster"]
423
- },
424
- github: {
425
- name: "github",
426
- description: "Protects GitHub repositories from destructive AI operations",
427
- aliases: ["git"],
428
- smartRules: [
429
- {
430
- // Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
431
- // This rule adds coverage for `git push --delete` which the built-in does not match.
432
- name: "shield:github:review-delete-branch-remote",
433
- tool: "bash",
434
- conditions: [
435
- {
436
- field: "command",
437
- op: "matches",
438
- value: "git\\s+push\\s+.*--delete",
439
- flags: "i"
440
- }
441
- ],
442
- verdict: "review",
443
- reason: "Remote branch deletion requires human approval (GitHub shield)"
444
- },
445
- {
446
- name: "shield:github:block-delete-repo",
447
- tool: "*",
448
- conditions: [
449
- { field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
450
- ],
451
- verdict: "block",
452
- reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
453
- }
454
- ],
455
- dangerousWords: []
456
- },
457
- aws: {
458
- name: "aws",
459
- description: "Protects AWS infrastructure from destructive AI operations",
460
- aliases: ["amazon"],
461
- smartRules: [
462
- {
463
- name: "shield:aws:block-delete-s3-bucket",
464
- tool: "*",
465
- conditions: [
466
- {
467
- field: "command",
468
- op: "matches",
469
- value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
470
- flags: "i"
471
- }
472
- ],
473
- verdict: "block",
474
- reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
475
- },
476
- {
477
- name: "shield:aws:review-iam-changes",
478
- tool: "*",
479
- conditions: [
480
- {
481
- field: "command",
482
- op: "matches",
483
- value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
484
- flags: "i"
485
- }
486
- ],
487
- verdict: "review",
488
- reason: "IAM changes require human approval (AWS shield)"
489
- },
490
- {
491
- name: "shield:aws:block-ec2-terminate",
492
- tool: "*",
493
- conditions: [
494
- {
495
- field: "command",
496
- op: "matches",
497
- value: "aws\\s+ec2\\s+terminate-instances",
498
- flags: "i"
499
- }
500
- ],
501
- verdict: "block",
502
- reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
503
- },
504
- {
505
- name: "shield:aws:review-rds-delete",
506
- tool: "*",
507
- conditions: [
508
- { field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
509
- ],
510
- verdict: "review",
511
- reason: "RDS deletion requires human approval (AWS shield)"
512
- }
513
- ],
514
- dangerousWords: []
515
- },
516
- "bash-safe": {
517
- name: "bash-safe",
518
- description: "Blocks high-risk bash patterns: pipe-to-shell, rm -rf /, disk overwrites, eval",
519
- aliases: ["bash", "shell"],
520
- smartRules: [
521
- {
522
- name: "shield:bash-safe:block-pipe-to-shell",
523
- tool: "bash",
524
- conditions: [
525
- {
526
- field: "command",
527
- op: "matches",
528
- value: "(curl|wget)\\s+[^|]*\\|\\s*(bash|sh|zsh|fish|python3?|ruby|perl|node)",
529
- flags: "i"
530
- }
531
- ],
532
- verdict: "block",
533
- reason: "Pipe-to-shell is a common supply-chain attack vector \u2014 blocked by bash-safe shield"
534
- },
535
- {
536
- name: "shield:bash-safe:block-obfuscated-exec",
537
- tool: "bash",
538
- conditions: [
539
- {
540
- field: "command",
541
- op: "matches",
542
- value: "base64\\s+(-d|--decode).*\\|\\s*(bash|sh|zsh)",
543
- flags: "i"
544
- }
545
- ],
546
- verdict: "block",
547
- reason: "Obfuscated execution via base64 decode \u2014 blocked by bash-safe shield"
548
- },
549
- {
550
- name: "shield:bash-safe:block-rm-root",
551
- tool: "bash",
552
- conditions: [
553
- {
554
- field: "command",
555
- op: "matches",
556
- 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*$",
557
- flags: "i"
558
- }
559
- ],
560
- verdict: "block",
561
- reason: "rm -rf of root or home directory is catastrophic \u2014 blocked by bash-safe shield"
562
- },
563
- {
564
- name: "shield:bash-safe:block-disk-overwrite",
565
- tool: "bash",
566
- conditions: [
567
- {
568
- field: "command",
569
- op: "matches",
570
- value: "dd\\s+.*of=\\/dev\\/(sd|nvme|hd|vd|xvd)",
571
- flags: "i"
572
- }
573
- ],
574
- verdict: "block",
575
- reason: "Writing directly to a block device is irreversible \u2014 blocked by bash-safe shield"
576
- },
577
- {
578
- name: "shield:bash-safe:review-eval",
579
- tool: "bash",
580
- conditions: [
581
- {
582
- field: "command",
583
- op: "matches",
584
- value: '\\beval\\s+[\\$`("]',
585
- flags: "i"
586
- }
587
- ],
588
- verdict: "review",
589
- reason: "eval of dynamic content requires human approval (bash-safe shield)"
590
- }
591
- ],
592
- dangerousWords: []
593
- },
594
- filesystem: {
595
- name: "filesystem",
596
- description: "Protects the local filesystem from dangerous AI operations",
597
- aliases: ["fs"],
598
- smartRules: [
599
- {
600
- name: "shield:filesystem:review-chmod-777",
601
- tool: "bash",
602
- conditions: [
603
- { field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
604
- ],
605
- verdict: "review",
606
- reason: "chmod 777 requires human approval (filesystem shield)"
607
- },
608
- {
609
- name: "shield:filesystem:review-write-etc",
610
- tool: "bash",
611
- conditions: [
612
- {
613
- field: "command",
614
- // Narrow to write-indicative operations to avoid approval fatigue on reads.
615
- // Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
616
- op: "matches",
617
- value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
618
- }
619
- ],
620
- verdict: "review",
621
- reason: "Writing to /etc requires human approval (filesystem shield)"
622
- }
623
- ],
624
- // dd removed: too common as a legitimate tool (disk imaging, file ops).
625
- // mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
626
- // wipefs retained: rarely legitimate in an agent context and not in built-ins.
627
- dangerousWords: ["wipefs"]
628
- }
629
- };
466
+ BUILTIN_DIR = import_path2.default.join(__dirname, "shields", "builtin");
467
+ USER_SHIELDS_DIR = import_path2.default.join(import_os2.default.homedir(), ".node9", "shields");
468
+ SHIELDS = buildSHIELDS();
630
469
  SHIELDS_STATE_FILE = import_path2.default.join(import_os2.default.homedir(), ".node9", "shields.json");
631
470
  }
632
471
  });
@@ -2598,7 +2437,7 @@ function isDaemonRunning() {
2598
2437
  return false;
2599
2438
  }
2600
2439
  }
2601
- async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly) {
2440
+ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched) {
2602
2441
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2603
2442
  const ctrl = new AbortController();
2604
2443
  const timer = setTimeout(() => ctrl.abort(), 5e3);
@@ -2619,7 +2458,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
2619
2458
  ...cwd && { cwd },
2620
2459
  ...recoveryCommand && { recoveryCommand },
2621
2460
  ...skipBackgroundAuth && { skipBackgroundAuth: true },
2622
- ...viewOnly && { viewOnly: true }
2461
+ ...viewOnly && { viewOnly: true },
2462
+ ...localSmartRuleMatched && { localSmartRuleMatched: true }
2623
2463
  }),
2624
2464
  signal: ctrl.signal
2625
2465
  });
@@ -3032,9 +2872,7 @@ end run`;
3032
2872
  "--text",
3033
2873
  pangoMessage,
3034
2874
  "--ok-label",
3035
- locked ? "Waiting..." : "Allow \u21B5",
3036
- "--timeout",
3037
- "300"
2875
+ locked ? "Waiting..." : "Allow \u21B5"
3038
2876
  ];
3039
2877
  if (!locked) {
3040
2878
  argsList.push("--cancel-label", "Block \u238B");
@@ -3314,6 +3152,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3314
3152
  let policyMatchedWord;
3315
3153
  let riskMetadata;
3316
3154
  let statefulRecoveryCommand;
3155
+ let localSmartRuleMatched = false;
3317
3156
  let taintWarning = null;
3318
3157
  if (isNetworkTool(toolName, args)) {
3319
3158
  const filePaths = extractFilePaths(toolName, args);
@@ -3457,6 +3296,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3457
3296
  explainableLabel = policyResult.blockedByLabel || "Local Config";
3458
3297
  policyMatchedField = policyResult.matchedField;
3459
3298
  policyMatchedWord = policyResult.matchedWord;
3299
+ if (policyResult.ruleName) localSmartRuleMatched = true;
3460
3300
  riskMetadata = computeRiskMetadata(
3461
3301
  args,
3462
3302
  policyResult.tier ?? 6,
@@ -3499,22 +3339,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3499
3339
  }
3500
3340
  let cloudRequestId = null;
3501
3341
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
3502
- if (cloudEnforced) {
3342
+ if (cloudEnforced && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
3503
3343
  try {
3504
3344
  const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
3505
3345
  if (!initResult.pending) {
3506
3346
  if (initResult.shadowMode) {
3507
3347
  return { approved: true, checkedBy: "cloud" };
3508
3348
  }
3509
- return {
3510
- approved: !!initResult.approved,
3511
- reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
3512
- checkedBy: initResult.approved ? "cloud" : void 0,
3513
- blockedBy: initResult.approved ? void 0 : "team-policy",
3514
- blockedByLabel: "Organization Policy (SaaS)"
3515
- };
3349
+ if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
3350
+ return {
3351
+ approved: !!initResult.approved,
3352
+ reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
3353
+ checkedBy: initResult.approved ? "cloud" : void 0,
3354
+ blockedBy: initResult.approved ? void 0 : "team-policy",
3355
+ blockedByLabel: "Organization Policy (SaaS)"
3356
+ };
3357
+ }
3358
+ }
3359
+ if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
3360
+ cloudRequestId = initResult.requestId || null;
3516
3361
  }
3517
- cloudRequestId = initResult.requestId || null;
3518
3362
  if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
3519
3363
  } catch {
3520
3364
  }
@@ -3560,7 +3404,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3560
3404
  riskMetadata,
3561
3405
  options?.activityId,
3562
3406
  options?.cwd,
3563
- statefulRecoveryCommand
3407
+ statefulRecoveryCommand,
3408
+ void 0,
3409
+ void 0,
3410
+ localSmartRuleMatched || options?.localSmartRuleMatched
3564
3411
  );
3565
3412
  daemonEntryId = entry.id;
3566
3413
  daemonAllowCount = entry.allowCount;
@@ -3568,7 +3415,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3568
3415
  }
3569
3416
  }
3570
3417
  }
3571
- if (cloudEnforced && cloudRequestId) {
3418
+ if (cloudEnforced && cloudRequestId && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
3572
3419
  racePromises.push(
3573
3420
  (async () => {
3574
3421
  try {
@@ -5842,6 +5689,12 @@ function estimateToolCost(tool, args) {
5842
5689
  const newStr = a.new_string ?? "";
5843
5690
  return String(newStr).length / BYTES_PER_TOKEN / 1e6 * OUTPUT_PRICE_PER_1M;
5844
5691
  }
5692
+ if (t === "bash" || t === "shell" || t === "run_shell_command" || t === "terminal_execute") {
5693
+ const command = String(a.command ?? a.cmd ?? a.input ?? "");
5694
+ if (command.length > 0) {
5695
+ return command.length / BYTES_PER_TOKEN / 1e6 * OUTPUT_PRICE_PER_1M;
5696
+ }
5697
+ }
5845
5698
  return void 0;
5846
5699
  }
5847
5700
  function broadcast(event, data) {
@@ -6230,7 +6083,8 @@ data: ${JSON.stringify(item.data)}
6230
6083
  viewOnly = false,
6231
6084
  fromCLI = false,
6232
6085
  activityId,
6233
- cwd
6086
+ cwd,
6087
+ localSmartRuleMatched = false
6234
6088
  } = JSON.parse(body);
6235
6089
  const id = fromCLI && typeof activityId === "string" && activityId || (0, import_crypto6.randomUUID)();
6236
6090
  const entry = {
@@ -6310,7 +6164,7 @@ data: ${JSON.stringify(item.data)}
6310
6164
  agent: typeof agent === "string" ? agent : void 0,
6311
6165
  mcpServer: typeof mcpServer === "string" ? mcpServer : void 0
6312
6166
  },
6313
- { calledFromDaemon: true }
6167
+ { calledFromDaemon: true, localSmartRuleMatched: !!localSmartRuleMatched }
6314
6168
  ).then((result) => {
6315
6169
  const e = pending.get(id);
6316
6170
  if (!e) return;
@@ -6973,7 +6827,7 @@ async function ensureDaemon() {
6973
6827
  } catch {
6974
6828
  }
6975
6829
  console.log(import_chalk17.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
6976
- const child = (0, import_child_process13.spawn)(process.execPath, [process.argv[1], "daemon"], {
6830
+ const child = (0, import_child_process14.spawn)(process.execPath, [process.argv[1], "daemon"], {
6977
6831
  detached: true,
6978
6832
  stdio: "ignore",
6979
6833
  env: { ...process.env, NODE9_AUTO_STARTED: "1" }
@@ -7147,6 +7001,7 @@ async function startTail(options = {}) {
7147
7001
  return;
7148
7002
  }
7149
7003
  const connectionTime = Date.now();
7004
+ let initialReplayDone = false;
7150
7005
  const activityPending = /* @__PURE__ */ new Map();
7151
7006
  const orphanedResults = /* @__PURE__ */ new Map();
7152
7007
  let csrfToken = "";
@@ -7342,10 +7197,10 @@ async function startTail(options = {}) {
7342
7197
  try {
7343
7198
  const browserEnabled = getConfig().settings.approvers?.browser !== false;
7344
7199
  if (browserEnabled) {
7345
- if (process.platform === "darwin") (0, import_child_process13.execSync)(`open "${dashboardUrl}"`, { stdio: "ignore" });
7200
+ if (process.platform === "darwin") (0, import_child_process14.execSync)(`open "${dashboardUrl}"`, { stdio: "ignore" });
7346
7201
  else if (process.platform === "win32")
7347
- (0, import_child_process13.execSync)(`cmd /c start "" "${dashboardUrl}"`, { stdio: "ignore" });
7348
- else (0, import_child_process13.execSync)(`xdg-open "${dashboardUrl}"`, { stdio: "ignore" });
7202
+ (0, import_child_process14.execSync)(`cmd /c start "" "${dashboardUrl}"`, { stdio: "ignore" });
7203
+ else (0, import_child_process14.execSync)(`xdg-open "${dashboardUrl}"`, { stdio: "ignore" });
7349
7204
  const intToken = getInternalToken();
7350
7205
  fetch(`http://127.0.0.1:${port}/browser-opened`, {
7351
7206
  method: "POST",
@@ -7478,11 +7333,17 @@ async function startTail(options = {}) {
7478
7333
  return;
7479
7334
  }
7480
7335
  if (event === "activity") {
7336
+ const isReplayEvent = data.status && data.status !== "pending";
7337
+ if (isReplayEvent && !initialReplayDone) {
7338
+ renderResult(data, data);
7339
+ return;
7340
+ }
7481
7341
  if (!options.history && data.ts > 0 && data.ts < connectionTime) return;
7482
- if (data.status && data.status !== "pending") {
7342
+ if (isReplayEvent) {
7483
7343
  renderResult(data, data);
7484
7344
  return;
7485
7345
  }
7346
+ if (!initialReplayDone) initialReplayDone = true;
7486
7347
  const orphaned = orphanedResults.get(data.id);
7487
7348
  if (orphaned) {
7488
7349
  orphanedResults.delete(data.id);
@@ -7521,7 +7382,7 @@ async function startTail(options = {}) {
7521
7382
  process.exit(1);
7522
7383
  });
7523
7384
  }
7524
- var import_http2, import_chalk17, import_fs25, import_os21, import_path28, import_readline5, import_child_process13, PID_FILE, ICONS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, DIVIDER;
7385
+ var import_http2, import_chalk17, import_fs25, import_os21, import_path28, import_readline5, import_child_process14, PID_FILE, ICONS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, DIVIDER;
7525
7386
  var init_tail = __esm({
7526
7387
  "src/tui/tail.ts"() {
7527
7388
  "use strict";
@@ -7531,7 +7392,7 @@ var init_tail = __esm({
7531
7392
  import_os21 = __toESM(require("os"));
7532
7393
  import_path28 = __toESM(require("path"));
7533
7394
  import_readline5 = __toESM(require("readline"));
7534
- import_child_process13 = require("child_process");
7395
+ import_child_process14 = require("child_process");
7535
7396
  init_daemon2();
7536
7397
  init_daemon();
7537
7398
  init_core();
@@ -7866,6 +7727,24 @@ function renderContextLine(stdin) {
7866
7727
  async function main() {
7867
7728
  try {
7868
7729
  const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
7730
+ if (import_fs26.default.existsSync(import_path29.default.join(import_os22.default.homedir(), ".node9", "hud-debug"))) {
7731
+ try {
7732
+ const logPath = import_path29.default.join(import_os22.default.homedir(), ".node9", "hud-debug.log");
7733
+ const MAX_LOG_SIZE = 10 * 1024 * 1024;
7734
+ let size = 0;
7735
+ try {
7736
+ size = import_fs26.default.statSync(logPath).size;
7737
+ } catch {
7738
+ }
7739
+ if (size < MAX_LOG_SIZE) {
7740
+ import_fs26.default.appendFileSync(
7741
+ logPath,
7742
+ JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
7743
+ );
7744
+ }
7745
+ } catch {
7746
+ }
7747
+ }
7869
7748
  if (!daemonStatus2) {
7870
7749
  renderOffline();
7871
7750
  return;
@@ -7982,7 +7861,7 @@ function isNode9Hook(cmd) {
7982
7861
  function teardownClaude() {
7983
7862
  const homeDir2 = import_os10.default.homedir();
7984
7863
  const hooksPath = import_path14.default.join(homeDir2, ".claude", "settings.json");
7985
- const mcpPath = import_path14.default.join(homeDir2, ".claude.json");
7864
+ const mcpPath = import_path14.default.join(homeDir2, ".claude", ".mcp.json");
7986
7865
  let changed = false;
7987
7866
  const settings = readJson(hooksPath);
7988
7867
  if (settings?.hooks) {
@@ -8008,11 +7887,12 @@ function teardownClaude() {
8008
7887
  let mcpChanged = false;
8009
7888
  if (removeNode9McpServer(claudeConfig.mcpServers)) {
8010
7889
  mcpChanged = true;
8011
- console.log(import_chalk.default.green(" \u2705 Removed node9 MCP server entry from ~/.claude.json"));
7890
+ console.log(import_chalk.default.green(" \u2705 Removed node9 MCP server entry from ~/.claude/.mcp.json"));
8012
7891
  }
8013
7892
  for (const [name, server] of Object.entries(claudeConfig.mcpServers)) {
8014
- if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
8015
- const [originalCmd, ...originalArgs] = server.args;
7893
+ const args = server.args;
7894
+ if (server.command === "node9" && Array.isArray(args) && args[0] === "mcp" && args[1] === "--upstream" && typeof args[2] === "string") {
7895
+ const [originalCmd, ...originalArgs] = args[2].split(" ");
8016
7896
  claudeConfig.mcpServers[name] = {
8017
7897
  ...server,
8018
7898
  command: originalCmd,
@@ -8020,16 +7900,11 @@ function teardownClaude() {
8020
7900
  };
8021
7901
  mcpChanged = true;
8022
7902
  } else if (server.command === "node9") {
8023
- console.warn(
8024
- import_chalk.default.yellow(
8025
- ` \u26A0\uFE0F Cannot unwrap MCP server "${name}" in ~/.claude.json \u2014 args is empty. Remove it manually.`
8026
- )
8027
- );
8028
7903
  }
8029
7904
  }
8030
7905
  if (mcpChanged) {
8031
7906
  writeJson(mcpPath, claudeConfig);
8032
- console.log(import_chalk.default.green(" \u2705 Unwrapped MCP servers in ~/.claude.json"));
7907
+ console.log(import_chalk.default.green(" \u2705 Unwrapped MCP servers in ~/.claude/.mcp.json"));
8033
7908
  }
8034
7909
  }
8035
7910
  }
@@ -8058,8 +7933,9 @@ function teardownGemini() {
8058
7933
  console.log(import_chalk.default.green(" \u2705 Removed node9 MCP server entry from ~/.gemini/settings.json"));
8059
7934
  }
8060
7935
  for (const [name, server] of Object.entries(settings.mcpServers)) {
8061
- if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
8062
- const [originalCmd, ...originalArgs] = server.args;
7936
+ const args = server.args;
7937
+ if (server.command === "node9" && Array.isArray(args) && args[0] === "mcp" && args[1] === "--upstream" && typeof args[2] === "string") {
7938
+ const [originalCmd, ...originalArgs] = args[2].split(" ");
8063
7939
  settings.mcpServers[name] = {
8064
7940
  ...server,
8065
7941
  command: originalCmd,
@@ -8090,8 +7966,9 @@ function teardownCursor() {
8090
7966
  console.log(import_chalk.default.green(" \u2705 Removed node9 MCP server entry from ~/.cursor/mcp.json"));
8091
7967
  }
8092
7968
  for (const [name, server] of Object.entries(mcpConfig.mcpServers)) {
8093
- if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
8094
- const [originalCmd, ...originalArgs] = server.args;
7969
+ const args = server.args;
7970
+ if (server.command === "node9" && Array.isArray(args) && args[0] === "mcp" && args[1] === "--upstream" && typeof args[2] === "string") {
7971
+ const [originalCmd, ...originalArgs] = args[2].split(" ");
8095
7972
  mcpConfig.mcpServers[name] = {
8096
7973
  ...server,
8097
7974
  command: originalCmd,
@@ -8109,7 +7986,7 @@ function teardownCursor() {
8109
7986
  }
8110
7987
  async function setupClaude() {
8111
7988
  const homeDir2 = import_os10.default.homedir();
8112
- const mcpPath = import_path14.default.join(homeDir2, ".claude.json");
7989
+ const mcpPath = import_path14.default.join(homeDir2, ".claude", ".mcp.json");
8113
7990
  const hooksPath = import_path14.default.join(homeDir2, ".claude", "settings.json");
8114
7991
  const claudeConfig = readJson(mcpPath) ?? {};
8115
7992
  const settings = readJson(hooksPath) ?? {};
@@ -8124,7 +8001,7 @@ async function setupClaude() {
8124
8001
  if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
8125
8002
  settings.hooks.PreToolUse.push({
8126
8003
  matcher: ".*",
8127
- hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
8004
+ hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 600 }]
8128
8005
  });
8129
8006
  console.log(import_chalk.default.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
8130
8007
  hooksChanged = true;
@@ -8150,6 +8027,15 @@ async function setupClaude() {
8150
8027
  console.log(import_chalk.default.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
8151
8028
  anythingChanged = true;
8152
8029
  }
8030
+ const hudCommand = fullPathCommand("hud");
8031
+ const statusLineObj = { type: "command", command: hudCommand };
8032
+ const existingStatusLine = settings.statusLine;
8033
+ const existingStatusCommand = typeof existingStatusLine === "object" ? existingStatusLine?.command : existingStatusLine;
8034
+ if (existingStatusCommand !== hudCommand) {
8035
+ settings.statusLine = statusLineObj;
8036
+ hooksChanged = true;
8037
+ anythingChanged = true;
8038
+ }
8153
8039
  if (hooksChanged) {
8154
8040
  writeJson(hooksPath, settings);
8155
8041
  console.log("");
@@ -8157,20 +8043,24 @@ async function setupClaude() {
8157
8043
  const serversToWrap = [];
8158
8044
  for (const [name, server] of Object.entries(servers)) {
8159
8045
  if (!server.command || server.command === "node9") continue;
8160
- const parts = [server.command, ...server.args ?? []];
8161
- serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
8046
+ const upstream = [server.command, ...server.args ?? []].join(" ");
8047
+ serversToWrap.push({ name, upstream });
8162
8048
  }
8163
8049
  if (serversToWrap.length > 0) {
8164
8050
  console.log(import_chalk.default.bold("The following existing entries will be modified:\n"));
8165
8051
  console.log(import_chalk.default.white(` ${mcpPath}`));
8166
- for (const { name, originalCmd } of serversToWrap) {
8167
- console.log(import_chalk.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
8052
+ for (const { name, upstream } of serversToWrap) {
8053
+ console.log(import_chalk.default.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
8168
8054
  }
8169
8055
  console.log("");
8170
8056
  const proceed = await (0, import_prompts.confirm)({ message: "Wrap these MCP servers?", default: true });
8171
8057
  if (proceed) {
8172
- for (const { name, parts } of serversToWrap) {
8173
- servers[name] = { ...servers[name], command: "node9", args: parts };
8058
+ for (const { name, upstream } of serversToWrap) {
8059
+ servers[name] = {
8060
+ ...servers[name],
8061
+ command: "node9",
8062
+ args: ["mcp", "--upstream", upstream]
8063
+ };
8174
8064
  }
8175
8065
  claudeConfig.mcpServers = servers;
8176
8066
  writeJson(mcpPath, claudeConfig);
@@ -8250,20 +8140,24 @@ async function setupGemini() {
8250
8140
  const serversToWrap = [];
8251
8141
  for (const [name, server] of Object.entries(servers)) {
8252
8142
  if (!server.command || server.command === "node9") continue;
8253
- const parts = [server.command, ...server.args ?? []];
8254
- serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
8143
+ const upstream = [server.command, ...server.args ?? []].join(" ");
8144
+ serversToWrap.push({ name, upstream });
8255
8145
  }
8256
8146
  if (serversToWrap.length > 0) {
8257
8147
  console.log(import_chalk.default.bold("The following existing entries will be modified:\n"));
8258
8148
  console.log(import_chalk.default.white(` ${settingsPath} (mcpServers)`));
8259
- for (const { name, originalCmd } of serversToWrap) {
8260
- console.log(import_chalk.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
8149
+ for (const { name, upstream } of serversToWrap) {
8150
+ console.log(import_chalk.default.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
8261
8151
  }
8262
8152
  console.log("");
8263
8153
  const proceed = await (0, import_prompts.confirm)({ message: "Wrap these MCP servers?", default: true });
8264
8154
  if (proceed) {
8265
- for (const { name, parts } of serversToWrap) {
8266
- servers[name] = { ...servers[name], command: "node9", args: parts };
8155
+ for (const { name, upstream } of serversToWrap) {
8156
+ servers[name] = {
8157
+ ...servers[name],
8158
+ command: "node9",
8159
+ args: ["mcp", "--upstream", upstream]
8160
+ };
8267
8161
  }
8268
8162
  settings.mcpServers = servers;
8269
8163
  writeJson(settingsPath, settings);
@@ -8322,20 +8216,24 @@ async function setupCursor() {
8322
8216
  const serversToWrap = [];
8323
8217
  for (const [name, server] of Object.entries(servers)) {
8324
8218
  if (!server.command || server.command === "node9") continue;
8325
- const parts = [server.command, ...server.args ?? []];
8326
- serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
8219
+ const upstream = [server.command, ...server.args ?? []].join(" ");
8220
+ serversToWrap.push({ name, upstream });
8327
8221
  }
8328
8222
  if (serversToWrap.length > 0) {
8329
8223
  console.log(import_chalk.default.bold("The following existing entries will be modified:\n"));
8330
8224
  console.log(import_chalk.default.white(` ${mcpPath}`));
8331
- for (const { name, originalCmd } of serversToWrap) {
8332
- console.log(import_chalk.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
8225
+ for (const { name, upstream } of serversToWrap) {
8226
+ console.log(import_chalk.default.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
8333
8227
  }
8334
8228
  console.log("");
8335
8229
  const proceed = await (0, import_prompts.confirm)({ message: "Wrap these MCP servers?", default: true });
8336
8230
  if (proceed) {
8337
- for (const { name, parts } of serversToWrap) {
8338
- servers[name] = { ...servers[name], command: "node9", args: parts };
8231
+ for (const { name, upstream } of serversToWrap) {
8232
+ servers[name] = {
8233
+ ...servers[name],
8234
+ command: "node9",
8235
+ args: ["mcp", "--upstream", upstream]
8236
+ };
8339
8237
  }
8340
8238
  mcpConfig.mcpServers = servers;
8341
8239
  writeJson(mcpPath, mcpConfig);
@@ -8398,20 +8296,24 @@ async function setupCodex() {
8398
8296
  const serversToWrap = [];
8399
8297
  for (const [name, server] of Object.entries(servers)) {
8400
8298
  if (!server.command || server.command === "node9") continue;
8401
- const parts = [server.command, ...server.args ?? []];
8402
- serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
8299
+ const upstream = [server.command, ...server.args ?? []].join(" ");
8300
+ serversToWrap.push({ name, upstream });
8403
8301
  }
8404
8302
  if (serversToWrap.length > 0) {
8405
8303
  console.log(import_chalk.default.bold("The following existing entries will be modified:\n"));
8406
8304
  console.log(import_chalk.default.white(` ${configPath}`));
8407
- for (const { name, originalCmd } of serversToWrap) {
8408
- console.log(import_chalk.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
8305
+ for (const { name, upstream } of serversToWrap) {
8306
+ console.log(import_chalk.default.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
8409
8307
  }
8410
8308
  console.log("");
8411
8309
  const proceed = await (0, import_prompts.confirm)({ message: "Wrap these MCP servers?", default: true });
8412
8310
  if (proceed) {
8413
- for (const { name, parts } of serversToWrap) {
8414
- servers[name] = { ...servers[name], command: "node9", args: parts };
8311
+ for (const { name, upstream } of serversToWrap) {
8312
+ servers[name] = {
8313
+ ...servers[name],
8314
+ command: "node9",
8315
+ args: ["mcp", "--upstream", upstream]
8316
+ };
8415
8317
  }
8416
8318
  config.mcp_servers = servers;
8417
8319
  writeToml(configPath, config);
@@ -8600,18 +8502,20 @@ async function runProxy(targetCommand) {
8600
8502
  const cmd = commandParts[0];
8601
8503
  const args = commandParts.slice(1);
8602
8504
  let executable = cmd;
8505
+ let useShell = false;
8603
8506
  try {
8604
8507
  const { stdout } = await (0, import_execa.execa)("which", [cmd]);
8605
8508
  if (stdout) executable = stdout.trim();
8606
8509
  } catch {
8510
+ useShell = true;
8607
8511
  }
8608
8512
  console.error(import_chalk4.default.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
8609
- const child = (0, import_child_process6.spawn)(executable, args, {
8513
+ const spawnEnv = { ...process.env, FORCE_COLOR: "1" };
8514
+ const child = useShell ? (0, import_child_process6.spawn)("/bin/bash", ["-c", targetCommand], {
8610
8515
  stdio: ["pipe", "pipe", "inherit"],
8611
- // We control STDIN and STDOUT
8612
8516
  shell: false,
8613
- env: { ...process.env, FORCE_COLOR: "1" }
8614
- });
8517
+ env: spawnEnv
8518
+ }) : (0, import_child_process6.spawn)(executable, args, { stdio: ["pipe", "pipe", "inherit"], shell: false, env: spawnEnv });
8615
8519
  const agentIn = import_readline.default.createInterface({ input: process.stdin, terminal: false });
8616
8520
  agentIn.on("line", async (line) => {
8617
8521
  let message;
@@ -8720,6 +8624,7 @@ async function autoStartDaemonAndWait() {
8720
8624
  // src/cli/commands/check.ts
8721
8625
  var import_chalk5 = __toESM(require("chalk"));
8722
8626
  var import_fs18 = __toESM(require("fs"));
8627
+ var import_child_process9 = require("child_process");
8723
8628
  var import_path20 = __toESM(require("path"));
8724
8629
  var import_os14 = __toESM(require("os"));
8725
8630
  init_orchestrator();
@@ -9103,6 +9008,37 @@ RAW: ${raw}
9103
9008
  process.exit(0);
9104
9009
  }
9105
9010
  const config = getConfig(payload.cwd || void 0);
9011
+ if (config.settings.autoStartDaemon && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON) {
9012
+ try {
9013
+ const scriptPath = process.argv[1];
9014
+ if (typeof scriptPath !== "string" || !import_path20.default.isAbsolute(scriptPath))
9015
+ throw new Error("node9: argv[1] is not an absolute path");
9016
+ const resolvedScript = import_fs18.default.realpathSync(scriptPath);
9017
+ const expectedCli = import_fs18.default.realpathSync(import_path20.default.resolve(__dirname, "../../cli.js"));
9018
+ if (resolvedScript !== expectedCli)
9019
+ throw new Error(
9020
+ "node9: daemon spawn aborted \u2014 argv[1] does not resolve to the node9 CLI"
9021
+ );
9022
+ const safeEnv = { ...process.env };
9023
+ for (const key of [
9024
+ "NODE_OPTIONS",
9025
+ "LD_PRELOAD",
9026
+ "LD_LIBRARY_PATH",
9027
+ "DYLD_INSERT_LIBRARIES",
9028
+ "NODE_PATH",
9029
+ "ELECTRON_RUN_AS_NODE"
9030
+ ]) {
9031
+ delete safeEnv[key];
9032
+ }
9033
+ const d = (0, import_child_process9.spawn)(process.execPath, [scriptPath, "daemon"], {
9034
+ detached: true,
9035
+ stdio: "ignore",
9036
+ env: { ...safeEnv, NODE9_AUTO_STARTED: "1", NODE9_BROWSER_OPENED: "1" }
9037
+ });
9038
+ d.unref();
9039
+ } catch {
9040
+ }
9041
+ }
9106
9042
  if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
9107
9043
  const logPath = import_path20.default.join(import_os14.default.homedir(), ".node9", "hook-debug.log");
9108
9044
  if (!import_fs18.default.existsSync(import_path20.default.dirname(logPath)))
@@ -9161,7 +9097,7 @@ RAW: ${raw}
9161
9097
  }
9162
9098
  }) + "\n"
9163
9099
  );
9164
- process.exit(0);
9100
+ process.exit(2);
9165
9101
  };
9166
9102
  if (!toolName) {
9167
9103
  sendBlock("Node9: unrecognised hook payload \u2014 tool name missing.");
@@ -9396,6 +9332,27 @@ var import_chalk6 = __toESM(require("chalk"));
9396
9332
  init_shields();
9397
9333
  init_audit();
9398
9334
  init_config();
9335
+
9336
+ // src/utils/https-fetch.ts
9337
+ var import_https = __toESM(require("https"));
9338
+ function httpsFetch(url) {
9339
+ return new Promise((resolve, reject) => {
9340
+ import_https.default.get(url, (res) => {
9341
+ if (res.statusCode !== 200) {
9342
+ reject(new Error(`HTTP ${String(res.statusCode)} for ${url}`));
9343
+ res.resume();
9344
+ return;
9345
+ }
9346
+ const chunks = [];
9347
+ res.on("data", (chunk) => chunks.push(chunk));
9348
+ res.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
9349
+ res.on("error", reject);
9350
+ }).on("error", reject);
9351
+ });
9352
+ }
9353
+
9354
+ // src/cli/commands/shield.ts
9355
+ var COMMUNITY_INDEX_URL = "https://raw.githubusercontent.com/node9ai/node9-proxy/main/shields/community/index.json";
9399
9356
  function registerShieldCommand(program2) {
9400
9357
  const shieldCmd = program2.command("shield").description("Manage pre-packaged security shield templates");
9401
9358
  shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
@@ -9455,7 +9412,32 @@ function registerShieldCommand(program2) {
9455
9412
  \u{1F6E1}\uFE0F Shield "${name}" disabled.
9456
9413
  `));
9457
9414
  });
9458
- shieldCmd.command("list").description("Show all available shields").action(() => {
9415
+ shieldCmd.command("list").description("Show available shields (add --community to browse the marketplace)").option("--community", "List shields available from the community marketplace").action((opts) => {
9416
+ if (opts.community) {
9417
+ console.log(import_chalk6.default.bold("\n\u{1F6E1}\uFE0F Community Shield Marketplace\n"));
9418
+ console.log(import_chalk6.default.gray(" Fetching index\u2026\n"));
9419
+ httpsFetch(COMMUNITY_INDEX_URL).then((body) => {
9420
+ const entries = JSON.parse(body);
9421
+ const installed = new Set(listShields().map((s) => s.name));
9422
+ for (const e of entries) {
9423
+ const tag = installed.has(e.name) ? import_chalk6.default.green("installed") : import_chalk6.default.gray("available");
9424
+ console.log(
9425
+ ` ${tag} ${import_chalk6.default.cyan(e.name.padEnd(12))} ${e.description} ${import_chalk6.default.gray(`by ${e.author}`)}`
9426
+ );
9427
+ }
9428
+ console.log("");
9429
+ console.log(
9430
+ import_chalk6.default.gray(` Install a shield: ${import_chalk6.default.cyan("node9 shield install <name>")}
9431
+ `)
9432
+ );
9433
+ }).catch((err2) => {
9434
+ console.error(import_chalk6.default.red(`
9435
+ \u274C Could not fetch community index: ${String(err2)}
9436
+ `));
9437
+ process.exit(1);
9438
+ });
9439
+ return;
9440
+ }
9459
9441
  const active = new Set(readActiveShields());
9460
9442
  console.log(import_chalk6.default.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
9461
9443
  for (const shield of listShields()) {
@@ -9465,6 +9447,10 @@ function registerShieldCommand(program2) {
9465
9447
  console.log(import_chalk6.default.gray(` aliases: ${shield.aliases.join(", ")}`));
9466
9448
  }
9467
9449
  console.log("");
9450
+ console.log(
9451
+ import_chalk6.default.gray(` Browse community shields: ${import_chalk6.default.cyan("node9 shield list --community")}
9452
+ `)
9453
+ );
9468
9454
  });
9469
9455
  shieldCmd.command("status").description("Show active shields and their individual rules with verdicts").action(() => {
9470
9456
  const active = readActiveShields();
@@ -9602,6 +9588,52 @@ function registerShieldCommand(program2) {
9602
9588
  `)
9603
9589
  );
9604
9590
  });
9591
+ shieldCmd.command("install <name>").description("Install a shield from the community marketplace into ~/.node9/shields/").action((name) => {
9592
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
9593
+ console.error(
9594
+ import_chalk6.default.red(
9595
+ `
9596
+ \u274C Invalid shield name: only alphanumeric characters, hyphens, and underscores are allowed
9597
+ `
9598
+ )
9599
+ );
9600
+ process.exit(1);
9601
+ }
9602
+ console.log(import_chalk6.default.bold(`
9603
+ \u{1F6E1}\uFE0F Installing shield "${name}"\u2026
9604
+ `));
9605
+ httpsFetch(COMMUNITY_INDEX_URL).then((indexBody) => {
9606
+ const entries = JSON.parse(indexBody);
9607
+ const entry = entries.find((e) => e.name === name);
9608
+ if (!entry) {
9609
+ const names = entries.map((e) => import_chalk6.default.cyan(e.name)).join(", ");
9610
+ console.error(
9611
+ import_chalk6.default.red(`\u274C Shield "${name}" not found in the community marketplace.
9612
+ `)
9613
+ );
9614
+ console.error(` Available: ${names}
9615
+ `);
9616
+ process.exit(1);
9617
+ }
9618
+ return httpsFetch(entry.url);
9619
+ }).then((shieldBody) => {
9620
+ const shieldJson = JSON.parse(shieldBody);
9621
+ installShield(name, shieldJson);
9622
+ console.log(
9623
+ import_chalk6.default.green(`\u2705 Shield "${name}" installed to ~/.node9/shields/${name}.json`)
9624
+ );
9625
+ console.log(
9626
+ import_chalk6.default.gray(` Activate it with: ${import_chalk6.default.cyan(`node9 shield enable ${name}`)}
9627
+ `)
9628
+ );
9629
+ appendConfigAudit({ event: "shield-install", shield: name });
9630
+ }).catch((err2) => {
9631
+ console.error(import_chalk6.default.red(`
9632
+ \u274C Install failed: ${String(err2)}
9633
+ `));
9634
+ process.exit(1);
9635
+ });
9636
+ });
9605
9637
  }
9606
9638
  function registerConfigShowCommand(program2) {
9607
9639
  program2.command("config show").description(
@@ -9675,7 +9707,7 @@ var import_chalk7 = __toESM(require("chalk"));
9675
9707
  var import_fs20 = __toESM(require("fs"));
9676
9708
  var import_path22 = __toESM(require("path"));
9677
9709
  var import_os16 = __toESM(require("os"));
9678
- var import_child_process9 = require("child_process");
9710
+ var import_child_process10 = require("child_process");
9679
9711
  init_daemon();
9680
9712
  function registerDoctorCommand(program2, version2) {
9681
9713
  program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
@@ -9701,7 +9733,7 @@ function registerDoctorCommand(program2, version2) {
9701
9733
  `));
9702
9734
  section("Binary");
9703
9735
  try {
9704
- const which = (0, import_child_process9.execSync)("which node9", { encoding: "utf-8", timeout: 3e3 }).trim();
9736
+ const which = (0, import_child_process10.execSync)("which node9", { encoding: "utf-8", timeout: 3e3 }).trim();
9705
9737
  pass(`node9 found at ${which}`);
9706
9738
  } catch {
9707
9739
  warn(
@@ -9719,7 +9751,7 @@ function registerDoctorCommand(program2, version2) {
9719
9751
  );
9720
9752
  }
9721
9753
  try {
9722
- const gitVersion = (0, import_child_process9.execSync)("git --version", { encoding: "utf-8", timeout: 3e3 }).trim();
9754
+ const gitVersion = (0, import_child_process10.execSync)("git --version", { encoding: "utf-8", timeout: 3e3 }).trim();
9723
9755
  pass(gitVersion);
9724
9756
  } catch {
9725
9757
  warn(
@@ -9916,7 +9948,7 @@ function registerAuditCommand(program2) {
9916
9948
 
9917
9949
  // src/cli/commands/daemon-cmd.ts
9918
9950
  var import_chalk9 = __toESM(require("chalk"));
9919
- var import_child_process10 = require("child_process");
9951
+ var import_child_process11 = require("child_process");
9920
9952
  init_daemon2();
9921
9953
  init_daemon();
9922
9954
  function registerDaemonCommand(program2) {
@@ -9949,7 +9981,7 @@ function registerDaemonCommand(program2) {
9949
9981
  console.log(import_chalk9.default.green(`\u{1F310} Opened browser: http://${DAEMON_HOST}:${DAEMON_PORT}/`));
9950
9982
  process.exit(0);
9951
9983
  }
9952
- const child = (0, import_child_process10.spawn)(process.execPath, [process.argv[1], "daemon"], {
9984
+ const child = (0, import_child_process11.spawn)(process.execPath, [process.argv[1], "daemon"], {
9953
9985
  detached: true,
9954
9986
  stdio: "ignore"
9955
9987
  });
@@ -9964,7 +9996,7 @@ function registerDaemonCommand(program2) {
9964
9996
  process.exit(0);
9965
9997
  }
9966
9998
  if (options.background) {
9967
- const child = (0, import_child_process10.spawn)(process.execPath, [process.argv[1], "daemon"], {
9999
+ const child = (0, import_child_process11.spawn)(process.execPath, [process.argv[1], "daemon"], {
9968
10000
  detached: true,
9969
10001
  stdio: "ignore"
9970
10002
  });
@@ -10135,7 +10167,7 @@ var import_chalk11 = __toESM(require("chalk"));
10135
10167
  var import_fs23 = __toESM(require("fs"));
10136
10168
  var import_path25 = __toESM(require("path"));
10137
10169
  var import_os19 = __toESM(require("os"));
10138
- var import_https = __toESM(require("https"));
10170
+ var import_https2 = __toESM(require("https"));
10139
10171
  init_core();
10140
10172
  init_shields();
10141
10173
  var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
@@ -10147,7 +10179,7 @@ function fireTelemetryPing(agents) {
10147
10179
  os: process.platform,
10148
10180
  node9_version: process.env.npm_package_version ?? "unknown"
10149
10181
  });
10150
- const req = import_https.default.request(
10182
+ const req = import_https2.default.request(
10151
10183
  {
10152
10184
  hostname: "api.node9.ai",
10153
10185
  path: "/api/v1/telemetry",
@@ -10552,7 +10584,7 @@ function registerUndoCommand(program2) {
10552
10584
 
10553
10585
  // src/cli/commands/watch.ts
10554
10586
  var import_chalk14 = __toESM(require("chalk"));
10555
- var import_child_process11 = require("child_process");
10587
+ var import_child_process12 = require("child_process");
10556
10588
  init_daemon();
10557
10589
  function registerWatchCommand(program2) {
10558
10590
  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) => {
@@ -10569,7 +10601,7 @@ function registerWatchCommand(program2) {
10569
10601
  }
10570
10602
  } catch {
10571
10603
  console.error(import_chalk14.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
10572
- const child = (0, import_child_process11.spawn)(process.execPath, [process.argv[1], "daemon"], {
10604
+ const child = (0, import_child_process12.spawn)(process.execPath, [process.argv[1], "daemon"], {
10573
10605
  detached: true,
10574
10606
  stdio: "ignore",
10575
10607
  env: { ...process.env, NODE9_AUTO_STARTED: "1", NODE9_WATCH_MODE: "1" }
@@ -10599,7 +10631,7 @@ function registerWatchCommand(program2) {
10599
10631
  "\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
10600
10632
  )
10601
10633
  );
10602
- const result = (0, import_child_process11.spawnSync)(cmd, args, {
10634
+ const result = (0, import_child_process12.spawnSync)(cmd, args, {
10603
10635
  stdio: "inherit",
10604
10636
  env: { ...process.env, NODE9_WATCH_MODE: "1" }
10605
10637
  });
@@ -10614,7 +10646,7 @@ function registerWatchCommand(program2) {
10614
10646
  // src/mcp-gateway/index.ts
10615
10647
  var import_readline3 = __toESM(require("readline"));
10616
10648
  var import_chalk15 = __toESM(require("chalk"));
10617
- var import_child_process12 = require("child_process");
10649
+ var import_child_process13 = require("child_process");
10618
10650
  var import_execa3 = require("execa");
10619
10651
  init_orchestrator();
10620
10652
  init_provenance();
@@ -10702,7 +10734,7 @@ async function runMcpGateway(upstreamCommand) {
10702
10734
  const safeEnv = Object.fromEntries(
10703
10735
  Object.entries(process.env).filter(([k]) => !UPSTREAM_INJECTOR_VARS.has(k))
10704
10736
  );
10705
- const child = (0, import_child_process12.spawn)(executable, cmdArgs, {
10737
+ const child = (0, import_child_process13.spawn)(executable, cmdArgs, {
10706
10738
  stdio: ["pipe", "pipe", "inherit"],
10707
10739
  // control stdin/stdout; inherit stderr
10708
10740
  shell: false,
@@ -10785,8 +10817,11 @@ async function runMcpGateway(upstreamCommand) {
10785
10817
  return;
10786
10818
  } finally {
10787
10819
  authPending = false;
10788
- agentIn.resume();
10789
- if (deferredStdinEnd) child.stdin.end();
10820
+ if (deferredStdinEnd) {
10821
+ child.stdin.end();
10822
+ } else {
10823
+ agentIn.resume();
10824
+ }
10790
10825
  if (deferredExitCode !== null) process.exit(deferredExitCode);
10791
10826
  }
10792
10827
  return;
@@ -11521,7 +11556,43 @@ registerMcpGatewayCommand(program);
11521
11556
  registerMcpServerCommand(program);
11522
11557
  registerCheckCommand(program);
11523
11558
  registerLogCommand(program);
11524
- program.command("hud").description("Render node9 security statusline (spawned by Claude Code statusLine)").action(async () => {
11559
+ program.command("hud").description("Render node9 security statusline (spawned by Claude Code statusLine)").addHelpText(
11560
+ "after",
11561
+ `
11562
+ Outputs up to 3 lines to stdout, then exits:
11563
+
11564
+ Line 1 \u2014 Security state (always shown):
11565
+ \u{1F6E1} node9 | <mode> [shields] | \u2705 allowed \u{1F6D1} blocked \u{1F6A8} dlp ~$cost
11566
+ Shows "offline" if the node9 daemon is not running.
11567
+
11568
+ Line 2 \u2014 Claude context & rate limits (shown when available):
11569
+ <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)
11570
+ Only appears when Claude Code passes context_window / rate_limits data via stdin.
11571
+
11572
+ Line 3 \u2014 Environment counts (shown when non-zero):
11573
+ 2 CLAUDE.md | 5 rules | 4 MCPs | 3 hooks
11574
+ Counts CLAUDE.md files, rules/, MCP servers, and hook entries across user + project scope.
11575
+ Disable with: { "settings": { "hud": { "showEnvironmentCounts": false } } } in node9.config.json
11576
+
11577
+ Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
11578
+ Run "node9 addto claude" to register it as the statusLine.`
11579
+ ).argument("[subcommand]", 'Optional: "debug on" / "debug off" to toggle stdin logging').argument("[state]", 'on|off \u2014 used with "debug" subcommand').action(async (subcommand, state) => {
11580
+ if (subcommand === "debug") {
11581
+ const flagFile = import_path30.default.join(import_os23.default.homedir(), ".node9", "hud-debug");
11582
+ if (state === "on") {
11583
+ import_fs27.default.mkdirSync(import_path30.default.dirname(flagFile), { recursive: true });
11584
+ import_fs27.default.writeFileSync(flagFile, "");
11585
+ console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
11586
+ console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
11587
+ } else if (state === "off") {
11588
+ if (import_fs27.default.existsSync(flagFile)) import_fs27.default.unlinkSync(flagFile);
11589
+ console.log("HUD debug logging disabled.");
11590
+ } else {
11591
+ console.error("Usage: node9 hud debug on|off");
11592
+ process.exit(1);
11593
+ }
11594
+ return;
11595
+ }
11525
11596
  const { main: main2 } = await Promise.resolve().then(() => (init_hud(), hud_exports));
11526
11597
  await main2();
11527
11598
  });