@node9/proxy 1.0.0 → 1.0.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/dist/cli.js +200 -223
- package/dist/cli.mjs +200 -223
- package/dist/index.js +109 -132
- package/dist/index.mjs +109 -132
- package/package.json +30 -1
package/dist/cli.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/core.ts
|
|
7
|
-
import
|
|
7
|
+
import chalk2 from "chalk";
|
|
8
8
|
import { confirm } from "@inquirer/prompts";
|
|
9
9
|
import fs from "fs";
|
|
10
10
|
import path from "path";
|
|
@@ -14,19 +14,69 @@ import { parse } from "sh-syntax";
|
|
|
14
14
|
|
|
15
15
|
// src/ui/native.ts
|
|
16
16
|
import { spawn } from "child_process";
|
|
17
|
+
import chalk from "chalk";
|
|
17
18
|
var isTestEnv = () => {
|
|
18
19
|
return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
|
|
19
20
|
};
|
|
21
|
+
function smartTruncate(str, maxLen = 500) {
|
|
22
|
+
if (str.length <= maxLen) return str;
|
|
23
|
+
const edge = Math.floor(maxLen / 2) - 3;
|
|
24
|
+
return `${str.slice(0, edge)} ... ${str.slice(-edge)}`;
|
|
25
|
+
}
|
|
26
|
+
function formatArgs(args) {
|
|
27
|
+
if (args === null || args === void 0) return "(none)";
|
|
28
|
+
let parsed = args;
|
|
29
|
+
if (typeof args === "string") {
|
|
30
|
+
const trimmed = args.trim();
|
|
31
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
32
|
+
try {
|
|
33
|
+
parsed = JSON.parse(trimmed);
|
|
34
|
+
} catch {
|
|
35
|
+
parsed = args;
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
return smartTruncate(args, 600);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
42
|
+
const obj = parsed;
|
|
43
|
+
const codeKeys = [
|
|
44
|
+
"command",
|
|
45
|
+
"cmd",
|
|
46
|
+
"shell_command",
|
|
47
|
+
"bash_command",
|
|
48
|
+
"script",
|
|
49
|
+
"code",
|
|
50
|
+
"input",
|
|
51
|
+
"sql",
|
|
52
|
+
"query",
|
|
53
|
+
"arguments",
|
|
54
|
+
"args",
|
|
55
|
+
"param",
|
|
56
|
+
"params",
|
|
57
|
+
"text"
|
|
58
|
+
];
|
|
59
|
+
const foundKey = Object.keys(obj).find((k) => codeKeys.includes(k.toLowerCase()));
|
|
60
|
+
if (foundKey) {
|
|
61
|
+
const val = obj[foundKey];
|
|
62
|
+
const str = typeof val === "string" ? val : JSON.stringify(val);
|
|
63
|
+
return `[${foundKey.toUpperCase()}]:
|
|
64
|
+
${smartTruncate(str, 500)}`;
|
|
65
|
+
}
|
|
66
|
+
return Object.entries(obj).slice(0, 5).map(
|
|
67
|
+
([k, v]) => ` ${k}: ${smartTruncate(typeof v === "string" ? v : JSON.stringify(v), 300)}`
|
|
68
|
+
).join("\n");
|
|
69
|
+
}
|
|
70
|
+
return smartTruncate(JSON.stringify(parsed), 200);
|
|
71
|
+
}
|
|
20
72
|
function sendDesktopNotification(title, body) {
|
|
21
73
|
if (isTestEnv()) return;
|
|
22
74
|
try {
|
|
23
|
-
const safeTitle = title.replace(/"/g, '\\"');
|
|
24
|
-
const safeBody = body.replace(/"/g, '\\"');
|
|
25
75
|
if (process.platform === "darwin") {
|
|
26
|
-
const script = `display notification "${
|
|
76
|
+
const script = `display notification "${body.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"`;
|
|
27
77
|
spawn("osascript", ["-e", script], { detached: true, stdio: "ignore" }).unref();
|
|
28
78
|
} else if (process.platform === "linux") {
|
|
29
|
-
spawn("notify-send", [
|
|
79
|
+
spawn("notify-send", [title, body, "--icon=dialog-warning"], {
|
|
30
80
|
detached: true,
|
|
31
81
|
stdio: "ignore"
|
|
32
82
|
}).unref();
|
|
@@ -34,69 +84,28 @@ function sendDesktopNotification(title, body) {
|
|
|
34
84
|
} catch {
|
|
35
85
|
}
|
|
36
86
|
}
|
|
37
|
-
function formatArgs(args) {
|
|
38
|
-
if (args === null || args === void 0) return "(none)";
|
|
39
|
-
if (typeof args !== "object" || Array.isArray(args)) {
|
|
40
|
-
const str = typeof args === "string" ? args : JSON.stringify(args);
|
|
41
|
-
return str.length > 200 ? str.slice(0, 200) + "\u2026" : str;
|
|
42
|
-
}
|
|
43
|
-
const entries = Object.entries(args).filter(
|
|
44
|
-
([, v]) => v !== null && v !== void 0 && v !== ""
|
|
45
|
-
);
|
|
46
|
-
if (entries.length === 0) return "(none)";
|
|
47
|
-
const MAX_FIELDS = 5;
|
|
48
|
-
const MAX_VALUE_LEN = 120;
|
|
49
|
-
const lines = entries.slice(0, MAX_FIELDS).map(([key, val]) => {
|
|
50
|
-
const str = typeof val === "string" ? val : JSON.stringify(val);
|
|
51
|
-
const truncated = str.length > MAX_VALUE_LEN ? str.slice(0, MAX_VALUE_LEN) + "\u2026" : str;
|
|
52
|
-
return ` ${key}: ${truncated}`;
|
|
53
|
-
});
|
|
54
|
-
if (entries.length > MAX_FIELDS) {
|
|
55
|
-
lines.push(` \u2026 and ${entries.length - MAX_FIELDS} more field(s)`);
|
|
56
|
-
}
|
|
57
|
-
return lines.join("\n");
|
|
58
|
-
}
|
|
59
87
|
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
|
|
60
88
|
if (isTestEnv()) return "deny";
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
console.log(`[DEBUG Native] isTestEnv check:`, {
|
|
64
|
-
VITEST: process.env.VITEST,
|
|
65
|
-
NODE_ENV: process.env.NODE_ENV,
|
|
66
|
-
CI: process.env.CI,
|
|
67
|
-
isTest: isTestEnv()
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
const title = locked ? `\u26A1 Node9 \u2014 Locked by Admin Policy` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Requires Approval`;
|
|
89
|
+
const formattedArgs = formatArgs(args);
|
|
90
|
+
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Approval`;
|
|
71
91
|
let message = "";
|
|
72
|
-
if (locked)
|
|
73
|
-
message += `\u26A1 Awaiting remote approval via Slack. Local override is disabled.
|
|
74
|
-
`;
|
|
75
|
-
message += `\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
76
|
-
`;
|
|
77
|
-
}
|
|
78
|
-
message += `Tool: ${toolName}
|
|
92
|
+
if (locked) message += `\u26A0\uFE0F LOCKED BY ADMIN POLICY
|
|
79
93
|
`;
|
|
80
|
-
message += `
|
|
94
|
+
message += `Tool: ${toolName}
|
|
81
95
|
`;
|
|
82
|
-
|
|
83
|
-
message += `Reason: ${explainableLabel}
|
|
96
|
+
message += `Agent: ${agent || "AI Agent"}
|
|
84
97
|
`;
|
|
85
|
-
}
|
|
86
|
-
message += `
|
|
87
|
-
Arguments:
|
|
88
|
-
${formatArgs(args)}`;
|
|
89
|
-
if (!locked) {
|
|
90
|
-
message += `
|
|
98
|
+
message += `Rule: ${explainableLabel || "Security Policy"}
|
|
91
99
|
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
100
|
+
`;
|
|
101
|
+
message += `${formattedArgs}`;
|
|
102
|
+
process.stderr.write(chalk.yellow(`
|
|
103
|
+
\u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
|
|
104
|
+
`));
|
|
96
105
|
return new Promise((resolve) => {
|
|
97
106
|
let childProcess = null;
|
|
98
107
|
const onAbort = () => {
|
|
99
|
-
if (childProcess) {
|
|
108
|
+
if (childProcess && childProcess.pid) {
|
|
100
109
|
try {
|
|
101
110
|
process.kill(childProcess.pid, "SIGKILL");
|
|
102
111
|
} catch {
|
|
@@ -108,83 +117,51 @@ Enter = Allow | Click "Block" to deny`;
|
|
|
108
117
|
if (signal.aborted) return resolve("deny");
|
|
109
118
|
signal.addEventListener("abort", onAbort);
|
|
110
119
|
}
|
|
111
|
-
const cleanup = () => {
|
|
112
|
-
if (signal) signal.removeEventListener("abort", onAbort);
|
|
113
|
-
};
|
|
114
120
|
try {
|
|
115
121
|
if (process.platform === "darwin") {
|
|
116
122
|
const buttons = locked ? `buttons {"Waiting\u2026"} default button "Waiting\u2026"` : `buttons {"Block", "Always Allow", "Allow"} default button "Allow" cancel button "Block"`;
|
|
117
|
-
const script = `
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
childProcess.stdout?.on("data", (d) => output += d.toString());
|
|
125
|
-
childProcess.on("close", (code) => {
|
|
126
|
-
cleanup();
|
|
127
|
-
if (locked) return resolve("deny");
|
|
128
|
-
if (code === 0) {
|
|
129
|
-
if (output.includes("Always Allow")) return resolve("always_allow");
|
|
130
|
-
if (output.includes("Allow")) return resolve("allow");
|
|
131
|
-
}
|
|
132
|
-
resolve("deny");
|
|
133
|
-
});
|
|
123
|
+
const script = `on run argv
|
|
124
|
+
tell application "System Events"
|
|
125
|
+
activate
|
|
126
|
+
display dialog (item 1 of argv) with title (item 2 of argv) ${buttons}
|
|
127
|
+
end tell
|
|
128
|
+
end run`;
|
|
129
|
+
childProcess = spawn("osascript", ["-e", script, "--", message, title]);
|
|
134
130
|
} else if (process.platform === "linux") {
|
|
135
|
-
const argsList =
|
|
136
|
-
"--info",
|
|
137
|
-
"--
|
|
138
|
-
|
|
139
|
-
"--text",
|
|
140
|
-
safeMessage,
|
|
141
|
-
"--ok-label",
|
|
142
|
-
"Waiting for Slack\u2026",
|
|
143
|
-
"--timeout",
|
|
144
|
-
"300"
|
|
145
|
-
] : [
|
|
146
|
-
"--question",
|
|
131
|
+
const argsList = [
|
|
132
|
+
locked ? "--info" : "--question",
|
|
133
|
+
"--modal",
|
|
134
|
+
"--width=450",
|
|
147
135
|
"--title",
|
|
148
136
|
title,
|
|
149
137
|
"--text",
|
|
150
|
-
|
|
138
|
+
message,
|
|
151
139
|
"--ok-label",
|
|
152
|
-
"Allow",
|
|
153
|
-
"--cancel-label",
|
|
154
|
-
"Block",
|
|
155
|
-
"--extra-button",
|
|
156
|
-
"Always Allow",
|
|
140
|
+
locked ? "Waiting..." : "Allow",
|
|
157
141
|
"--timeout",
|
|
158
142
|
"300"
|
|
159
143
|
];
|
|
144
|
+
if (!locked) {
|
|
145
|
+
argsList.push("--cancel-label", "Block");
|
|
146
|
+
argsList.push("--extra-button", "Always Allow");
|
|
147
|
+
}
|
|
160
148
|
childProcess = spawn("zenity", argsList);
|
|
161
|
-
let output = "";
|
|
162
|
-
childProcess.stdout?.on("data", (d) => output += d.toString());
|
|
163
|
-
childProcess.on("close", (code) => {
|
|
164
|
-
cleanup();
|
|
165
|
-
if (locked) return resolve("deny");
|
|
166
|
-
if (output.trim() === "Always Allow") return resolve("always_allow");
|
|
167
|
-
if (code === 0) return resolve("allow");
|
|
168
|
-
resolve("deny");
|
|
169
|
-
});
|
|
170
149
|
} else if (process.platform === "win32") {
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
$res = [System.Windows.MessageBox]::Show("${safeMessage}", "${safeTitle}", "${buttonType}", "Warning", "Button2", "DefaultDesktopOnly");
|
|
175
|
-
if ($res -eq "Yes") { exit 0 } else { exit 1 }`;
|
|
150
|
+
const b64Msg = Buffer.from(message).toString("base64");
|
|
151
|
+
const b64Title = Buffer.from(title).toString("base64");
|
|
152
|
+
const ps = `Add-Type -AssemblyName PresentationFramework; $msg = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("${b64Msg}")); $title = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("${b64Title}")); $res = [System.Windows.MessageBox]::Show($msg, $title, "${locked ? "OK" : "YesNo"}", "Warning", "Button2", "DefaultDesktopOnly"); if ($res -eq "Yes") { exit 0 } else { exit 1 }`;
|
|
176
153
|
childProcess = spawn("powershell", ["-Command", ps]);
|
|
177
|
-
childProcess.on("close", (code) => {
|
|
178
|
-
cleanup();
|
|
179
|
-
if (locked) return resolve("deny");
|
|
180
|
-
resolve(code === 0 ? "allow" : "deny");
|
|
181
|
-
});
|
|
182
|
-
} else {
|
|
183
|
-
cleanup();
|
|
184
|
-
resolve("deny");
|
|
185
154
|
}
|
|
155
|
+
let output = "";
|
|
156
|
+
childProcess?.stdout?.on("data", (d) => output += d.toString());
|
|
157
|
+
childProcess?.on("close", (code) => {
|
|
158
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
159
|
+
if (locked) return resolve("deny");
|
|
160
|
+
if (output.includes("Always Allow")) return resolve("always_allow");
|
|
161
|
+
if (code === 0) return resolve("allow");
|
|
162
|
+
resolve("deny");
|
|
163
|
+
});
|
|
186
164
|
} catch {
|
|
187
|
-
cleanup();
|
|
188
165
|
resolve("deny");
|
|
189
166
|
}
|
|
190
167
|
});
|
|
@@ -712,8 +689,8 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
712
689
|
const isNetworkError = error.message.includes("fetch") || error.name === "AbortError" || error.message.includes("ECONNREFUSED");
|
|
713
690
|
const reason = isAuthError ? "Invalid or missing API key. Run `node9 login` to generate a key (must start with n9_live_)." : isNetworkError ? "Could not reach the Node9 cloud. Check your network or API URL." : error.message;
|
|
714
691
|
console.error(
|
|
715
|
-
|
|
716
|
-
\u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) +
|
|
692
|
+
chalk2.yellow(`
|
|
693
|
+
\u26A0\uFE0F Node9: Cloud API Handshake failed \u2014 ${reason}`) + chalk2.dim(`
|
|
717
694
|
Falling back to local rules...
|
|
718
695
|
`)
|
|
719
696
|
);
|
|
@@ -721,13 +698,13 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
721
698
|
}
|
|
722
699
|
if (cloudEnforced && cloudRequestId) {
|
|
723
700
|
console.error(
|
|
724
|
-
|
|
701
|
+
chalk2.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
|
|
725
702
|
);
|
|
726
|
-
console.error(
|
|
703
|
+
console.error(chalk2.cyan(" Dashboard \u2192 ") + chalk2.bold("Mission Control > Activity Feed\n"));
|
|
727
704
|
} else if (!cloudEnforced) {
|
|
728
705
|
const cloudOffReason = !creds?.apiKey ? "no API key \u2014 run `node9 login` to connect" : "privacy mode (cloud disabled)";
|
|
729
706
|
console.error(
|
|
730
|
-
|
|
707
|
+
chalk2.dim(`
|
|
731
708
|
\u{1F6E1}\uFE0F Node9: intercepted "${toolName}" \u2014 cloud off (${cloudOffReason})
|
|
732
709
|
`)
|
|
733
710
|
);
|
|
@@ -792,9 +769,9 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
792
769
|
try {
|
|
793
770
|
if (!approvers.native && !cloudEnforced) {
|
|
794
771
|
console.error(
|
|
795
|
-
|
|
772
|
+
chalk2.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for browser approval.")
|
|
796
773
|
);
|
|
797
|
-
console.error(
|
|
774
|
+
console.error(chalk2.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
|
|
798
775
|
`));
|
|
799
776
|
}
|
|
800
777
|
const daemonDecision = await askDaemon(toolName, args, meta, signal);
|
|
@@ -817,11 +794,11 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
817
794
|
racePromises.push(
|
|
818
795
|
(async () => {
|
|
819
796
|
try {
|
|
820
|
-
console.log(
|
|
821
|
-
console.log(`${
|
|
822
|
-
console.log(`${
|
|
797
|
+
console.log(chalk2.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
|
|
798
|
+
console.log(`${chalk2.bold("Action:")} ${chalk2.red(toolName)}`);
|
|
799
|
+
console.log(`${chalk2.bold("Flagged By:")} ${chalk2.yellow(explainableLabel)}`);
|
|
823
800
|
if (isRemoteLocked) {
|
|
824
|
-
console.log(
|
|
801
|
+
console.log(chalk2.yellow(`\u26A1 LOCKED BY ADMIN POLICY: Waiting for Slack Approval...
|
|
825
802
|
`));
|
|
826
803
|
await new Promise((_, reject) => {
|
|
827
804
|
signal.addEventListener("abort", () => reject(new Error("Aborted by SaaS")));
|
|
@@ -1068,11 +1045,11 @@ async function pollNode9SaaS(requestId, creds, signal) {
|
|
|
1068
1045
|
if (!statusRes.ok) continue;
|
|
1069
1046
|
const { status, reason } = await statusRes.json();
|
|
1070
1047
|
if (status === "APPROVED") {
|
|
1071
|
-
console.error(
|
|
1048
|
+
console.error(chalk2.green("\u2705 Approved via Cloud.\n"));
|
|
1072
1049
|
return { approved: true, reason };
|
|
1073
1050
|
}
|
|
1074
1051
|
if (status === "DENIED" || status === "AUTO_BLOCKED" || status === "TIMED_OUT") {
|
|
1075
|
-
console.error(
|
|
1052
|
+
console.error(chalk2.red("\u274C Denied via Cloud.\n"));
|
|
1076
1053
|
return { approved: false, reason };
|
|
1077
1054
|
}
|
|
1078
1055
|
} catch {
|
|
@@ -1100,11 +1077,11 @@ async function resolveNode9SaaS(requestId, creds, approved) {
|
|
|
1100
1077
|
import fs2 from "fs";
|
|
1101
1078
|
import path2 from "path";
|
|
1102
1079
|
import os2 from "os";
|
|
1103
|
-
import
|
|
1080
|
+
import chalk3 from "chalk";
|
|
1104
1081
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
1105
1082
|
function printDaemonTip() {
|
|
1106
1083
|
console.log(
|
|
1107
|
-
|
|
1084
|
+
chalk3.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + chalk3.white("\n To view your history or manage persistent rules, run:") + chalk3.green("\n node9 daemon --openui")
|
|
1108
1085
|
);
|
|
1109
1086
|
}
|
|
1110
1087
|
function fullPathCommand(subcommand) {
|
|
@@ -1145,7 +1122,7 @@ async function setupClaude() {
|
|
|
1145
1122
|
matcher: ".*",
|
|
1146
1123
|
hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
|
|
1147
1124
|
});
|
|
1148
|
-
console.log(
|
|
1125
|
+
console.log(chalk3.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
|
|
1149
1126
|
anythingChanged = true;
|
|
1150
1127
|
}
|
|
1151
1128
|
const hasPostHook = settings.hooks.PostToolUse?.some(
|
|
@@ -1157,7 +1134,7 @@ async function setupClaude() {
|
|
|
1157
1134
|
matcher: ".*",
|
|
1158
1135
|
hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
|
|
1159
1136
|
});
|
|
1160
|
-
console.log(
|
|
1137
|
+
console.log(chalk3.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
|
|
1161
1138
|
anythingChanged = true;
|
|
1162
1139
|
}
|
|
1163
1140
|
if (anythingChanged) {
|
|
@@ -1171,10 +1148,10 @@ async function setupClaude() {
|
|
|
1171
1148
|
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
1172
1149
|
}
|
|
1173
1150
|
if (serversToWrap.length > 0) {
|
|
1174
|
-
console.log(
|
|
1175
|
-
console.log(
|
|
1151
|
+
console.log(chalk3.bold("The following existing entries will be modified:\n"));
|
|
1152
|
+
console.log(chalk3.white(` ${mcpPath}`));
|
|
1176
1153
|
for (const { name, originalCmd } of serversToWrap) {
|
|
1177
|
-
console.log(
|
|
1154
|
+
console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
1178
1155
|
}
|
|
1179
1156
|
console.log("");
|
|
1180
1157
|
const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
|
|
@@ -1184,22 +1161,22 @@ async function setupClaude() {
|
|
|
1184
1161
|
}
|
|
1185
1162
|
claudeConfig.mcpServers = servers;
|
|
1186
1163
|
writeJson(mcpPath, claudeConfig);
|
|
1187
|
-
console.log(
|
|
1164
|
+
console.log(chalk3.green(`
|
|
1188
1165
|
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
1189
1166
|
anythingChanged = true;
|
|
1190
1167
|
} else {
|
|
1191
|
-
console.log(
|
|
1168
|
+
console.log(chalk3.yellow(" Skipped MCP server wrapping."));
|
|
1192
1169
|
}
|
|
1193
1170
|
console.log("");
|
|
1194
1171
|
}
|
|
1195
1172
|
if (!anythingChanged && serversToWrap.length === 0) {
|
|
1196
|
-
console.log(
|
|
1173
|
+
console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
|
|
1197
1174
|
printDaemonTip();
|
|
1198
1175
|
return;
|
|
1199
1176
|
}
|
|
1200
1177
|
if (anythingChanged) {
|
|
1201
|
-
console.log(
|
|
1202
|
-
console.log(
|
|
1178
|
+
console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
|
|
1179
|
+
console.log(chalk3.gray(" Restart Claude Code for changes to take effect."));
|
|
1203
1180
|
printDaemonTip();
|
|
1204
1181
|
}
|
|
1205
1182
|
}
|
|
@@ -1227,7 +1204,7 @@ async function setupGemini() {
|
|
|
1227
1204
|
}
|
|
1228
1205
|
]
|
|
1229
1206
|
});
|
|
1230
|
-
console.log(
|
|
1207
|
+
console.log(chalk3.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
|
|
1231
1208
|
anythingChanged = true;
|
|
1232
1209
|
}
|
|
1233
1210
|
const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
|
|
@@ -1240,7 +1217,7 @@ async function setupGemini() {
|
|
|
1240
1217
|
matcher: ".*",
|
|
1241
1218
|
hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
|
|
1242
1219
|
});
|
|
1243
|
-
console.log(
|
|
1220
|
+
console.log(chalk3.green(" \u2705 AfterTool hook added \u2192 node9 log"));
|
|
1244
1221
|
anythingChanged = true;
|
|
1245
1222
|
}
|
|
1246
1223
|
if (anythingChanged) {
|
|
@@ -1254,10 +1231,10 @@ async function setupGemini() {
|
|
|
1254
1231
|
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
1255
1232
|
}
|
|
1256
1233
|
if (serversToWrap.length > 0) {
|
|
1257
|
-
console.log(
|
|
1258
|
-
console.log(
|
|
1234
|
+
console.log(chalk3.bold("The following existing entries will be modified:\n"));
|
|
1235
|
+
console.log(chalk3.white(` ${settingsPath} (mcpServers)`));
|
|
1259
1236
|
for (const { name, originalCmd } of serversToWrap) {
|
|
1260
|
-
console.log(
|
|
1237
|
+
console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
1261
1238
|
}
|
|
1262
1239
|
console.log("");
|
|
1263
1240
|
const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
|
|
@@ -1267,22 +1244,22 @@ async function setupGemini() {
|
|
|
1267
1244
|
}
|
|
1268
1245
|
settings.mcpServers = servers;
|
|
1269
1246
|
writeJson(settingsPath, settings);
|
|
1270
|
-
console.log(
|
|
1247
|
+
console.log(chalk3.green(`
|
|
1271
1248
|
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
1272
1249
|
anythingChanged = true;
|
|
1273
1250
|
} else {
|
|
1274
|
-
console.log(
|
|
1251
|
+
console.log(chalk3.yellow(" Skipped MCP server wrapping."));
|
|
1275
1252
|
}
|
|
1276
1253
|
console.log("");
|
|
1277
1254
|
}
|
|
1278
1255
|
if (!anythingChanged && serversToWrap.length === 0) {
|
|
1279
|
-
console.log(
|
|
1256
|
+
console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
|
|
1280
1257
|
printDaemonTip();
|
|
1281
1258
|
return;
|
|
1282
1259
|
}
|
|
1283
1260
|
if (anythingChanged) {
|
|
1284
|
-
console.log(
|
|
1285
|
-
console.log(
|
|
1261
|
+
console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
|
|
1262
|
+
console.log(chalk3.gray(" Restart Gemini CLI for changes to take effect."));
|
|
1286
1263
|
printDaemonTip();
|
|
1287
1264
|
}
|
|
1288
1265
|
}
|
|
@@ -1301,7 +1278,7 @@ async function setupCursor() {
|
|
|
1301
1278
|
if (!hasPreHook) {
|
|
1302
1279
|
if (!hooksFile.hooks.preToolUse) hooksFile.hooks.preToolUse = [];
|
|
1303
1280
|
hooksFile.hooks.preToolUse.push({ command: fullPathCommand("check") });
|
|
1304
|
-
console.log(
|
|
1281
|
+
console.log(chalk3.green(" \u2705 preToolUse hook added \u2192 node9 check"));
|
|
1305
1282
|
anythingChanged = true;
|
|
1306
1283
|
}
|
|
1307
1284
|
const hasPostHook = hooksFile.hooks.postToolUse?.some(
|
|
@@ -1310,7 +1287,7 @@ async function setupCursor() {
|
|
|
1310
1287
|
if (!hasPostHook) {
|
|
1311
1288
|
if (!hooksFile.hooks.postToolUse) hooksFile.hooks.postToolUse = [];
|
|
1312
1289
|
hooksFile.hooks.postToolUse.push({ command: fullPathCommand("log") });
|
|
1313
|
-
console.log(
|
|
1290
|
+
console.log(chalk3.green(" \u2705 postToolUse hook added \u2192 node9 log"));
|
|
1314
1291
|
anythingChanged = true;
|
|
1315
1292
|
}
|
|
1316
1293
|
if (anythingChanged) {
|
|
@@ -1324,10 +1301,10 @@ async function setupCursor() {
|
|
|
1324
1301
|
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
1325
1302
|
}
|
|
1326
1303
|
if (serversToWrap.length > 0) {
|
|
1327
|
-
console.log(
|
|
1328
|
-
console.log(
|
|
1304
|
+
console.log(chalk3.bold("The following existing entries will be modified:\n"));
|
|
1305
|
+
console.log(chalk3.white(` ${mcpPath}`));
|
|
1329
1306
|
for (const { name, originalCmd } of serversToWrap) {
|
|
1330
|
-
console.log(
|
|
1307
|
+
console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
1331
1308
|
}
|
|
1332
1309
|
console.log("");
|
|
1333
1310
|
const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
|
|
@@ -1337,22 +1314,22 @@ async function setupCursor() {
|
|
|
1337
1314
|
}
|
|
1338
1315
|
mcpConfig.mcpServers = servers;
|
|
1339
1316
|
writeJson(mcpPath, mcpConfig);
|
|
1340
|
-
console.log(
|
|
1317
|
+
console.log(chalk3.green(`
|
|
1341
1318
|
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
1342
1319
|
anythingChanged = true;
|
|
1343
1320
|
} else {
|
|
1344
|
-
console.log(
|
|
1321
|
+
console.log(chalk3.yellow(" Skipped MCP server wrapping."));
|
|
1345
1322
|
}
|
|
1346
1323
|
console.log("");
|
|
1347
1324
|
}
|
|
1348
1325
|
if (!anythingChanged && serversToWrap.length === 0) {
|
|
1349
|
-
console.log(
|
|
1326
|
+
console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Cursor."));
|
|
1350
1327
|
printDaemonTip();
|
|
1351
1328
|
return;
|
|
1352
1329
|
}
|
|
1353
1330
|
if (anythingChanged) {
|
|
1354
|
-
console.log(
|
|
1355
|
-
console.log(
|
|
1331
|
+
console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor!"));
|
|
1332
|
+
console.log(chalk3.gray(" Restart Cursor for changes to take effect."));
|
|
1356
1333
|
printDaemonTip();
|
|
1357
1334
|
}
|
|
1358
1335
|
}
|
|
@@ -2331,7 +2308,7 @@ import path3 from "path";
|
|
|
2331
2308
|
import os3 from "os";
|
|
2332
2309
|
import { spawn as spawn2 } from "child_process";
|
|
2333
2310
|
import { randomUUID } from "crypto";
|
|
2334
|
-
import
|
|
2311
|
+
import chalk4 from "chalk";
|
|
2335
2312
|
var DAEMON_PORT2 = 7391;
|
|
2336
2313
|
var DAEMON_HOST2 = "127.0.0.1";
|
|
2337
2314
|
var homeDir = os3.homedir();
|
|
@@ -2777,7 +2754,7 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
2777
2754
|
return;
|
|
2778
2755
|
}
|
|
2779
2756
|
}
|
|
2780
|
-
console.error(
|
|
2757
|
+
console.error(chalk4.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
|
|
2781
2758
|
process.exit(1);
|
|
2782
2759
|
});
|
|
2783
2760
|
server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
|
|
@@ -2786,17 +2763,17 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
2786
2763
|
JSON.stringify({ pid: process.pid, port: DAEMON_PORT2, internalToken, autoStarted }),
|
|
2787
2764
|
{ mode: 384 }
|
|
2788
2765
|
);
|
|
2789
|
-
console.log(
|
|
2766
|
+
console.log(chalk4.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
|
|
2790
2767
|
});
|
|
2791
2768
|
}
|
|
2792
2769
|
function stopDaemon() {
|
|
2793
|
-
if (!fs3.existsSync(DAEMON_PID_FILE)) return console.log(
|
|
2770
|
+
if (!fs3.existsSync(DAEMON_PID_FILE)) return console.log(chalk4.yellow("Not running."));
|
|
2794
2771
|
try {
|
|
2795
2772
|
const { pid } = JSON.parse(fs3.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
2796
2773
|
process.kill(pid, "SIGTERM");
|
|
2797
|
-
console.log(
|
|
2774
|
+
console.log(chalk4.green("\u2705 Stopped."));
|
|
2798
2775
|
} catch {
|
|
2799
|
-
console.log(
|
|
2776
|
+
console.log(chalk4.gray("Cleaned up stale PID file."));
|
|
2800
2777
|
} finally {
|
|
2801
2778
|
try {
|
|
2802
2779
|
fs3.unlinkSync(DAEMON_PID_FILE);
|
|
@@ -2806,13 +2783,13 @@ function stopDaemon() {
|
|
|
2806
2783
|
}
|
|
2807
2784
|
function daemonStatus() {
|
|
2808
2785
|
if (!fs3.existsSync(DAEMON_PID_FILE))
|
|
2809
|
-
return console.log(
|
|
2786
|
+
return console.log(chalk4.yellow("Node9 daemon: not running"));
|
|
2810
2787
|
try {
|
|
2811
2788
|
const { pid } = JSON.parse(fs3.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
2812
2789
|
process.kill(pid, 0);
|
|
2813
|
-
console.log(
|
|
2790
|
+
console.log(chalk4.green("Node9 daemon: running"));
|
|
2814
2791
|
} catch {
|
|
2815
|
-
console.log(
|
|
2792
|
+
console.log(chalk4.yellow("Node9 daemon: not running (stale PID)"));
|
|
2816
2793
|
}
|
|
2817
2794
|
}
|
|
2818
2795
|
|
|
@@ -2820,7 +2797,7 @@ function daemonStatus() {
|
|
|
2820
2797
|
import { spawn as spawn3, execSync } from "child_process";
|
|
2821
2798
|
import { parseCommandString } from "execa";
|
|
2822
2799
|
import { execa } from "execa";
|
|
2823
|
-
import
|
|
2800
|
+
import chalk5 from "chalk";
|
|
2824
2801
|
import readline from "readline";
|
|
2825
2802
|
import fs5 from "fs";
|
|
2826
2803
|
import path5 from "path";
|
|
@@ -2959,7 +2936,7 @@ async function runProxy(targetCommand) {
|
|
|
2959
2936
|
if (stdout) executable = stdout.trim();
|
|
2960
2937
|
} catch {
|
|
2961
2938
|
}
|
|
2962
|
-
console.log(
|
|
2939
|
+
console.log(chalk5.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
|
|
2963
2940
|
const child = spawn3(executable, args, {
|
|
2964
2941
|
stdio: ["pipe", "pipe", "inherit"],
|
|
2965
2942
|
// We control STDIN and STDOUT
|
|
@@ -3060,28 +3037,28 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
3060
3037
|
fs5.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
3061
3038
|
}
|
|
3062
3039
|
if (options.profile && profileName !== "default") {
|
|
3063
|
-
console.log(
|
|
3064
|
-
console.log(
|
|
3040
|
+
console.log(chalk5.green(`\u2705 Profile "${profileName}" saved`));
|
|
3041
|
+
console.log(chalk5.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
3065
3042
|
} else if (options.local) {
|
|
3066
|
-
console.log(
|
|
3067
|
-
console.log(
|
|
3043
|
+
console.log(chalk5.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
3044
|
+
console.log(chalk5.gray(` All decisions stay on this machine.`));
|
|
3068
3045
|
} else {
|
|
3069
|
-
console.log(
|
|
3070
|
-
console.log(
|
|
3046
|
+
console.log(chalk5.green(`\u2705 Logged in \u2014 agent mode`));
|
|
3047
|
+
console.log(chalk5.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
3071
3048
|
}
|
|
3072
3049
|
});
|
|
3073
3050
|
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) => {
|
|
3074
3051
|
if (target === "gemini") return await setupGemini();
|
|
3075
3052
|
if (target === "claude") return await setupClaude();
|
|
3076
3053
|
if (target === "cursor") return await setupCursor();
|
|
3077
|
-
console.error(
|
|
3054
|
+
console.error(chalk5.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
3078
3055
|
process.exit(1);
|
|
3079
3056
|
});
|
|
3080
3057
|
program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").action((options) => {
|
|
3081
3058
|
const configPath = path5.join(os5.homedir(), ".node9", "config.json");
|
|
3082
3059
|
if (fs5.existsSync(configPath) && !options.force) {
|
|
3083
|
-
console.log(
|
|
3084
|
-
console.log(
|
|
3060
|
+
console.log(chalk5.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
3061
|
+
console.log(chalk5.gray(` Run with --force to overwrite.`));
|
|
3085
3062
|
return;
|
|
3086
3063
|
}
|
|
3087
3064
|
const defaultConfig = {
|
|
@@ -3144,8 +3121,8 @@ program.command("init").description("Create ~/.node9/config.json with default po
|
|
|
3144
3121
|
if (!fs5.existsSync(path5.dirname(configPath)))
|
|
3145
3122
|
fs5.mkdirSync(path5.dirname(configPath), { recursive: true });
|
|
3146
3123
|
fs5.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
|
|
3147
|
-
console.log(
|
|
3148
|
-
console.log(
|
|
3124
|
+
console.log(chalk5.green(`\u2705 Global config created: ${configPath}`));
|
|
3125
|
+
console.log(chalk5.gray(` Edit this file to add custom tool inspection or security rules.`));
|
|
3149
3126
|
});
|
|
3150
3127
|
program.command("status").description("Show current Node9 mode, policy source, and persistent decisions").action(() => {
|
|
3151
3128
|
const creds = getCredentials();
|
|
@@ -3154,43 +3131,43 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
3154
3131
|
const settings = mergedConfig.settings;
|
|
3155
3132
|
console.log("");
|
|
3156
3133
|
if (creds && settings.approvers.cloud) {
|
|
3157
|
-
console.log(
|
|
3134
|
+
console.log(chalk5.green(" \u25CF Agent mode") + chalk5.gray(" \u2014 cloud team policy enforced"));
|
|
3158
3135
|
} else if (creds && !settings.approvers.cloud) {
|
|
3159
3136
|
console.log(
|
|
3160
|
-
|
|
3137
|
+
chalk5.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + chalk5.gray(" \u2014 all decisions stay on this machine")
|
|
3161
3138
|
);
|
|
3162
3139
|
} else {
|
|
3163
3140
|
console.log(
|
|
3164
|
-
|
|
3141
|
+
chalk5.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + chalk5.gray(" \u2014 no API key (Local rules only)")
|
|
3165
3142
|
);
|
|
3166
3143
|
}
|
|
3167
3144
|
console.log("");
|
|
3168
3145
|
if (daemonRunning) {
|
|
3169
3146
|
console.log(
|
|
3170
|
-
|
|
3147
|
+
chalk5.green(" \u25CF Daemon running") + chalk5.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
|
|
3171
3148
|
);
|
|
3172
3149
|
} else {
|
|
3173
|
-
console.log(
|
|
3150
|
+
console.log(chalk5.gray(" \u25CB Daemon stopped"));
|
|
3174
3151
|
}
|
|
3175
3152
|
if (settings.enableUndo) {
|
|
3176
3153
|
console.log(
|
|
3177
|
-
|
|
3154
|
+
chalk5.magenta(" \u25CF Undo Engine") + chalk5.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
|
|
3178
3155
|
);
|
|
3179
3156
|
}
|
|
3180
3157
|
console.log("");
|
|
3181
|
-
const modeLabel = settings.mode === "audit" ?
|
|
3158
|
+
const modeLabel = settings.mode === "audit" ? chalk5.blue("audit") : settings.mode === "strict" ? chalk5.red("strict") : chalk5.white("standard");
|
|
3182
3159
|
console.log(` Mode: ${modeLabel}`);
|
|
3183
3160
|
const projectConfig = path5.join(process.cwd(), "node9.config.json");
|
|
3184
3161
|
const globalConfig = path5.join(os5.homedir(), ".node9", "config.json");
|
|
3185
3162
|
console.log(
|
|
3186
|
-
` Local: ${fs5.existsSync(projectConfig) ?
|
|
3163
|
+
` Local: ${fs5.existsSync(projectConfig) ? chalk5.green("Active (node9.config.json)") : chalk5.gray("Not present")}`
|
|
3187
3164
|
);
|
|
3188
3165
|
console.log(
|
|
3189
|
-
` Global: ${fs5.existsSync(globalConfig) ?
|
|
3166
|
+
` Global: ${fs5.existsSync(globalConfig) ? chalk5.green("Active (~/.node9/config.json)") : chalk5.gray("Not present")}`
|
|
3190
3167
|
);
|
|
3191
3168
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
3192
3169
|
console.log(
|
|
3193
|
-
` Sandbox: ${
|
|
3170
|
+
` Sandbox: ${chalk5.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
3194
3171
|
);
|
|
3195
3172
|
}
|
|
3196
3173
|
const pauseState = checkPause();
|
|
@@ -3198,7 +3175,7 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
3198
3175
|
const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
|
|
3199
3176
|
console.log("");
|
|
3200
3177
|
console.log(
|
|
3201
|
-
|
|
3178
|
+
chalk5.yellow(` \u23F8 PAUSED until ${expiresAt}`) + chalk5.gray(" \u2014 all tool calls allowed")
|
|
3202
3179
|
);
|
|
3203
3180
|
}
|
|
3204
3181
|
console.log("");
|
|
@@ -3209,13 +3186,13 @@ program.command("daemon").description("Run the local approval server").argument(
|
|
|
3209
3186
|
if (cmd === "stop") return stopDaemon();
|
|
3210
3187
|
if (cmd === "status") return daemonStatus();
|
|
3211
3188
|
if (cmd !== "start" && action !== void 0) {
|
|
3212
|
-
console.error(
|
|
3189
|
+
console.error(chalk5.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
|
|
3213
3190
|
process.exit(1);
|
|
3214
3191
|
}
|
|
3215
3192
|
if (options.openui) {
|
|
3216
3193
|
if (isDaemonRunning()) {
|
|
3217
3194
|
openBrowserLocal();
|
|
3218
|
-
console.log(
|
|
3195
|
+
console.log(chalk5.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
|
|
3219
3196
|
process.exit(0);
|
|
3220
3197
|
}
|
|
3221
3198
|
const child = spawn3("node9", ["daemon"], { detached: true, stdio: "ignore" });
|
|
@@ -3225,14 +3202,14 @@ program.command("daemon").description("Run the local approval server").argument(
|
|
|
3225
3202
|
if (isDaemonRunning()) break;
|
|
3226
3203
|
}
|
|
3227
3204
|
openBrowserLocal();
|
|
3228
|
-
console.log(
|
|
3205
|
+
console.log(chalk5.green(`
|
|
3229
3206
|
\u{1F6E1}\uFE0F Node9 daemon started + browser opened`));
|
|
3230
3207
|
process.exit(0);
|
|
3231
3208
|
}
|
|
3232
3209
|
if (options.background) {
|
|
3233
3210
|
const child = spawn3("node9", ["daemon"], { detached: true, stdio: "ignore" });
|
|
3234
3211
|
child.unref();
|
|
3235
|
-
console.log(
|
|
3212
|
+
console.log(chalk5.green(`
|
|
3236
3213
|
\u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
|
|
3237
3214
|
process.exit(0);
|
|
3238
3215
|
}
|
|
@@ -3284,10 +3261,10 @@ RAW: ${raw}
|
|
|
3284
3261
|
const sendBlock = (msg, result2) => {
|
|
3285
3262
|
const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || "Local Security Policy";
|
|
3286
3263
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
3287
|
-
console.error(
|
|
3264
|
+
console.error(chalk5.red(`
|
|
3288
3265
|
\u{1F6D1} Node9 blocked "${toolName}"`));
|
|
3289
|
-
console.error(
|
|
3290
|
-
if (result2?.changeHint) console.error(
|
|
3266
|
+
console.error(chalk5.gray(` Triggered by: ${blockedByContext}`));
|
|
3267
|
+
if (result2?.changeHint) console.error(chalk5.cyan(` To change: ${result2.changeHint}`));
|
|
3291
3268
|
console.error("");
|
|
3292
3269
|
let aiFeedbackMessage = "";
|
|
3293
3270
|
if (isHumanDecision) {
|
|
@@ -3349,7 +3326,7 @@ RAW: ${raw}
|
|
|
3349
3326
|
process.exit(0);
|
|
3350
3327
|
}
|
|
3351
3328
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
3352
|
-
console.error(
|
|
3329
|
+
console.error(chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
3353
3330
|
const daemonReady = await autoStartDaemonAndWait();
|
|
3354
3331
|
if (daemonReady) {
|
|
3355
3332
|
const retry = await authorizeHeadless(toolName, toolInput, false, meta);
|
|
@@ -3457,7 +3434,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
3457
3434
|
const ms = parseDuration(options.duration);
|
|
3458
3435
|
if (ms === null) {
|
|
3459
3436
|
console.error(
|
|
3460
|
-
|
|
3437
|
+
chalk5.red(`
|
|
3461
3438
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
3462
3439
|
`)
|
|
3463
3440
|
);
|
|
@@ -3465,20 +3442,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
3465
3442
|
}
|
|
3466
3443
|
pauseNode9(ms, options.duration);
|
|
3467
3444
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
3468
|
-
console.log(
|
|
3445
|
+
console.log(chalk5.yellow(`
|
|
3469
3446
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
3470
|
-
console.log(
|
|
3471
|
-
console.log(
|
|
3447
|
+
console.log(chalk5.gray(` All tool calls will be allowed without review.`));
|
|
3448
|
+
console.log(chalk5.gray(` Run "node9 resume" to re-enable early.
|
|
3472
3449
|
`));
|
|
3473
3450
|
});
|
|
3474
3451
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
3475
3452
|
const { paused } = checkPause();
|
|
3476
3453
|
if (!paused) {
|
|
3477
|
-
console.log(
|
|
3454
|
+
console.log(chalk5.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
3478
3455
|
return;
|
|
3479
3456
|
}
|
|
3480
3457
|
resumeNode9();
|
|
3481
|
-
console.log(
|
|
3458
|
+
console.log(chalk5.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
3482
3459
|
});
|
|
3483
3460
|
var HOOK_BASED_AGENTS = {
|
|
3484
3461
|
claude: "claude",
|
|
@@ -3491,21 +3468,21 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
3491
3468
|
if (HOOK_BASED_AGENTS[firstArg] !== void 0) {
|
|
3492
3469
|
const target = HOOK_BASED_AGENTS[firstArg];
|
|
3493
3470
|
console.error(
|
|
3494
|
-
|
|
3471
|
+
chalk5.yellow(`
|
|
3495
3472
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
3496
3473
|
);
|
|
3497
|
-
console.error(
|
|
3474
|
+
console.error(chalk5.white(`
|
|
3498
3475
|
"${target}" uses its own hook system. Use:`));
|
|
3499
3476
|
console.error(
|
|
3500
|
-
|
|
3477
|
+
chalk5.green(` node9 addto ${target} `) + chalk5.gray("# one-time setup")
|
|
3501
3478
|
);
|
|
3502
|
-
console.error(
|
|
3479
|
+
console.error(chalk5.green(` ${target} `) + chalk5.gray("# run normally"));
|
|
3503
3480
|
process.exit(1);
|
|
3504
3481
|
}
|
|
3505
3482
|
const fullCommand = commandArgs.join(" ");
|
|
3506
3483
|
let result = await authorizeHeadless("shell", { command: fullCommand });
|
|
3507
3484
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
3508
|
-
console.error(
|
|
3485
|
+
console.error(chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
3509
3486
|
const daemonReady = await autoStartDaemonAndWait();
|
|
3510
3487
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
3511
3488
|
}
|
|
@@ -3514,12 +3491,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
3514
3491
|
}
|
|
3515
3492
|
if (!result.approved) {
|
|
3516
3493
|
console.error(
|
|
3517
|
-
|
|
3494
|
+
chalk5.red(`
|
|
3518
3495
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
3519
3496
|
);
|
|
3520
3497
|
process.exit(1);
|
|
3521
3498
|
}
|
|
3522
|
-
console.error(
|
|
3499
|
+
console.error(chalk5.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
3523
3500
|
await runProxy(fullCommand);
|
|
3524
3501
|
} else {
|
|
3525
3502
|
program.help();
|
|
@@ -3528,20 +3505,20 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
3528
3505
|
program.command("undo").description("Revert the project to the state before the last AI action").action(async () => {
|
|
3529
3506
|
const hash = getLatestSnapshotHash();
|
|
3530
3507
|
if (!hash) {
|
|
3531
|
-
console.log(
|
|
3508
|
+
console.log(chalk5.yellow("\n\u2139\uFE0F No Undo snapshot found for this machine.\n"));
|
|
3532
3509
|
return;
|
|
3533
3510
|
}
|
|
3534
|
-
console.log(
|
|
3535
|
-
console.log(
|
|
3511
|
+
console.log(chalk5.magenta.bold("\n\u23EA NODE9 UNDO ENGINE"));
|
|
3512
|
+
console.log(chalk5.white(`Target Snapshot: ${chalk5.gray(hash.slice(0, 7))}`));
|
|
3536
3513
|
const proceed = await confirm3({
|
|
3537
3514
|
message: "Revert all files to the state before the last AI action?",
|
|
3538
3515
|
default: false
|
|
3539
3516
|
});
|
|
3540
3517
|
if (proceed) {
|
|
3541
3518
|
if (applyUndo(hash)) {
|
|
3542
|
-
console.log(
|
|
3519
|
+
console.log(chalk5.green("\u2705 Project reverted successfully.\n"));
|
|
3543
3520
|
} else {
|
|
3544
|
-
console.error(
|
|
3521
|
+
console.error(chalk5.red("\u274C Undo failed. Ensure you are in a Git repository.\n"));
|
|
3545
3522
|
}
|
|
3546
3523
|
}
|
|
3547
3524
|
});
|