@node9/proxy 1.6.0 → 1.7.1
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/README.md +0 -4
- package/dist/cli.js +545 -37
- package/dist/cli.mjs +545 -37
- package/dist/index.js +80 -1
- package/dist/index.mjs +80 -1
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -513,6 +513,84 @@ var init_shields = __esm({
|
|
|
513
513
|
],
|
|
514
514
|
dangerousWords: []
|
|
515
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
|
+
},
|
|
516
594
|
filesystem: {
|
|
517
595
|
name: "filesystem",
|
|
518
596
|
description: "Protects the local filesystem from dangerous AI operations",
|
|
@@ -799,7 +877,7 @@ var init_config = __esm({
|
|
|
799
877
|
DEFAULT_CONFIG = {
|
|
800
878
|
version: "1.0",
|
|
801
879
|
settings: {
|
|
802
|
-
mode: "
|
|
880
|
+
mode: "standard",
|
|
803
881
|
autoStartDaemon: true,
|
|
804
882
|
enableUndo: true,
|
|
805
883
|
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
@@ -3198,6 +3276,7 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
3198
3276
|
await notifyActivity({
|
|
3199
3277
|
id: actId,
|
|
3200
3278
|
tool: toolName,
|
|
3279
|
+
args,
|
|
3201
3280
|
ts: actTs,
|
|
3202
3281
|
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
|
|
3203
3282
|
label: result.blockedByLabel,
|
|
@@ -5476,6 +5555,7 @@ var init_session_counters = __esm({
|
|
|
5476
5555
|
_blocked = 0;
|
|
5477
5556
|
_dlpHits = 0;
|
|
5478
5557
|
_wouldBlock = 0;
|
|
5558
|
+
_estimatedCost = 0;
|
|
5479
5559
|
_lastRuleHit = null;
|
|
5480
5560
|
_lastBlockedTool = null;
|
|
5481
5561
|
incrementAllowed() {
|
|
@@ -5490,6 +5570,10 @@ var init_session_counters = __esm({
|
|
|
5490
5570
|
incrementWouldBlock() {
|
|
5491
5571
|
this._wouldBlock++;
|
|
5492
5572
|
}
|
|
5573
|
+
addCost(amount) {
|
|
5574
|
+
if (!isFinite(amount) || amount < 0) return;
|
|
5575
|
+
this._estimatedCost += amount;
|
|
5576
|
+
}
|
|
5493
5577
|
recordRuleHit(label) {
|
|
5494
5578
|
this._lastRuleHit = label;
|
|
5495
5579
|
}
|
|
@@ -5502,6 +5586,7 @@ var init_session_counters = __esm({
|
|
|
5502
5586
|
blocked: this._blocked,
|
|
5503
5587
|
dlpHits: this._dlpHits,
|
|
5504
5588
|
wouldBlock: this._wouldBlock,
|
|
5589
|
+
estimatedCost: this._estimatedCost,
|
|
5505
5590
|
lastRuleHit: this._lastRuleHit,
|
|
5506
5591
|
lastBlockedTool: this._lastBlockedTool
|
|
5507
5592
|
};
|
|
@@ -5511,6 +5596,7 @@ var init_session_counters = __esm({
|
|
|
5511
5596
|
this._blocked = 0;
|
|
5512
5597
|
this._dlpHits = 0;
|
|
5513
5598
|
this._wouldBlock = 0;
|
|
5599
|
+
this._estimatedCost = 0;
|
|
5514
5600
|
this._lastRuleHit = null;
|
|
5515
5601
|
this._lastBlockedTool = null;
|
|
5516
5602
|
}
|
|
@@ -5735,15 +5821,38 @@ function openBrowser(url) {
|
|
|
5735
5821
|
} catch {
|
|
5736
5822
|
}
|
|
5737
5823
|
}
|
|
5824
|
+
function estimateToolCost(tool, args) {
|
|
5825
|
+
const a = args ?? {};
|
|
5826
|
+
const t = tool.toLowerCase().replace(/[^a-z_]/g, "_");
|
|
5827
|
+
if (t.includes("read") || t === "glob" || t === "grep") {
|
|
5828
|
+
const filePath = a.file_path ?? a.path;
|
|
5829
|
+
if (filePath) {
|
|
5830
|
+
try {
|
|
5831
|
+
const bytes = import_fs13.default.statSync(filePath).size;
|
|
5832
|
+
return bytes / BYTES_PER_TOKEN / 1e6 * INPUT_PRICE_PER_1M;
|
|
5833
|
+
} catch {
|
|
5834
|
+
}
|
|
5835
|
+
}
|
|
5836
|
+
}
|
|
5837
|
+
if (t.includes("write")) {
|
|
5838
|
+
const content = a.content ?? "";
|
|
5839
|
+
return String(content).length / BYTES_PER_TOKEN / 1e6 * OUTPUT_PRICE_PER_1M;
|
|
5840
|
+
}
|
|
5841
|
+
if (t.includes("edit") || t === "str_replace_based_edit_tool") {
|
|
5842
|
+
const newStr = a.new_string ?? "";
|
|
5843
|
+
return String(newStr).length / BYTES_PER_TOKEN / 1e6 * OUTPUT_PRICE_PER_1M;
|
|
5844
|
+
}
|
|
5845
|
+
return void 0;
|
|
5846
|
+
}
|
|
5738
5847
|
function broadcast(event, data) {
|
|
5739
5848
|
if (event === "activity") {
|
|
5740
5849
|
activityRing.push({ event, data });
|
|
5741
5850
|
if (activityRing.length > ACTIVITY_RING_SIZE) activityRing.shift();
|
|
5742
5851
|
} else if (event === "activity-result") {
|
|
5743
|
-
const { id, status, label } = data;
|
|
5852
|
+
const { id, status, label, costEstimate } = data;
|
|
5744
5853
|
for (let i = activityRing.length - 1; i >= 0; i--) {
|
|
5745
5854
|
if (activityRing[i].data.id === id) {
|
|
5746
|
-
Object.assign(activityRing[i].data, { status, label });
|
|
5855
|
+
Object.assign(activityRing[i].data, { status, label, costEstimate });
|
|
5747
5856
|
break;
|
|
5748
5857
|
}
|
|
5749
5858
|
}
|
|
@@ -5845,10 +5954,13 @@ function startActivitySocket() {
|
|
|
5845
5954
|
sessionCounters.incrementBlocked();
|
|
5846
5955
|
sessionCounters.recordBlockedTool(data.tool);
|
|
5847
5956
|
}
|
|
5957
|
+
const costEstimate = data.status === "allow" ? estimateToolCost(data.tool, data.args) : void 0;
|
|
5958
|
+
if (costEstimate != null && costEstimate > 0) sessionCounters.addCost(costEstimate);
|
|
5848
5959
|
broadcast("activity-result", {
|
|
5849
5960
|
id: data.id,
|
|
5850
5961
|
status: data.status,
|
|
5851
|
-
label: data.label
|
|
5962
|
+
label: data.label,
|
|
5963
|
+
costEstimate
|
|
5852
5964
|
});
|
|
5853
5965
|
}
|
|
5854
5966
|
} catch {
|
|
@@ -5865,7 +5977,7 @@ function startActivitySocket() {
|
|
|
5865
5977
|
}
|
|
5866
5978
|
});
|
|
5867
5979
|
}
|
|
5868
|
-
var import_net2, import_fs13, import_path16, import_os11, import_child_process3, import_crypto5, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE, WRITE_TOOL_NAMES;
|
|
5980
|
+
var import_net2, import_fs13, import_path16, import_os11, import_child_process3, import_crypto5, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE, INPUT_PRICE_PER_1M, OUTPUT_PRICE_PER_1M, BYTES_PER_TOKEN, WRITE_TOOL_NAMES;
|
|
5869
5981
|
var init_state2 = __esm({
|
|
5870
5982
|
"src/daemon/state.ts"() {
|
|
5871
5983
|
"use strict";
|
|
@@ -5909,6 +6021,9 @@ var init_state2 = __esm({
|
|
|
5909
6021
|
ACTIVITY_RING_SIZE = 100;
|
|
5910
6022
|
activityRing = [];
|
|
5911
6023
|
SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
6024
|
+
INPUT_PRICE_PER_1M = 3;
|
|
6025
|
+
OUTPUT_PRICE_PER_1M = 15;
|
|
6026
|
+
BYTES_PER_TOKEN = 4;
|
|
5912
6027
|
WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
5913
6028
|
"write",
|
|
5914
6029
|
"write_file",
|
|
@@ -6366,7 +6481,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
6366
6481
|
allowed: counters.allowed,
|
|
6367
6482
|
blocked: counters.blocked,
|
|
6368
6483
|
dlpHits: counters.dlpHits,
|
|
6369
|
-
wouldBlock: counters.wouldBlock
|
|
6484
|
+
wouldBlock: counters.wouldBlock,
|
|
6485
|
+
estimatedCost: counters.estimatedCost
|
|
6370
6486
|
},
|
|
6371
6487
|
taintedCount: taintStore.list().length,
|
|
6372
6488
|
lastRuleHit: counters.lastRuleHit,
|
|
@@ -6498,6 +6614,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
6498
6614
|
}
|
|
6499
6615
|
if (req.method === "POST" && pathname === "/events/clear") {
|
|
6500
6616
|
activityRing.length = 0;
|
|
6617
|
+
sessionCounters.reset();
|
|
6501
6618
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6502
6619
|
return res.end(JSON.stringify({ ok: true }));
|
|
6503
6620
|
}
|
|
@@ -6811,7 +6928,7 @@ function formatBase(activity) {
|
|
|
6811
6928
|
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
6812
6929
|
const icon = getIcon(activity.tool);
|
|
6813
6930
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
6814
|
-
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
|
|
6931
|
+
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(import_os21.default.homedir(), "~");
|
|
6815
6932
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
6816
6933
|
return `${import_chalk17.default.gray(time)} ${icon} ${import_chalk17.default.white.bold(toolName)} ${import_chalk17.default.dim(argsPreview)}`;
|
|
6817
6934
|
}
|
|
@@ -6825,11 +6942,13 @@ function renderResult(activity, result) {
|
|
|
6825
6942
|
} else {
|
|
6826
6943
|
status = import_chalk17.default.red("\u2717 BLOCK");
|
|
6827
6944
|
}
|
|
6945
|
+
const cost = result.costEstimate ?? activity.costEstimate;
|
|
6946
|
+
const costSuffix = cost == null ? "" : import_chalk17.default.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
|
|
6828
6947
|
if (process.stdout.isTTY) {
|
|
6829
6948
|
import_readline5.default.clearLine(process.stdout, 0);
|
|
6830
6949
|
import_readline5.default.cursorTo(process.stdout, 0);
|
|
6831
6950
|
}
|
|
6832
|
-
console.log(`${base} ${status}`);
|
|
6951
|
+
console.log(`${base} ${status}${costSuffix}`);
|
|
6833
6952
|
}
|
|
6834
6953
|
function renderPending(activity) {
|
|
6835
6954
|
if (!process.stdout.isTTY) return;
|
|
@@ -6958,6 +7077,39 @@ function buildRecoveryCardLines(req) {
|
|
|
6958
7077
|
``
|
|
6959
7078
|
];
|
|
6960
7079
|
}
|
|
7080
|
+
function readApproversFromDisk() {
|
|
7081
|
+
const configPath = import_path28.default.join(import_os21.default.homedir(), ".node9", "config.json");
|
|
7082
|
+
try {
|
|
7083
|
+
const raw = JSON.parse(import_fs25.default.readFileSync(configPath, "utf-8"));
|
|
7084
|
+
const settings = raw.settings ?? {};
|
|
7085
|
+
return settings.approvers ?? {};
|
|
7086
|
+
} catch {
|
|
7087
|
+
return {};
|
|
7088
|
+
}
|
|
7089
|
+
}
|
|
7090
|
+
function approverStatusLine() {
|
|
7091
|
+
const a = readApproversFromDisk();
|
|
7092
|
+
const fmt = (label, key) => {
|
|
7093
|
+
const on = a[key] !== false;
|
|
7094
|
+
return `[${key[0]}]${label.slice(1)} ${on ? import_chalk17.default.green("\u2713") : import_chalk17.default.dim("\u2717")}`;
|
|
7095
|
+
};
|
|
7096
|
+
return `${fmt("native", "native")} ${fmt("browser", "browser")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
|
|
7097
|
+
}
|
|
7098
|
+
function toggleApprover(channel) {
|
|
7099
|
+
const configPath = import_path28.default.join(import_os21.default.homedir(), ".node9", "config.json");
|
|
7100
|
+
try {
|
|
7101
|
+
const raw = JSON.parse(import_fs25.default.readFileSync(configPath, "utf-8"));
|
|
7102
|
+
const settings = raw.settings ?? {};
|
|
7103
|
+
const approvers = settings.approvers ?? {};
|
|
7104
|
+
approvers[channel] = approvers[channel] === false;
|
|
7105
|
+
settings.approvers = approvers;
|
|
7106
|
+
raw.settings = settings;
|
|
7107
|
+
import_fs25.default.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
|
|
7108
|
+
} catch (err2) {
|
|
7109
|
+
process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
|
|
7110
|
+
`);
|
|
7111
|
+
}
|
|
7112
|
+
}
|
|
6961
7113
|
async function startTail(options = {}) {
|
|
6962
7114
|
const port = await ensureDaemon();
|
|
6963
7115
|
if (options.clear) {
|
|
@@ -6996,6 +7148,7 @@ async function startTail(options = {}) {
|
|
|
6996
7148
|
}
|
|
6997
7149
|
const connectionTime = Date.now();
|
|
6998
7150
|
const activityPending = /* @__PURE__ */ new Map();
|
|
7151
|
+
const orphanedResults = /* @__PURE__ */ new Map();
|
|
6999
7152
|
let csrfToken = "";
|
|
7000
7153
|
const approvalQueue = [];
|
|
7001
7154
|
let cardActive = false;
|
|
@@ -7004,6 +7157,44 @@ async function startTail(options = {}) {
|
|
|
7004
7157
|
const localAllowCounts = /* @__PURE__ */ new Map();
|
|
7005
7158
|
const canApprove = process.stdout.isTTY && process.stdin.isTTY;
|
|
7006
7159
|
if (canApprove) import_readline5.default.emitKeypressEvents(process.stdin);
|
|
7160
|
+
let idleKeypressHandler = null;
|
|
7161
|
+
function enterIdleMode() {
|
|
7162
|
+
if (!canApprove || idleKeypressHandler !== null) return;
|
|
7163
|
+
try {
|
|
7164
|
+
process.stdin.setRawMode(true);
|
|
7165
|
+
} catch {
|
|
7166
|
+
return;
|
|
7167
|
+
}
|
|
7168
|
+
process.stdin.resume();
|
|
7169
|
+
idleKeypressHandler = (_str, key) => {
|
|
7170
|
+
const name = key?.name ?? "";
|
|
7171
|
+
if (key?.ctrl && name === "c") {
|
|
7172
|
+
process.kill(process.pid, "SIGINT");
|
|
7173
|
+
return;
|
|
7174
|
+
}
|
|
7175
|
+
if (name === "q") {
|
|
7176
|
+
process.kill(process.pid, "SIGINT");
|
|
7177
|
+
return;
|
|
7178
|
+
}
|
|
7179
|
+
const channel = name === "n" ? "native" : name === "b" ? "browser" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
|
|
7180
|
+
if (channel) {
|
|
7181
|
+
toggleApprover(channel);
|
|
7182
|
+
console.log(import_chalk17.default.dim(` Approvers: ${approverStatusLine()}`));
|
|
7183
|
+
}
|
|
7184
|
+
};
|
|
7185
|
+
process.stdin.on("keypress", idleKeypressHandler);
|
|
7186
|
+
}
|
|
7187
|
+
function exitIdleMode() {
|
|
7188
|
+
if (idleKeypressHandler) {
|
|
7189
|
+
process.stdin.removeListener("keypress", idleKeypressHandler);
|
|
7190
|
+
idleKeypressHandler = null;
|
|
7191
|
+
}
|
|
7192
|
+
try {
|
|
7193
|
+
process.stdin.setRawMode(false);
|
|
7194
|
+
} catch {
|
|
7195
|
+
}
|
|
7196
|
+
process.stdin.pause();
|
|
7197
|
+
}
|
|
7007
7198
|
function clearCard() {
|
|
7008
7199
|
if (cardLineCount > 0) {
|
|
7009
7200
|
import_readline5.default.moveCursor(process.stdout, 0, -cardLineCount);
|
|
@@ -7022,10 +7213,12 @@ async function startTail(options = {}) {
|
|
|
7022
7213
|
}
|
|
7023
7214
|
function showNextCard() {
|
|
7024
7215
|
if (cardActive || approvalQueue.length === 0 || !canApprove) return;
|
|
7216
|
+
exitIdleMode();
|
|
7025
7217
|
try {
|
|
7026
7218
|
process.stdin.setRawMode(true);
|
|
7027
7219
|
} catch {
|
|
7028
7220
|
cardActive = false;
|
|
7221
|
+
enterIdleMode();
|
|
7029
7222
|
return;
|
|
7030
7223
|
}
|
|
7031
7224
|
cardActive = true;
|
|
@@ -7037,12 +7230,8 @@ async function startTail(options = {}) {
|
|
|
7037
7230
|
const handler = onKeypress;
|
|
7038
7231
|
onKeypress = null;
|
|
7039
7232
|
if (handler) process.stdin.removeListener("keypress", handler);
|
|
7040
|
-
try {
|
|
7041
|
-
process.stdin.setRawMode(false);
|
|
7042
|
-
} catch {
|
|
7043
|
-
}
|
|
7044
|
-
process.stdin.pause();
|
|
7045
7233
|
cancelActiveCard = null;
|
|
7234
|
+
enterIdleMode();
|
|
7046
7235
|
};
|
|
7047
7236
|
const settle = (action) => {
|
|
7048
7237
|
if (settled) return;
|
|
@@ -7169,18 +7358,16 @@ async function startTail(options = {}) {
|
|
|
7169
7358
|
console.log(import_chalk17.default.cyan.bold(`
|
|
7170
7359
|
\u{1F6F0}\uFE0F Node9 tail `) + import_chalk17.default.dim(`\u2192 ${dashboardUrl}`));
|
|
7171
7360
|
if (canApprove) {
|
|
7172
|
-
console.log(
|
|
7173
|
-
|
|
7174
|
-
);
|
|
7361
|
+
console.log(import_chalk17.default.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
|
|
7362
|
+
console.log(import_chalk17.default.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
|
|
7175
7363
|
}
|
|
7176
7364
|
if (options.history) {
|
|
7177
|
-
console.log(import_chalk17.default.dim("Showing history + live events
|
|
7365
|
+
console.log(import_chalk17.default.dim("Showing history + live events.\n"));
|
|
7178
7366
|
} else {
|
|
7179
|
-
console.log(
|
|
7180
|
-
import_chalk17.default.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
|
|
7181
|
-
);
|
|
7367
|
+
console.log(import_chalk17.default.dim("Showing live events only. Use --history to include past.\n"));
|
|
7182
7368
|
}
|
|
7183
7369
|
process.on("SIGINT", () => {
|
|
7370
|
+
exitIdleMode();
|
|
7184
7371
|
clearCard();
|
|
7185
7372
|
process.stdout.write(SHOW_CURSOR);
|
|
7186
7373
|
if (process.stdout.isTTY) {
|
|
@@ -7196,6 +7383,7 @@ async function startTail(options = {}) {
|
|
|
7196
7383
|
console.error(import_chalk17.default.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
7197
7384
|
process.exit(1);
|
|
7198
7385
|
}
|
|
7386
|
+
if (canApprove) enterIdleMode();
|
|
7199
7387
|
let currentEvent = "";
|
|
7200
7388
|
let currentData = "";
|
|
7201
7389
|
res.on("error", () => {
|
|
@@ -7295,9 +7483,14 @@ async function startTail(options = {}) {
|
|
|
7295
7483
|
renderResult(data, data);
|
|
7296
7484
|
return;
|
|
7297
7485
|
}
|
|
7486
|
+
const orphaned = orphanedResults.get(data.id);
|
|
7487
|
+
if (orphaned) {
|
|
7488
|
+
orphanedResults.delete(data.id);
|
|
7489
|
+
renderResult(data, orphaned);
|
|
7490
|
+
return;
|
|
7491
|
+
}
|
|
7298
7492
|
activityPending.set(data.id, data);
|
|
7299
|
-
|
|
7300
|
-
if (slowTool) renderPending(data);
|
|
7493
|
+
renderPending(data);
|
|
7301
7494
|
}
|
|
7302
7495
|
if (event === "snapshot") {
|
|
7303
7496
|
const time = new Date(data.ts).toLocaleTimeString([], { hour12: false });
|
|
@@ -7316,6 +7509,8 @@ async function startTail(options = {}) {
|
|
|
7316
7509
|
if (original) {
|
|
7317
7510
|
renderResult(original, data);
|
|
7318
7511
|
activityPending.delete(data.id);
|
|
7512
|
+
} else {
|
|
7513
|
+
orphanedResults.set(data.id, data);
|
|
7319
7514
|
}
|
|
7320
7515
|
}
|
|
7321
7516
|
}
|
|
@@ -7558,6 +7753,29 @@ function renderOffline() {
|
|
|
7558
7753
|
process.stdout.write(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")} ${dim("|")} ${dim("offline")}
|
|
7559
7754
|
`);
|
|
7560
7755
|
}
|
|
7756
|
+
function readActiveShieldsHud() {
|
|
7757
|
+
const now = Date.now();
|
|
7758
|
+
if (shieldsCache && now - shieldsCache.ts < SHIELDS_CACHE_TTL_MS) {
|
|
7759
|
+
return shieldsCache.value;
|
|
7760
|
+
}
|
|
7761
|
+
try {
|
|
7762
|
+
const shieldsPath = import_path29.default.join(import_os22.default.homedir(), ".node9", "shields.json");
|
|
7763
|
+
if (!import_fs26.default.existsSync(shieldsPath)) {
|
|
7764
|
+
shieldsCache = { value: [], ts: now };
|
|
7765
|
+
return [];
|
|
7766
|
+
}
|
|
7767
|
+
const parsed = JSON.parse(import_fs26.default.readFileSync(shieldsPath, "utf-8"));
|
|
7768
|
+
if (!Array.isArray(parsed.active)) {
|
|
7769
|
+
shieldsCache = { value: [], ts: now };
|
|
7770
|
+
return [];
|
|
7771
|
+
}
|
|
7772
|
+
const value = parsed.active.filter((s) => typeof s === "string").map((s) => s.slice(0, 64)).slice(0, 20);
|
|
7773
|
+
shieldsCache = { value, ts: now };
|
|
7774
|
+
return value;
|
|
7775
|
+
} catch {
|
|
7776
|
+
return [];
|
|
7777
|
+
}
|
|
7778
|
+
}
|
|
7561
7779
|
function renderSecurityLine(status) {
|
|
7562
7780
|
const parts = [];
|
|
7563
7781
|
parts.push(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")}`);
|
|
@@ -7575,6 +7793,18 @@ function renderSecurityLine(status) {
|
|
|
7575
7793
|
};
|
|
7576
7794
|
const mc = modeColors[status.mode] ?? WHITE;
|
|
7577
7795
|
parts.push(`${dim("|")} ${color(mc, modeIcon[status.mode] ?? "")}${color(mc, status.mode)}`);
|
|
7796
|
+
const activeShields = readActiveShieldsHud();
|
|
7797
|
+
if (activeShields.length > 0) {
|
|
7798
|
+
const shieldAbbrevs = {
|
|
7799
|
+
"bash-safe": "bash",
|
|
7800
|
+
filesystem: "fs",
|
|
7801
|
+
postgres: "pg",
|
|
7802
|
+
github: "gh",
|
|
7803
|
+
aws: "aws"
|
|
7804
|
+
};
|
|
7805
|
+
const labels = activeShields.map((s) => shieldAbbrevs[s] ?? s).join(" ");
|
|
7806
|
+
parts.push(color(DIM, `[${labels}]`));
|
|
7807
|
+
}
|
|
7578
7808
|
if (status.mode === "observe") {
|
|
7579
7809
|
parts.push(`${dim("|")} ${color(GREEN2, `\u2705 ${status.session.allowed} passed`)}`);
|
|
7580
7810
|
if (status.session.wouldBlock > 0) {
|
|
@@ -7589,6 +7819,11 @@ function renderSecurityLine(status) {
|
|
|
7589
7819
|
parts.push(color(RED2, `\u{1F6A8} ${status.session.dlpHits} dlp`));
|
|
7590
7820
|
}
|
|
7591
7821
|
}
|
|
7822
|
+
if (status.session.estimatedCost > 0) {
|
|
7823
|
+
const cost = status.session.estimatedCost;
|
|
7824
|
+
const costStr = cost >= 0.01 ? `$${cost.toFixed(2)}` : cost >= 1e-3 ? `$${cost.toFixed(3)}` : "<$0.001";
|
|
7825
|
+
parts.push(color(DIM, `~${costStr}`));
|
|
7826
|
+
}
|
|
7592
7827
|
if (status.taintedCount > 0) {
|
|
7593
7828
|
parts.push(color(YELLOW2, `\u{1F4A7} ${status.taintedCount} tainted`));
|
|
7594
7829
|
}
|
|
@@ -7666,7 +7901,7 @@ async function main() {
|
|
|
7666
7901
|
renderOffline();
|
|
7667
7902
|
}
|
|
7668
7903
|
}
|
|
7669
|
-
var import_fs26, import_path29, import_os22, import_http3, RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH;
|
|
7904
|
+
var import_fs26, import_path29, import_os22, import_http3, RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH, shieldsCache, SHIELDS_CACHE_TTL_MS;
|
|
7670
7905
|
var init_hud = __esm({
|
|
7671
7906
|
"src/cli/hud.ts"() {
|
|
7672
7907
|
"use strict";
|
|
@@ -7688,6 +7923,8 @@ var init_hud = __esm({
|
|
|
7688
7923
|
BAR_FILLED = "\u2588";
|
|
7689
7924
|
BAR_EMPTY = "\u2591";
|
|
7690
7925
|
BAR_WIDTH = 10;
|
|
7926
|
+
shieldsCache = null;
|
|
7927
|
+
SHIELDS_CACHE_TTL_MS = 2e3;
|
|
7691
7928
|
}
|
|
7692
7929
|
});
|
|
7693
7930
|
|
|
@@ -7701,6 +7938,7 @@ var import_path14 = __toESM(require("path"));
|
|
|
7701
7938
|
var import_os10 = __toESM(require("os"));
|
|
7702
7939
|
var import_chalk = __toESM(require("chalk"));
|
|
7703
7940
|
var import_prompts = require("@inquirer/prompts");
|
|
7941
|
+
var import_smol_toml = require("smol-toml");
|
|
7704
7942
|
var NODE9_MCP_SERVER_ENTRY = { command: "node9", args: ["mcp-server"] };
|
|
7705
7943
|
function hasNode9McpServer(servers) {
|
|
7706
7944
|
const entry = servers["node9"];
|
|
@@ -8064,7 +8302,8 @@ function detectAgents(homeDir2 = import_os10.default.homedir()) {
|
|
|
8064
8302
|
return {
|
|
8065
8303
|
claude: exists(import_path14.default.join(homeDir2, ".claude")) || exists(import_path14.default.join(homeDir2, ".claude.json")),
|
|
8066
8304
|
gemini: exists(import_path14.default.join(homeDir2, ".gemini")),
|
|
8067
|
-
cursor: exists(import_path14.default.join(homeDir2, ".cursor"))
|
|
8305
|
+
cursor: exists(import_path14.default.join(homeDir2, ".cursor")),
|
|
8306
|
+
codex: exists(import_path14.default.join(homeDir2, ".codex"))
|
|
8068
8307
|
};
|
|
8069
8308
|
}
|
|
8070
8309
|
async function setupCursor() {
|
|
@@ -8129,6 +8368,82 @@ async function setupCursor() {
|
|
|
8129
8368
|
printDaemonTip();
|
|
8130
8369
|
}
|
|
8131
8370
|
}
|
|
8371
|
+
function readToml(filePath) {
|
|
8372
|
+
try {
|
|
8373
|
+
if (import_fs11.default.existsSync(filePath)) {
|
|
8374
|
+
return (0, import_smol_toml.parse)(import_fs11.default.readFileSync(filePath, "utf-8"));
|
|
8375
|
+
}
|
|
8376
|
+
} catch {
|
|
8377
|
+
}
|
|
8378
|
+
return null;
|
|
8379
|
+
}
|
|
8380
|
+
function writeToml(filePath, data) {
|
|
8381
|
+
const dir = import_path14.default.dirname(filePath);
|
|
8382
|
+
if (!import_fs11.default.existsSync(dir)) import_fs11.default.mkdirSync(dir, { recursive: true });
|
|
8383
|
+
import_fs11.default.writeFileSync(filePath, (0, import_smol_toml.stringify)(data));
|
|
8384
|
+
}
|
|
8385
|
+
async function setupCodex() {
|
|
8386
|
+
const homeDir2 = import_os10.default.homedir();
|
|
8387
|
+
const configPath = import_path14.default.join(homeDir2, ".codex", "config.toml");
|
|
8388
|
+
const config = readToml(configPath) ?? {};
|
|
8389
|
+
const servers = config.mcp_servers ?? {};
|
|
8390
|
+
let anythingChanged = false;
|
|
8391
|
+
if (!hasNode9McpServer(servers)) {
|
|
8392
|
+
servers["node9"] = NODE9_MCP_SERVER_ENTRY;
|
|
8393
|
+
config.mcp_servers = servers;
|
|
8394
|
+
writeToml(configPath, config);
|
|
8395
|
+
console.log(import_chalk.default.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
|
|
8396
|
+
anythingChanged = true;
|
|
8397
|
+
}
|
|
8398
|
+
const serversToWrap = [];
|
|
8399
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
8400
|
+
if (!server.command || server.command === "node9") continue;
|
|
8401
|
+
const parts = [server.command, ...server.args ?? []];
|
|
8402
|
+
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
8403
|
+
}
|
|
8404
|
+
if (serversToWrap.length > 0) {
|
|
8405
|
+
console.log(import_chalk.default.bold("The following existing entries will be modified:\n"));
|
|
8406
|
+
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}`));
|
|
8409
|
+
}
|
|
8410
|
+
console.log("");
|
|
8411
|
+
const proceed = await (0, import_prompts.confirm)({ message: "Wrap these MCP servers?", default: true });
|
|
8412
|
+
if (proceed) {
|
|
8413
|
+
for (const { name, parts } of serversToWrap) {
|
|
8414
|
+
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
8415
|
+
}
|
|
8416
|
+
config.mcp_servers = servers;
|
|
8417
|
+
writeToml(configPath, config);
|
|
8418
|
+
console.log(import_chalk.default.green(`
|
|
8419
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
8420
|
+
anythingChanged = true;
|
|
8421
|
+
} else {
|
|
8422
|
+
console.log(import_chalk.default.yellow(" Skipped MCP server wrapping."));
|
|
8423
|
+
}
|
|
8424
|
+
console.log("");
|
|
8425
|
+
}
|
|
8426
|
+
console.log(
|
|
8427
|
+
import_chalk.default.yellow(
|
|
8428
|
+
" \u26A0\uFE0F Note: Codex does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Codex.\n Native bash and file operations are not monitored."
|
|
8429
|
+
)
|
|
8430
|
+
);
|
|
8431
|
+
console.log("");
|
|
8432
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
8433
|
+
console.log(
|
|
8434
|
+
import_chalk.default.blue(
|
|
8435
|
+
"\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.codex/config.toml and re-run."
|
|
8436
|
+
)
|
|
8437
|
+
);
|
|
8438
|
+
printDaemonTip();
|
|
8439
|
+
return;
|
|
8440
|
+
}
|
|
8441
|
+
if (anythingChanged) {
|
|
8442
|
+
console.log(import_chalk.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Codex via MCP proxy!"));
|
|
8443
|
+
console.log(import_chalk.default.gray(" Restart Codex for changes to take effect."));
|
|
8444
|
+
printDaemonTip();
|
|
8445
|
+
}
|
|
8446
|
+
}
|
|
8132
8447
|
function setupHud() {
|
|
8133
8448
|
const homeDir2 = import_os10.default.homedir();
|
|
8134
8449
|
const hooksPath = import_path14.default.join(homeDir2, ".claude", "settings.json");
|
|
@@ -8621,6 +8936,9 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
8621
8936
|
if (filesRes.status === 0) {
|
|
8622
8937
|
capturedFiles = filesRes.stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
8623
8938
|
}
|
|
8939
|
+
if (capturedFiles.length === 0) {
|
|
8940
|
+
return prevEntry.hash;
|
|
8941
|
+
}
|
|
8624
8942
|
const diffRes = (0, import_child_process8.spawnSync)("git", ["diff", prevEntry.hash, commitHash], {
|
|
8625
8943
|
env: shadowEnv,
|
|
8626
8944
|
timeout: GIT_TIMEOUT
|
|
@@ -9817,25 +10135,91 @@ var import_chalk11 = __toESM(require("chalk"));
|
|
|
9817
10135
|
var import_fs23 = __toESM(require("fs"));
|
|
9818
10136
|
var import_path25 = __toESM(require("path"));
|
|
9819
10137
|
var import_os19 = __toESM(require("os"));
|
|
10138
|
+
var import_https = __toESM(require("https"));
|
|
9820
10139
|
init_core();
|
|
10140
|
+
init_shields();
|
|
10141
|
+
var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
|
|
10142
|
+
function fireTelemetryPing(agents) {
|
|
10143
|
+
try {
|
|
10144
|
+
const body = JSON.stringify({
|
|
10145
|
+
event: "init_completed",
|
|
10146
|
+
agents_detected: agents,
|
|
10147
|
+
os: process.platform,
|
|
10148
|
+
node9_version: process.env.npm_package_version ?? "unknown"
|
|
10149
|
+
});
|
|
10150
|
+
const req = import_https.default.request(
|
|
10151
|
+
{
|
|
10152
|
+
hostname: "api.node9.ai",
|
|
10153
|
+
path: "/api/v1/telemetry",
|
|
10154
|
+
method: "POST",
|
|
10155
|
+
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
|
|
10156
|
+
timeout: 3e3
|
|
10157
|
+
},
|
|
10158
|
+
(res) => {
|
|
10159
|
+
res.resume();
|
|
10160
|
+
}
|
|
10161
|
+
);
|
|
10162
|
+
req.on("error", () => {
|
|
10163
|
+
});
|
|
10164
|
+
req.on("timeout", () => {
|
|
10165
|
+
req.destroy();
|
|
10166
|
+
});
|
|
10167
|
+
req.end(body);
|
|
10168
|
+
} catch {
|
|
10169
|
+
}
|
|
10170
|
+
}
|
|
9821
10171
|
function registerInitCommand(program2) {
|
|
9822
10172
|
program2.command("init").description("Set up Node9: create config and wire all detected AI agents").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").option("--skip-setup", "Only create config \u2014 do not wire AI agents").action(async (options) => {
|
|
9823
10173
|
console.log(import_chalk11.default.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
|
|
10174
|
+
let chosenMode = options.mode.toLowerCase();
|
|
10175
|
+
if (!["standard", "strict", "audit"].includes(chosenMode)) {
|
|
10176
|
+
chosenMode = DEFAULT_CONFIG.settings.mode;
|
|
10177
|
+
}
|
|
10178
|
+
{
|
|
10179
|
+
const { confirm: confirm3 } = await import("@inquirer/prompts");
|
|
10180
|
+
const enableShields = await confirm3({
|
|
10181
|
+
message: "Enable recommended safety shields? (blocks rm -rf, SQL drops, pipe-to-shell)",
|
|
10182
|
+
default: true
|
|
10183
|
+
});
|
|
10184
|
+
if (enableShields) {
|
|
10185
|
+
chosenMode = "standard";
|
|
10186
|
+
try {
|
|
10187
|
+
const current = readActiveShields();
|
|
10188
|
+
const merged = Array.from(/* @__PURE__ */ new Set([...current, ...DEFAULT_SHIELDS]));
|
|
10189
|
+
const hasNewShields = DEFAULT_SHIELDS.some((s) => !current.includes(s));
|
|
10190
|
+
if (hasNewShields) writeActiveShields(merged);
|
|
10191
|
+
} catch (err2) {
|
|
10192
|
+
console.log(import_chalk11.default.yellow(` \u26A0\uFE0F Could not update shields: ${String(err2)}`));
|
|
10193
|
+
}
|
|
10194
|
+
}
|
|
10195
|
+
console.log("");
|
|
10196
|
+
}
|
|
9824
10197
|
const configPath = import_path25.default.join(import_os19.default.homedir(), ".node9", "config.json");
|
|
9825
10198
|
if (import_fs23.default.existsSync(configPath) && !options.force) {
|
|
9826
|
-
|
|
10199
|
+
try {
|
|
10200
|
+
const existing = JSON.parse(import_fs23.default.readFileSync(configPath, "utf-8"));
|
|
10201
|
+
const settings = existing.settings ?? {};
|
|
10202
|
+
if (settings.mode !== chosenMode) {
|
|
10203
|
+
settings.mode = chosenMode;
|
|
10204
|
+
existing.settings = settings;
|
|
10205
|
+
import_fs23.default.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
|
|
10206
|
+
console.log(import_chalk11.default.green(`\u2705 Mode updated: ${chosenMode}`));
|
|
10207
|
+
} else {
|
|
10208
|
+
console.log(import_chalk11.default.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
10209
|
+
}
|
|
10210
|
+
} catch {
|
|
10211
|
+
console.log(import_chalk11.default.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
10212
|
+
}
|
|
9827
10213
|
} else {
|
|
9828
|
-
const requestedMode = options.mode.toLowerCase();
|
|
9829
|
-
const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
|
|
9830
10214
|
const configToSave = {
|
|
9831
10215
|
...DEFAULT_CONFIG,
|
|
9832
|
-
settings: { ...DEFAULT_CONFIG.settings, mode:
|
|
10216
|
+
settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
|
|
9833
10217
|
};
|
|
9834
10218
|
const dir = import_path25.default.dirname(configPath);
|
|
9835
10219
|
if (!import_fs23.default.existsSync(dir)) import_fs23.default.mkdirSync(dir, { recursive: true });
|
|
9836
|
-
import_fs23.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
10220
|
+
import_fs23.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
|
|
9837
10221
|
console.log(import_chalk11.default.green(`\u2705 Config created: ${configPath}`));
|
|
9838
|
-
console.log(import_chalk11.default.gray(` Mode: ${
|
|
10222
|
+
console.log(import_chalk11.default.gray(` Mode: ${chosenMode}`));
|
|
9839
10223
|
}
|
|
9840
10224
|
if (options.skipSetup) return;
|
|
9841
10225
|
console.log("");
|
|
@@ -9845,9 +10229,9 @@ function registerInitCommand(program2) {
|
|
|
9845
10229
|
);
|
|
9846
10230
|
if (found.length === 0) {
|
|
9847
10231
|
console.log(
|
|
9848
|
-
import_chalk11.default.gray("No AI agents detected. Install Claude Code, Gemini CLI, or
|
|
10232
|
+
import_chalk11.default.gray("No AI agents detected. Install Claude Code, Gemini CLI, Cursor, or Codex")
|
|
9849
10233
|
);
|
|
9850
|
-
console.log(import_chalk11.default.gray("then run: node9 addto <claude|gemini|cursor>"));
|
|
10234
|
+
console.log(import_chalk11.default.gray("then run: node9 addto <claude|gemini|cursor|codex>"));
|
|
9851
10235
|
return;
|
|
9852
10236
|
}
|
|
9853
10237
|
console.log(import_chalk11.default.bold("Detected agents:"));
|
|
@@ -9860,16 +10244,23 @@ function registerInitCommand(program2) {
|
|
|
9860
10244
|
if (agent === "claude") await setupClaude();
|
|
9861
10245
|
else if (agent === "gemini") await setupGemini();
|
|
9862
10246
|
else if (agent === "cursor") await setupCursor();
|
|
10247
|
+
else if (agent === "codex") await setupCodex();
|
|
9863
10248
|
console.log("");
|
|
9864
10249
|
}
|
|
9865
|
-
|
|
9866
|
-
|
|
9867
|
-
|
|
9868
|
-
|
|
10250
|
+
{
|
|
10251
|
+
const { confirm: confirm3 } = await import("@inquirer/prompts");
|
|
10252
|
+
const sendTelemetry = await confirm3({
|
|
10253
|
+
message: "Send anonymous usage stats to help improve node9? (no code, no args)",
|
|
10254
|
+
default: true
|
|
10255
|
+
});
|
|
10256
|
+
if (sendTelemetry) fireTelemetryPing(found);
|
|
9869
10257
|
console.log("");
|
|
9870
10258
|
}
|
|
9871
10259
|
console.log(import_chalk11.default.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
|
|
9872
|
-
console.log(
|
|
10260
|
+
console.log("");
|
|
10261
|
+
console.log(import_chalk11.default.white(" Start watching: ") + import_chalk11.default.cyan("node9 tail"));
|
|
10262
|
+
console.log(import_chalk11.default.white(" Browser view: ") + import_chalk11.default.cyan("node9 daemon --openui"));
|
|
10263
|
+
console.log(import_chalk11.default.white(" Cloud dashboard: ") + import_chalk11.default.cyan("node9.ai"));
|
|
9873
10264
|
});
|
|
9874
10265
|
}
|
|
9875
10266
|
|
|
@@ -10475,6 +10866,44 @@ var TOOLS = [
|
|
|
10475
10866
|
required: ["service"]
|
|
10476
10867
|
}
|
|
10477
10868
|
},
|
|
10869
|
+
{
|
|
10870
|
+
name: "node9_shield_disable",
|
|
10871
|
+
description: "Disable a node9 shield. Use node9_shield_list to see currently active shields.",
|
|
10872
|
+
inputSchema: {
|
|
10873
|
+
type: "object",
|
|
10874
|
+
properties: {
|
|
10875
|
+
service: {
|
|
10876
|
+
type: "string",
|
|
10877
|
+
description: 'Shield name to disable (e.g. "postgres", "aws", "github", "filesystem").'
|
|
10878
|
+
}
|
|
10879
|
+
},
|
|
10880
|
+
required: ["service"]
|
|
10881
|
+
}
|
|
10882
|
+
},
|
|
10883
|
+
{
|
|
10884
|
+
name: "node9_approver_list",
|
|
10885
|
+
description: "List all node9 approver channels and their current enabled/disabled state. Approvers are the channels through which node9 asks a human to approve risky tool calls. Channels: native (OS popup), browser (web UI), cloud (team policy server), terminal (stdin).",
|
|
10886
|
+
inputSchema: { type: "object", properties: {}, required: [] }
|
|
10887
|
+
},
|
|
10888
|
+
{
|
|
10889
|
+
name: "node9_approver_set",
|
|
10890
|
+
description: "Enable or disable a specific node9 approver channel in the global config (~/.node9/config.json). Use this to turn individual channels on or off without touching other settings. Channels: native, browser, cloud, terminal. WARNING: disabling all approvers means node9 cannot prompt for human approval \u2014 use with care.",
|
|
10891
|
+
inputSchema: {
|
|
10892
|
+
type: "object",
|
|
10893
|
+
properties: {
|
|
10894
|
+
channel: {
|
|
10895
|
+
type: "string",
|
|
10896
|
+
enum: ["native", "browser", "cloud", "terminal"],
|
|
10897
|
+
description: "Approver channel to configure."
|
|
10898
|
+
},
|
|
10899
|
+
enabled: {
|
|
10900
|
+
type: "boolean",
|
|
10901
|
+
description: "true to enable the channel, false to disable it."
|
|
10902
|
+
}
|
|
10903
|
+
},
|
|
10904
|
+
required: ["channel", "enabled"]
|
|
10905
|
+
}
|
|
10906
|
+
},
|
|
10478
10907
|
{
|
|
10479
10908
|
name: "node9_undo_list",
|
|
10480
10909
|
description: "List the node9 snapshot history. Each entry shows the git hash, tool that triggered it, a short summary, affected files, working directory, and timestamp. Use this to find a hash before calling node9_undo_revert.",
|
|
@@ -10580,6 +11009,79 @@ function handleShieldEnable(args) {
|
|
|
10580
11009
|
const shield = getShield(name);
|
|
10581
11010
|
return `Shield "${name}" enabled \u2014 ${shield.smartRules.length} smart rule${shield.smartRules.length === 1 ? "" : "s"} now active.`;
|
|
10582
11011
|
}
|
|
11012
|
+
function handleShieldDisable(args) {
|
|
11013
|
+
const service = args.service;
|
|
11014
|
+
if (typeof service !== "string" || !service) {
|
|
11015
|
+
throw new Error("service is required");
|
|
11016
|
+
}
|
|
11017
|
+
const name = resolveShieldName(service);
|
|
11018
|
+
if (!name) {
|
|
11019
|
+
throw new Error(
|
|
11020
|
+
`Unknown shield: "${service}". Run node9_shield_list to see available shields.`
|
|
11021
|
+
);
|
|
11022
|
+
}
|
|
11023
|
+
const active = readActiveShields();
|
|
11024
|
+
if (!active.includes(name)) {
|
|
11025
|
+
return `Shield "${name}" is not active.`;
|
|
11026
|
+
}
|
|
11027
|
+
writeActiveShields(active.filter((s) => s !== name));
|
|
11028
|
+
return `Shield "${name}" disabled.`;
|
|
11029
|
+
}
|
|
11030
|
+
var GLOBAL_CONFIG_PATH2 = import_path27.default.join(import_os20.default.homedir(), ".node9", "config.json");
|
|
11031
|
+
var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
|
|
11032
|
+
function readGlobalConfigRaw() {
|
|
11033
|
+
try {
|
|
11034
|
+
if (import_fs24.default.existsSync(GLOBAL_CONFIG_PATH2)) {
|
|
11035
|
+
return JSON.parse(import_fs24.default.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
|
|
11036
|
+
}
|
|
11037
|
+
} catch {
|
|
11038
|
+
}
|
|
11039
|
+
return {};
|
|
11040
|
+
}
|
|
11041
|
+
function writeGlobalConfigRaw(data) {
|
|
11042
|
+
const dir = import_path27.default.dirname(GLOBAL_CONFIG_PATH2);
|
|
11043
|
+
if (!import_fs24.default.existsSync(dir)) import_fs24.default.mkdirSync(dir, { recursive: true });
|
|
11044
|
+
import_fs24.default.writeFileSync(GLOBAL_CONFIG_PATH2, JSON.stringify(data, null, 2) + "\n");
|
|
11045
|
+
}
|
|
11046
|
+
function handleApproverList() {
|
|
11047
|
+
const config = getConfig();
|
|
11048
|
+
const approvers = config.settings.approvers;
|
|
11049
|
+
const lines = ["Approver channels:\n"];
|
|
11050
|
+
for (const ch of APPROVER_CHANNELS) {
|
|
11051
|
+
const on = approvers[ch];
|
|
11052
|
+
lines.push(` ${on ? "[enabled] " : "[disabled]"} ${ch}`);
|
|
11053
|
+
}
|
|
11054
|
+
const enabledCount = APPROVER_CHANNELS.filter((ch) => approvers[ch]).length;
|
|
11055
|
+
if (enabledCount === 0) {
|
|
11056
|
+
lines.push("\nWARNING: all approver channels are disabled \u2014 node9 cannot prompt for approval.");
|
|
11057
|
+
}
|
|
11058
|
+
return lines.join("\n");
|
|
11059
|
+
}
|
|
11060
|
+
function handleApproverSet(args) {
|
|
11061
|
+
const channel = args.channel;
|
|
11062
|
+
const enabled = args.enabled;
|
|
11063
|
+
if (!channel || !APPROVER_CHANNELS.includes(channel)) {
|
|
11064
|
+
throw new Error(
|
|
11065
|
+
`Invalid channel: "${channel}". Must be one of: ${APPROVER_CHANNELS.join(", ")}.`
|
|
11066
|
+
);
|
|
11067
|
+
}
|
|
11068
|
+
if (typeof enabled !== "boolean") {
|
|
11069
|
+
throw new Error("enabled must be a boolean (true or false).");
|
|
11070
|
+
}
|
|
11071
|
+
const raw = readGlobalConfigRaw();
|
|
11072
|
+
const settings = raw.settings ?? {};
|
|
11073
|
+
const approvers = settings.approvers ?? {};
|
|
11074
|
+
approvers[channel] = enabled;
|
|
11075
|
+
settings.approvers = approvers;
|
|
11076
|
+
raw.settings = settings;
|
|
11077
|
+
writeGlobalConfigRaw(raw);
|
|
11078
|
+
const currentApprovers = getConfig().settings.approvers;
|
|
11079
|
+
const anyEnabled = APPROVER_CHANNELS.some(
|
|
11080
|
+
(ch) => ch === channel ? enabled : currentApprovers[ch]
|
|
11081
|
+
);
|
|
11082
|
+
const suffix = anyEnabled ? "" : "\nWARNING: all approver channels are now disabled \u2014 node9 cannot prompt for approval.";
|
|
11083
|
+
return `Approver channel "${channel}" ${enabled ? "enabled" : "disabled"} in ~/.node9/config.json.${suffix}`;
|
|
11084
|
+
}
|
|
10583
11085
|
function handleUndoList() {
|
|
10584
11086
|
const history = getSnapshotHistory();
|
|
10585
11087
|
if (history.length === 0) {
|
|
@@ -10653,6 +11155,12 @@ function runMcpServer() {
|
|
|
10653
11155
|
text = handleShieldList();
|
|
10654
11156
|
} else if (toolName === "node9_shield_enable") {
|
|
10655
11157
|
text = handleShieldEnable(toolArgs);
|
|
11158
|
+
} else if (toolName === "node9_shield_disable") {
|
|
11159
|
+
text = handleShieldDisable(toolArgs);
|
|
11160
|
+
} else if (toolName === "node9_approver_list") {
|
|
11161
|
+
text = handleApproverList();
|
|
11162
|
+
} else if (toolName === "node9_approver_set") {
|
|
11163
|
+
text = handleApproverSet(toolArgs);
|
|
10656
11164
|
} else if (toolName === "node9_undo_list") {
|
|
10657
11165
|
text = handleUndoList();
|
|
10658
11166
|
} else if (toolName === "node9_undo_revert") {
|