@node9/proxy 1.2.0 → 1.3.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 +138 -18
- package/dist/cli.js +312 -110
- package/dist/cli.mjs +308 -106
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -5033,12 +5033,12 @@ __export(tail_exports, {
|
|
|
5033
5033
|
startTail: () => startTail
|
|
5034
5034
|
});
|
|
5035
5035
|
import http2 from "http";
|
|
5036
|
-
import
|
|
5036
|
+
import chalk14 from "chalk";
|
|
5037
5037
|
import fs19 from "fs";
|
|
5038
5038
|
import os17 from "os";
|
|
5039
5039
|
import path20 from "path";
|
|
5040
|
-
import
|
|
5041
|
-
import { spawn as
|
|
5040
|
+
import readline3 from "readline";
|
|
5041
|
+
import { spawn as spawn9, execSync as execSync3 } from "child_process";
|
|
5042
5042
|
function getIcon(tool) {
|
|
5043
5043
|
const t = tool.toLowerCase();
|
|
5044
5044
|
for (const [k, v] of Object.entries(ICONS)) {
|
|
@@ -5052,27 +5052,27 @@ function formatBase(activity) {
|
|
|
5052
5052
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
5053
5053
|
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
|
|
5054
5054
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
5055
|
-
return `${
|
|
5055
|
+
return `${chalk14.gray(time)} ${icon} ${chalk14.white.bold(toolName)} ${chalk14.dim(argsPreview)}`;
|
|
5056
5056
|
}
|
|
5057
5057
|
function renderResult(activity, result) {
|
|
5058
5058
|
const base = formatBase(activity);
|
|
5059
5059
|
let status;
|
|
5060
5060
|
if (result.status === "allow") {
|
|
5061
|
-
status =
|
|
5061
|
+
status = chalk14.green("\u2713 ALLOW");
|
|
5062
5062
|
} else if (result.status === "dlp") {
|
|
5063
|
-
status =
|
|
5063
|
+
status = chalk14.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
5064
5064
|
} else {
|
|
5065
|
-
status =
|
|
5065
|
+
status = chalk14.red("\u2717 BLOCK");
|
|
5066
5066
|
}
|
|
5067
5067
|
if (process.stdout.isTTY) {
|
|
5068
|
-
|
|
5069
|
-
|
|
5068
|
+
readline3.clearLine(process.stdout, 0);
|
|
5069
|
+
readline3.cursorTo(process.stdout, 0);
|
|
5070
5070
|
}
|
|
5071
5071
|
console.log(`${base} ${status}`);
|
|
5072
5072
|
}
|
|
5073
5073
|
function renderPending(activity) {
|
|
5074
5074
|
if (!process.stdout.isTTY) return;
|
|
5075
|
-
process.stdout.write(`${formatBase(activity)} ${
|
|
5075
|
+
process.stdout.write(`${formatBase(activity)} ${chalk14.yellow("\u25CF \u2026")}\r`);
|
|
5076
5076
|
}
|
|
5077
5077
|
async function ensureDaemon() {
|
|
5078
5078
|
let pidPort = null;
|
|
@@ -5081,7 +5081,7 @@ async function ensureDaemon() {
|
|
|
5081
5081
|
const { port } = JSON.parse(fs19.readFileSync(PID_FILE, "utf-8"));
|
|
5082
5082
|
pidPort = port;
|
|
5083
5083
|
} catch {
|
|
5084
|
-
console.error(
|
|
5084
|
+
console.error(chalk14.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
5085
5085
|
}
|
|
5086
5086
|
}
|
|
5087
5087
|
const checkPort = pidPort ?? DAEMON_PORT;
|
|
@@ -5092,8 +5092,8 @@ async function ensureDaemon() {
|
|
|
5092
5092
|
if (res.ok) return checkPort;
|
|
5093
5093
|
} catch {
|
|
5094
5094
|
}
|
|
5095
|
-
console.log(
|
|
5096
|
-
const child =
|
|
5095
|
+
console.log(chalk14.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
5096
|
+
const child = spawn9(process.execPath, [process.argv[1], "daemon"], {
|
|
5097
5097
|
detached: true,
|
|
5098
5098
|
stdio: "ignore",
|
|
5099
5099
|
env: { ...process.env, NODE9_AUTO_STARTED: "1" }
|
|
@@ -5109,7 +5109,7 @@ async function ensureDaemon() {
|
|
|
5109
5109
|
} catch {
|
|
5110
5110
|
}
|
|
5111
5111
|
}
|
|
5112
|
-
console.error(
|
|
5112
|
+
console.error(chalk14.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
5113
5113
|
process.exit(1);
|
|
5114
5114
|
}
|
|
5115
5115
|
function postDecisionHttp(id, decision, csrfToken, port) {
|
|
@@ -5180,7 +5180,7 @@ async function startTail(options = {}) {
|
|
|
5180
5180
|
req2.end();
|
|
5181
5181
|
});
|
|
5182
5182
|
if (result.ok) {
|
|
5183
|
-
console.log(
|
|
5183
|
+
console.log(chalk14.green("\u2713 Flight Recorder buffer cleared."));
|
|
5184
5184
|
} else if (result.code === "ECONNREFUSED") {
|
|
5185
5185
|
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
5186
5186
|
} else if (result.code === "ETIMEDOUT") {
|
|
@@ -5198,7 +5198,7 @@ async function startTail(options = {}) {
|
|
|
5198
5198
|
let cardLineCount = 0;
|
|
5199
5199
|
let cancelActiveCard = null;
|
|
5200
5200
|
const canApprove = process.stdout.isTTY && process.stdin.isTTY;
|
|
5201
|
-
if (canApprove)
|
|
5201
|
+
if (canApprove) readline3.emitKeypressEvents(process.stdin);
|
|
5202
5202
|
function clearCard() {
|
|
5203
5203
|
if (cardLineCount > 0) {
|
|
5204
5204
|
process.stdout.write(RESTORE_CURSOR + ERASE_DOWN);
|
|
@@ -5251,8 +5251,8 @@ async function startTail(options = {}) {
|
|
|
5251
5251
|
} catch {
|
|
5252
5252
|
}
|
|
5253
5253
|
});
|
|
5254
|
-
const decisionLabel = decision === "allow" ?
|
|
5255
|
-
console.log(`${
|
|
5254
|
+
const decisionLabel = decision === "allow" ? chalk14.green("\u2713 ALLOWED (terminal)") : chalk14.red("\u2717 DENIED (terminal)");
|
|
5255
|
+
console.log(`${chalk14.cyan("\u25C6")} ${chalk14.bold(req2.toolName.padEnd(16))} ${decisionLabel}`);
|
|
5256
5256
|
approvalQueue.shift();
|
|
5257
5257
|
cardActive = false;
|
|
5258
5258
|
showNextCard();
|
|
@@ -5289,39 +5289,39 @@ async function startTail(options = {}) {
|
|
|
5289
5289
|
}
|
|
5290
5290
|
} catch {
|
|
5291
5291
|
}
|
|
5292
|
-
console.log(
|
|
5293
|
-
\u{1F6F0}\uFE0F Node9 tail `) +
|
|
5292
|
+
console.log(chalk14.cyan.bold(`
|
|
5293
|
+
\u{1F6F0}\uFE0F Node9 tail `) + chalk14.dim(`\u2192 ${dashboardUrl}`));
|
|
5294
5294
|
if (canApprove) {
|
|
5295
|
-
console.log(
|
|
5295
|
+
console.log(chalk14.dim("Interactive approvals enabled. [A] Allow [D] Deny"));
|
|
5296
5296
|
}
|
|
5297
5297
|
if (options.history) {
|
|
5298
|
-
console.log(
|
|
5298
|
+
console.log(chalk14.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
|
|
5299
5299
|
} else {
|
|
5300
5300
|
console.log(
|
|
5301
|
-
|
|
5301
|
+
chalk14.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
|
|
5302
5302
|
);
|
|
5303
5303
|
}
|
|
5304
5304
|
process.on("SIGINT", () => {
|
|
5305
5305
|
clearCard();
|
|
5306
5306
|
process.stdout.write(SHOW_CURSOR);
|
|
5307
5307
|
if (process.stdout.isTTY) {
|
|
5308
|
-
|
|
5309
|
-
|
|
5308
|
+
readline3.clearLine(process.stdout, 0);
|
|
5309
|
+
readline3.cursorTo(process.stdout, 0);
|
|
5310
5310
|
}
|
|
5311
|
-
console.log(
|
|
5311
|
+
console.log(chalk14.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
5312
5312
|
process.exit(0);
|
|
5313
5313
|
});
|
|
5314
5314
|
const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
|
|
5315
5315
|
const req = http2.get(sseUrl, (res) => {
|
|
5316
5316
|
if (res.statusCode !== 200) {
|
|
5317
|
-
console.error(
|
|
5317
|
+
console.error(chalk14.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
5318
5318
|
process.exit(1);
|
|
5319
5319
|
}
|
|
5320
5320
|
let currentEvent = "";
|
|
5321
5321
|
let currentData = "";
|
|
5322
5322
|
res.on("error", () => {
|
|
5323
5323
|
});
|
|
5324
|
-
const rl =
|
|
5324
|
+
const rl = readline3.createInterface({ input: res, crlfDelay: Infinity });
|
|
5325
5325
|
rl.on("error", () => {
|
|
5326
5326
|
});
|
|
5327
5327
|
rl.on("line", (line) => {
|
|
@@ -5341,10 +5341,10 @@ async function startTail(options = {}) {
|
|
|
5341
5341
|
clearCard();
|
|
5342
5342
|
process.stdout.write(SHOW_CURSOR);
|
|
5343
5343
|
if (process.stdout.isTTY) {
|
|
5344
|
-
|
|
5345
|
-
|
|
5344
|
+
readline3.clearLine(process.stdout, 0);
|
|
5345
|
+
readline3.cursorTo(process.stdout, 0);
|
|
5346
5346
|
}
|
|
5347
|
-
console.log(
|
|
5347
|
+
console.log(chalk14.red("\n\u274C Daemon disconnected."));
|
|
5348
5348
|
process.exit(1);
|
|
5349
5349
|
});
|
|
5350
5350
|
});
|
|
@@ -5424,7 +5424,7 @@ async function startTail(options = {}) {
|
|
|
5424
5424
|
}
|
|
5425
5425
|
req.on("error", (err) => {
|
|
5426
5426
|
const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
|
|
5427
|
-
console.error(
|
|
5427
|
+
console.error(chalk14.red(`
|
|
5428
5428
|
\u274C ${msg}`));
|
|
5429
5429
|
process.exit(1);
|
|
5430
5430
|
});
|
|
@@ -5841,7 +5841,7 @@ async function setupCursor() {
|
|
|
5841
5841
|
|
|
5842
5842
|
// src/cli.ts
|
|
5843
5843
|
init_daemon2();
|
|
5844
|
-
import
|
|
5844
|
+
import chalk15 from "chalk";
|
|
5845
5845
|
import fs20 from "fs";
|
|
5846
5846
|
import path21 from "path";
|
|
5847
5847
|
import os18 from "os";
|
|
@@ -7373,6 +7373,207 @@ function registerWatchCommand(program2) {
|
|
|
7373
7373
|
});
|
|
7374
7374
|
}
|
|
7375
7375
|
|
|
7376
|
+
// src/mcp-gateway/index.ts
|
|
7377
|
+
init_orchestrator();
|
|
7378
|
+
import readline2 from "readline";
|
|
7379
|
+
import chalk13 from "chalk";
|
|
7380
|
+
import { spawn as spawn8 } from "child_process";
|
|
7381
|
+
import { execa as execa2 } from "execa";
|
|
7382
|
+
function sanitize4(value) {
|
|
7383
|
+
return value.replace(/[\x00-\x1F\x7F]/g, "");
|
|
7384
|
+
}
|
|
7385
|
+
var RPC_INVALID_REQUEST = -32600;
|
|
7386
|
+
var RPC_SERVER_ERROR = -32e3;
|
|
7387
|
+
function isValidId(id) {
|
|
7388
|
+
return id === null || typeof id === "string" || typeof id === "number";
|
|
7389
|
+
}
|
|
7390
|
+
function extractMcpServer(toolName) {
|
|
7391
|
+
const match = toolName.match(/^mcp__([^_](?:[^_]|_(?!_))*?)__/i);
|
|
7392
|
+
return match?.[1];
|
|
7393
|
+
}
|
|
7394
|
+
function tokenize2(cmd) {
|
|
7395
|
+
const tokens = [];
|
|
7396
|
+
let current = "";
|
|
7397
|
+
let inDouble = false;
|
|
7398
|
+
let i = 0;
|
|
7399
|
+
while (i < cmd.length) {
|
|
7400
|
+
const ch = cmd[i];
|
|
7401
|
+
if (inDouble) {
|
|
7402
|
+
if (ch === '"') {
|
|
7403
|
+
inDouble = false;
|
|
7404
|
+
} else if (ch === "\\" && i + 1 < cmd.length) {
|
|
7405
|
+
current += cmd[++i];
|
|
7406
|
+
} else {
|
|
7407
|
+
current += ch;
|
|
7408
|
+
}
|
|
7409
|
+
} else {
|
|
7410
|
+
if (ch === '"') {
|
|
7411
|
+
inDouble = true;
|
|
7412
|
+
} else if (ch === " " || ch === " ") {
|
|
7413
|
+
if (current) {
|
|
7414
|
+
tokens.push(current);
|
|
7415
|
+
current = "";
|
|
7416
|
+
}
|
|
7417
|
+
} else if (ch === "\\" && i + 1 < cmd.length) {
|
|
7418
|
+
current += cmd[++i];
|
|
7419
|
+
} else {
|
|
7420
|
+
current += ch;
|
|
7421
|
+
}
|
|
7422
|
+
}
|
|
7423
|
+
i++;
|
|
7424
|
+
}
|
|
7425
|
+
if (current) tokens.push(current);
|
|
7426
|
+
return tokens;
|
|
7427
|
+
}
|
|
7428
|
+
async function runMcpGateway(upstreamCommand) {
|
|
7429
|
+
const commandParts = tokenize2(upstreamCommand);
|
|
7430
|
+
const cmd = commandParts[0];
|
|
7431
|
+
const cmdArgs = commandParts.slice(1);
|
|
7432
|
+
let executable = cmd;
|
|
7433
|
+
try {
|
|
7434
|
+
const { stdout } = await execa2("which", [cmd]);
|
|
7435
|
+
if (stdout) executable = stdout.trim();
|
|
7436
|
+
} catch {
|
|
7437
|
+
}
|
|
7438
|
+
console.error(chalk13.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
|
|
7439
|
+
const UPSTREAM_INJECTOR_VARS = /* @__PURE__ */ new Set([
|
|
7440
|
+
"NODE_OPTIONS",
|
|
7441
|
+
"NODE_PATH",
|
|
7442
|
+
"LD_PRELOAD",
|
|
7443
|
+
"LD_LIBRARY_PATH",
|
|
7444
|
+
"DYLD_INSERT_LIBRARIES",
|
|
7445
|
+
"PYTHONPATH",
|
|
7446
|
+
"PYTHONSTARTUP",
|
|
7447
|
+
"PERL5LIB",
|
|
7448
|
+
"PERL5OPT",
|
|
7449
|
+
"RUBYLIB",
|
|
7450
|
+
"RUBYOPT",
|
|
7451
|
+
"JAVA_TOOL_OPTIONS",
|
|
7452
|
+
"JDK_JAVA_OPTIONS"
|
|
7453
|
+
]);
|
|
7454
|
+
const safeEnv = Object.fromEntries(
|
|
7455
|
+
Object.entries(process.env).filter(([k]) => !UPSTREAM_INJECTOR_VARS.has(k))
|
|
7456
|
+
);
|
|
7457
|
+
const child = spawn8(executable, cmdArgs, {
|
|
7458
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
7459
|
+
// control stdin/stdout; inherit stderr
|
|
7460
|
+
shell: false,
|
|
7461
|
+
env: { ...safeEnv, FORCE_COLOR: "1" }
|
|
7462
|
+
});
|
|
7463
|
+
let authPending = false;
|
|
7464
|
+
let deferredExitCode = null;
|
|
7465
|
+
let deferredStdinEnd = false;
|
|
7466
|
+
const agentIn = readline2.createInterface({ input: process.stdin, terminal: false });
|
|
7467
|
+
agentIn.on("line", async (line) => {
|
|
7468
|
+
let message;
|
|
7469
|
+
try {
|
|
7470
|
+
const parsed = JSON.parse(line);
|
|
7471
|
+
if ("id" in parsed && !isValidId(parsed.id)) {
|
|
7472
|
+
const errorResponse = {
|
|
7473
|
+
jsonrpc: "2.0",
|
|
7474
|
+
id: null,
|
|
7475
|
+
error: {
|
|
7476
|
+
code: RPC_INVALID_REQUEST,
|
|
7477
|
+
message: "Invalid Request: id must be string, number, or null"
|
|
7478
|
+
}
|
|
7479
|
+
};
|
|
7480
|
+
process.stdout.write(JSON.stringify(errorResponse) + "\n");
|
|
7481
|
+
return;
|
|
7482
|
+
}
|
|
7483
|
+
message = { ...parsed, id: parsed.id };
|
|
7484
|
+
} catch {
|
|
7485
|
+
child.stdin.write(line + "\n");
|
|
7486
|
+
return;
|
|
7487
|
+
}
|
|
7488
|
+
if (message.method === "tools/call" || message.method === "call_tool" || message.method === "use_tool") {
|
|
7489
|
+
agentIn.pause();
|
|
7490
|
+
authPending = true;
|
|
7491
|
+
try {
|
|
7492
|
+
const toolName = sanitize4(
|
|
7493
|
+
String(message.params?.name ?? message.params?.tool_name ?? "unknown")
|
|
7494
|
+
);
|
|
7495
|
+
const toolArgs = message.params?.arguments ?? message.params?.tool_input ?? {};
|
|
7496
|
+
const mcpServer = extractMcpServer(toolName);
|
|
7497
|
+
const result = await authorizeHeadless(toolName, toolArgs, {
|
|
7498
|
+
agent: "MCP-Gateway",
|
|
7499
|
+
mcpServer
|
|
7500
|
+
});
|
|
7501
|
+
if (!result.approved) {
|
|
7502
|
+
console.error(chalk13.red(`
|
|
7503
|
+
\u{1F6D1} Node9 MCP Gateway: Action Blocked`));
|
|
7504
|
+
console.error(chalk13.gray(` Tool: ${toolName}`));
|
|
7505
|
+
console.error(chalk13.gray(` Reason: ${result.reason ?? "Security Policy"}
|
|
7506
|
+
`));
|
|
7507
|
+
const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
|
|
7508
|
+
const isHumanDecision = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
|
|
7509
|
+
const aiInstruction = buildNegotiationMessage(
|
|
7510
|
+
blockedByLabel,
|
|
7511
|
+
isHumanDecision,
|
|
7512
|
+
result.reason
|
|
7513
|
+
);
|
|
7514
|
+
const errorResponse = {
|
|
7515
|
+
jsonrpc: "2.0",
|
|
7516
|
+
id: message.id ?? null,
|
|
7517
|
+
error: {
|
|
7518
|
+
code: RPC_SERVER_ERROR,
|
|
7519
|
+
message: aiInstruction,
|
|
7520
|
+
data: { reason: result.reason, blockedBy: result.blockedByLabel }
|
|
7521
|
+
}
|
|
7522
|
+
};
|
|
7523
|
+
process.stdout.write(JSON.stringify(errorResponse) + "\n");
|
|
7524
|
+
return;
|
|
7525
|
+
}
|
|
7526
|
+
child.stdin.write(line + "\n");
|
|
7527
|
+
} catch {
|
|
7528
|
+
const errorResponse = {
|
|
7529
|
+
jsonrpc: "2.0",
|
|
7530
|
+
id: message.id ?? null,
|
|
7531
|
+
error: {
|
|
7532
|
+
code: -32e3,
|
|
7533
|
+
message: "Node9: Security engine encountered an error. Action blocked for safety."
|
|
7534
|
+
}
|
|
7535
|
+
};
|
|
7536
|
+
process.stdout.write(JSON.stringify(errorResponse) + "\n");
|
|
7537
|
+
return;
|
|
7538
|
+
} finally {
|
|
7539
|
+
authPending = false;
|
|
7540
|
+
agentIn.resume();
|
|
7541
|
+
if (deferredStdinEnd) child.stdin.end();
|
|
7542
|
+
if (deferredExitCode !== null) process.exit(deferredExitCode);
|
|
7543
|
+
}
|
|
7544
|
+
return;
|
|
7545
|
+
}
|
|
7546
|
+
child.stdin.write(line + "\n");
|
|
7547
|
+
});
|
|
7548
|
+
child.stdout.pipe(process.stdout);
|
|
7549
|
+
process.stdin.on("close", () => {
|
|
7550
|
+
if (authPending) {
|
|
7551
|
+
deferredStdinEnd = true;
|
|
7552
|
+
} else {
|
|
7553
|
+
child.stdin.end();
|
|
7554
|
+
}
|
|
7555
|
+
});
|
|
7556
|
+
child.on("exit", (code) => {
|
|
7557
|
+
if (authPending) {
|
|
7558
|
+
deferredExitCode = code ?? 0;
|
|
7559
|
+
} else {
|
|
7560
|
+
process.exit(code ?? 0);
|
|
7561
|
+
}
|
|
7562
|
+
});
|
|
7563
|
+
}
|
|
7564
|
+
|
|
7565
|
+
// src/cli/commands/mcp-gateway.ts
|
|
7566
|
+
function registerMcpGatewayCommand(program2) {
|
|
7567
|
+
program2.command("mcp-gateway").description(
|
|
7568
|
+
"Run Node9 as an MCP gateway \u2014 intercepts and authorizes tool calls before forwarding to the upstream MCP server"
|
|
7569
|
+
).requiredOption(
|
|
7570
|
+
"--upstream <command>",
|
|
7571
|
+
'The upstream MCP server command to wrap (e.g. "npx -y @modelcontextprotocol/server-filesystem /workspace")'
|
|
7572
|
+
).action(async (options) => {
|
|
7573
|
+
await runMcpGateway(options.upstream);
|
|
7574
|
+
});
|
|
7575
|
+
}
|
|
7576
|
+
|
|
7376
7577
|
// src/cli.ts
|
|
7377
7578
|
var { version } = JSON.parse(
|
|
7378
7579
|
fs20.readFileSync(path21.join(__dirname, "../package.json"), "utf-8")
|
|
@@ -7426,31 +7627,31 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
7426
7627
|
fs20.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
7427
7628
|
}
|
|
7428
7629
|
if (options.profile && profileName !== "default") {
|
|
7429
|
-
console.log(
|
|
7430
|
-
console.log(
|
|
7630
|
+
console.log(chalk15.green(`\u2705 Profile "${profileName}" saved`));
|
|
7631
|
+
console.log(chalk15.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
7431
7632
|
} else if (options.local) {
|
|
7432
|
-
console.log(
|
|
7433
|
-
console.log(
|
|
7633
|
+
console.log(chalk15.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
7634
|
+
console.log(chalk15.gray(` All decisions stay on this machine.`));
|
|
7434
7635
|
} else {
|
|
7435
|
-
console.log(
|
|
7436
|
-
console.log(
|
|
7636
|
+
console.log(chalk15.green(`\u2705 Logged in \u2014 agent mode`));
|
|
7637
|
+
console.log(chalk15.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
7437
7638
|
}
|
|
7438
7639
|
});
|
|
7439
7640
|
program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to protect: claude | gemini | cursor").action(async (target) => {
|
|
7440
7641
|
if (target === "gemini") return await setupGemini();
|
|
7441
7642
|
if (target === "claude") return await setupClaude();
|
|
7442
7643
|
if (target === "cursor") return await setupCursor();
|
|
7443
|
-
console.error(
|
|
7644
|
+
console.error(chalk15.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
7444
7645
|
process.exit(1);
|
|
7445
7646
|
});
|
|
7446
7647
|
program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor").argument("[target]", "The agent to protect: claude | gemini | cursor").action(async (target) => {
|
|
7447
7648
|
if (!target) {
|
|
7448
|
-
console.log(
|
|
7449
|
-
console.log(" Usage: " +
|
|
7649
|
+
console.log(chalk15.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
7650
|
+
console.log(" Usage: " + chalk15.white("node9 setup <target>") + "\n");
|
|
7450
7651
|
console.log(" Targets:");
|
|
7451
|
-
console.log(" " +
|
|
7452
|
-
console.log(" " +
|
|
7453
|
-
console.log(" " +
|
|
7652
|
+
console.log(" " + chalk15.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
7653
|
+
console.log(" " + chalk15.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
7654
|
+
console.log(" " + chalk15.green("cursor") + " \u2014 Cursor (hook mode)");
|
|
7454
7655
|
console.log("");
|
|
7455
7656
|
return;
|
|
7456
7657
|
}
|
|
@@ -7458,7 +7659,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
7458
7659
|
if (t === "gemini") return await setupGemini();
|
|
7459
7660
|
if (t === "claude") return await setupClaude();
|
|
7460
7661
|
if (t === "cursor") return await setupCursor();
|
|
7461
|
-
console.error(
|
|
7662
|
+
console.error(chalk15.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
7462
7663
|
process.exit(1);
|
|
7463
7664
|
});
|
|
7464
7665
|
program.command("removefrom").description("Remove Node9 hooks from an AI agent configuration").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to remove from: claude | gemini | cursor").action((target) => {
|
|
@@ -7467,30 +7668,30 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
|
|
|
7467
7668
|
else if (target === "gemini") fn = teardownGemini;
|
|
7468
7669
|
else if (target === "cursor") fn = teardownCursor;
|
|
7469
7670
|
else {
|
|
7470
|
-
console.error(
|
|
7671
|
+
console.error(chalk15.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
7471
7672
|
process.exit(1);
|
|
7472
7673
|
}
|
|
7473
|
-
console.log(
|
|
7674
|
+
console.log(chalk15.cyan(`
|
|
7474
7675
|
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
7475
7676
|
`));
|
|
7476
7677
|
try {
|
|
7477
7678
|
fn();
|
|
7478
7679
|
} catch (err) {
|
|
7479
|
-
console.error(
|
|
7680
|
+
console.error(chalk15.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
7480
7681
|
process.exit(1);
|
|
7481
7682
|
}
|
|
7482
|
-
console.log(
|
|
7683
|
+
console.log(chalk15.gray("\n Restart the agent for changes to take effect."));
|
|
7483
7684
|
});
|
|
7484
7685
|
program.command("uninstall").description("Remove all Node9 hooks and optionally delete config files").option("--purge", "Also delete ~/.node9/ directory (config, audit log, credentials)").action(async (options) => {
|
|
7485
|
-
console.log(
|
|
7486
|
-
console.log(
|
|
7686
|
+
console.log(chalk15.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
7687
|
+
console.log(chalk15.bold("Stopping daemon..."));
|
|
7487
7688
|
try {
|
|
7488
7689
|
stopDaemon();
|
|
7489
|
-
console.log(
|
|
7690
|
+
console.log(chalk15.green(" \u2705 Daemon stopped"));
|
|
7490
7691
|
} catch {
|
|
7491
|
-
console.log(
|
|
7692
|
+
console.log(chalk15.blue(" \u2139\uFE0F Daemon was not running"));
|
|
7492
7693
|
}
|
|
7493
|
-
console.log(
|
|
7694
|
+
console.log(chalk15.bold("\nRemoving hooks..."));
|
|
7494
7695
|
let teardownFailed = false;
|
|
7495
7696
|
for (const [label, fn] of [
|
|
7496
7697
|
["Claude", teardownClaude],
|
|
@@ -7502,7 +7703,7 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
7502
7703
|
} catch (err) {
|
|
7503
7704
|
teardownFailed = true;
|
|
7504
7705
|
console.error(
|
|
7505
|
-
|
|
7706
|
+
chalk15.red(
|
|
7506
7707
|
` \u26A0\uFE0F Failed to remove ${label} hooks: ${err instanceof Error ? err.message : String(err)}`
|
|
7507
7708
|
)
|
|
7508
7709
|
);
|
|
@@ -7519,28 +7720,28 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
7519
7720
|
fs20.rmSync(node9Dir, { recursive: true });
|
|
7520
7721
|
if (fs20.existsSync(node9Dir)) {
|
|
7521
7722
|
console.error(
|
|
7522
|
-
|
|
7723
|
+
chalk15.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
7523
7724
|
);
|
|
7524
7725
|
} else {
|
|
7525
|
-
console.log(
|
|
7726
|
+
console.log(chalk15.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
7526
7727
|
}
|
|
7527
7728
|
} else {
|
|
7528
|
-
console.log(
|
|
7729
|
+
console.log(chalk15.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
7529
7730
|
}
|
|
7530
7731
|
} else {
|
|
7531
|
-
console.log(
|
|
7732
|
+
console.log(chalk15.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
7532
7733
|
}
|
|
7533
7734
|
} else {
|
|
7534
7735
|
console.log(
|
|
7535
|
-
|
|
7736
|
+
chalk15.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
7536
7737
|
);
|
|
7537
7738
|
}
|
|
7538
7739
|
if (teardownFailed) {
|
|
7539
|
-
console.error(
|
|
7740
|
+
console.error(chalk15.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
7540
7741
|
process.exit(1);
|
|
7541
7742
|
}
|
|
7542
|
-
console.log(
|
|
7543
|
-
console.log(
|
|
7743
|
+
console.log(chalk15.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
|
|
7744
|
+
console.log(chalk15.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
|
|
7544
7745
|
});
|
|
7545
7746
|
registerDoctorCommand(program, version);
|
|
7546
7747
|
program.command("explain").description(
|
|
@@ -7553,7 +7754,7 @@ program.command("explain").description(
|
|
|
7553
7754
|
try {
|
|
7554
7755
|
args = JSON.parse(trimmed);
|
|
7555
7756
|
} catch {
|
|
7556
|
-
console.error(
|
|
7757
|
+
console.error(chalk15.red(`
|
|
7557
7758
|
\u274C Invalid JSON: ${trimmed}
|
|
7558
7759
|
`));
|
|
7559
7760
|
process.exit(1);
|
|
@@ -7564,54 +7765,54 @@ program.command("explain").description(
|
|
|
7564
7765
|
}
|
|
7565
7766
|
const result = await explainPolicy(tool, args);
|
|
7566
7767
|
console.log("");
|
|
7567
|
-
console.log(
|
|
7768
|
+
console.log(chalk15.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
7568
7769
|
console.log("");
|
|
7569
|
-
console.log(` ${
|
|
7770
|
+
console.log(` ${chalk15.bold("Tool:")} ${chalk15.white(result.tool)}`);
|
|
7570
7771
|
if (argsRaw) {
|
|
7571
7772
|
const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
7572
|
-
console.log(` ${
|
|
7773
|
+
console.log(` ${chalk15.bold("Input:")} ${chalk15.gray(preview)}`);
|
|
7573
7774
|
}
|
|
7574
7775
|
console.log("");
|
|
7575
|
-
console.log(
|
|
7776
|
+
console.log(chalk15.bold("Config Sources (Waterfall):"));
|
|
7576
7777
|
for (const tier of result.waterfall) {
|
|
7577
|
-
const num =
|
|
7778
|
+
const num = chalk15.gray(` ${tier.tier}.`);
|
|
7578
7779
|
const label = tier.label.padEnd(16);
|
|
7579
7780
|
let statusStr;
|
|
7580
7781
|
if (tier.tier === 1) {
|
|
7581
|
-
statusStr =
|
|
7782
|
+
statusStr = chalk15.gray(tier.note ?? "");
|
|
7582
7783
|
} else if (tier.status === "active") {
|
|
7583
|
-
const loc = tier.path ?
|
|
7584
|
-
const note = tier.note ?
|
|
7585
|
-
statusStr =
|
|
7784
|
+
const loc = tier.path ? chalk15.gray(tier.path) : "";
|
|
7785
|
+
const note = tier.note ? chalk15.gray(`(${tier.note})`) : "";
|
|
7786
|
+
statusStr = chalk15.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
|
|
7586
7787
|
} else {
|
|
7587
|
-
statusStr =
|
|
7788
|
+
statusStr = chalk15.gray("\u25CB " + (tier.note ?? "not found"));
|
|
7588
7789
|
}
|
|
7589
|
-
console.log(`${num} ${
|
|
7790
|
+
console.log(`${num} ${chalk15.white(label)} ${statusStr}`);
|
|
7590
7791
|
}
|
|
7591
7792
|
console.log("");
|
|
7592
|
-
console.log(
|
|
7793
|
+
console.log(chalk15.bold("Policy Evaluation:"));
|
|
7593
7794
|
for (const step of result.steps) {
|
|
7594
7795
|
const isFinal = step.isFinal;
|
|
7595
7796
|
let icon;
|
|
7596
|
-
if (step.outcome === "allow") icon =
|
|
7597
|
-
else if (step.outcome === "review") icon =
|
|
7598
|
-
else if (step.outcome === "skip") icon =
|
|
7599
|
-
else icon =
|
|
7797
|
+
if (step.outcome === "allow") icon = chalk15.green(" \u2705");
|
|
7798
|
+
else if (step.outcome === "review") icon = chalk15.red(" \u{1F534}");
|
|
7799
|
+
else if (step.outcome === "skip") icon = chalk15.gray(" \u2500 ");
|
|
7800
|
+
else icon = chalk15.gray(" \u25CB ");
|
|
7600
7801
|
const name = step.name.padEnd(18);
|
|
7601
|
-
const nameStr = isFinal ?
|
|
7602
|
-
const detail = isFinal ?
|
|
7603
|
-
const arrow = isFinal ?
|
|
7802
|
+
const nameStr = isFinal ? chalk15.white.bold(name) : chalk15.white(name);
|
|
7803
|
+
const detail = isFinal ? chalk15.white(step.detail) : chalk15.gray(step.detail);
|
|
7804
|
+
const arrow = isFinal ? chalk15.yellow(" \u2190 STOP") : "";
|
|
7604
7805
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
7605
7806
|
}
|
|
7606
7807
|
console.log("");
|
|
7607
7808
|
if (result.decision === "allow") {
|
|
7608
|
-
console.log(
|
|
7809
|
+
console.log(chalk15.green.bold(" Decision: \u2705 ALLOW") + chalk15.gray(" \u2014 no approval needed"));
|
|
7609
7810
|
} else {
|
|
7610
7811
|
console.log(
|
|
7611
|
-
|
|
7812
|
+
chalk15.red.bold(" Decision: \u{1F534} REVIEW") + chalk15.gray(" \u2014 human approval required")
|
|
7612
7813
|
);
|
|
7613
7814
|
if (result.blockedByLabel) {
|
|
7614
|
-
console.log(
|
|
7815
|
+
console.log(chalk15.gray(` Reason: ${result.blockedByLabel}`));
|
|
7615
7816
|
}
|
|
7616
7817
|
}
|
|
7617
7818
|
console.log("");
|
|
@@ -7619,8 +7820,8 @@ program.command("explain").description(
|
|
|
7619
7820
|
program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").action((options) => {
|
|
7620
7821
|
const configPath = path21.join(os18.homedir(), ".node9", "config.json");
|
|
7621
7822
|
if (fs20.existsSync(configPath) && !options.force) {
|
|
7622
|
-
console.log(
|
|
7623
|
-
console.log(
|
|
7823
|
+
console.log(chalk15.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
7824
|
+
console.log(chalk15.gray(` Run with --force to overwrite.`));
|
|
7624
7825
|
return;
|
|
7625
7826
|
}
|
|
7626
7827
|
const requestedMode = options.mode.toLowerCase();
|
|
@@ -7635,10 +7836,10 @@ program.command("init").description("Create ~/.node9/config.json with default po
|
|
|
7635
7836
|
const dir = path21.dirname(configPath);
|
|
7636
7837
|
if (!fs20.existsSync(dir)) fs20.mkdirSync(dir, { recursive: true });
|
|
7637
7838
|
fs20.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
7638
|
-
console.log(
|
|
7639
|
-
console.log(
|
|
7839
|
+
console.log(chalk15.green(`\u2705 Global config created: ${configPath}`));
|
|
7840
|
+
console.log(chalk15.cyan(` Mode set to: ${safeMode}`));
|
|
7640
7841
|
console.log(
|
|
7641
|
-
|
|
7842
|
+
chalk15.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
|
|
7642
7843
|
);
|
|
7643
7844
|
});
|
|
7644
7845
|
registerAuditCommand(program);
|
|
@@ -7649,18 +7850,19 @@ program.command("tail").description("Stream live agent activity to the terminal"
|
|
|
7649
7850
|
try {
|
|
7650
7851
|
await startTail2(options);
|
|
7651
7852
|
} catch (err) {
|
|
7652
|
-
console.error(
|
|
7853
|
+
console.error(chalk15.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
|
|
7653
7854
|
process.exit(1);
|
|
7654
7855
|
}
|
|
7655
7856
|
});
|
|
7656
7857
|
registerWatchCommand(program);
|
|
7858
|
+
registerMcpGatewayCommand(program);
|
|
7657
7859
|
registerCheckCommand(program);
|
|
7658
7860
|
registerLogCommand(program);
|
|
7659
7861
|
program.command("pause").description("Temporarily disable Node9 protection for a set duration").option("-d, --duration <duration>", "How long to pause (e.g. 15m, 1h, 30s)", "15m").action((options) => {
|
|
7660
7862
|
const ms = parseDuration(options.duration);
|
|
7661
7863
|
if (ms === null) {
|
|
7662
7864
|
console.error(
|
|
7663
|
-
|
|
7865
|
+
chalk15.red(`
|
|
7664
7866
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
7665
7867
|
`)
|
|
7666
7868
|
);
|
|
@@ -7668,20 +7870,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
7668
7870
|
}
|
|
7669
7871
|
pauseNode9(ms, options.duration);
|
|
7670
7872
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
7671
|
-
console.log(
|
|
7873
|
+
console.log(chalk15.yellow(`
|
|
7672
7874
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
7673
|
-
console.log(
|
|
7674
|
-
console.log(
|
|
7875
|
+
console.log(chalk15.gray(` All tool calls will be allowed without review.`));
|
|
7876
|
+
console.log(chalk15.gray(` Run "node9 resume" to re-enable early.
|
|
7675
7877
|
`));
|
|
7676
7878
|
});
|
|
7677
7879
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
7678
7880
|
const { paused } = checkPause();
|
|
7679
7881
|
if (!paused) {
|
|
7680
|
-
console.log(
|
|
7882
|
+
console.log(chalk15.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
7681
7883
|
return;
|
|
7682
7884
|
}
|
|
7683
7885
|
resumeNode9();
|
|
7684
|
-
console.log(
|
|
7886
|
+
console.log(chalk15.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
7685
7887
|
});
|
|
7686
7888
|
var HOOK_BASED_AGENTS = {
|
|
7687
7889
|
claude: "claude",
|
|
@@ -7694,15 +7896,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
7694
7896
|
if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
|
|
7695
7897
|
const target = HOOK_BASED_AGENTS[firstArg2];
|
|
7696
7898
|
console.error(
|
|
7697
|
-
|
|
7899
|
+
chalk15.yellow(`
|
|
7698
7900
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
7699
7901
|
);
|
|
7700
|
-
console.error(
|
|
7902
|
+
console.error(chalk15.white(`
|
|
7701
7903
|
"${target}" uses its own hook system. Use:`));
|
|
7702
7904
|
console.error(
|
|
7703
|
-
|
|
7905
|
+
chalk15.green(` node9 addto ${target} `) + chalk15.gray("# one-time setup")
|
|
7704
7906
|
);
|
|
7705
|
-
console.error(
|
|
7907
|
+
console.error(chalk15.green(` ${target} `) + chalk15.gray("# run normally"));
|
|
7706
7908
|
process.exit(1);
|
|
7707
7909
|
}
|
|
7708
7910
|
const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
|
|
@@ -7719,7 +7921,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
7719
7921
|
}
|
|
7720
7922
|
);
|
|
7721
7923
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
7722
|
-
console.error(
|
|
7924
|
+
console.error(chalk15.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
7723
7925
|
const daemonReady = await autoStartDaemonAndWait();
|
|
7724
7926
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
7725
7927
|
}
|
|
@@ -7732,12 +7934,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
7732
7934
|
}
|
|
7733
7935
|
if (!result.approved) {
|
|
7734
7936
|
console.error(
|
|
7735
|
-
|
|
7937
|
+
chalk15.red(`
|
|
7736
7938
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
7737
7939
|
);
|
|
7738
7940
|
process.exit(1);
|
|
7739
7941
|
}
|
|
7740
|
-
console.error(
|
|
7942
|
+
console.error(chalk15.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
7741
7943
|
await runProxy(fullCommand);
|
|
7742
7944
|
} else {
|
|
7743
7945
|
program.help();
|