@node9/proxy 1.0.6 → 1.0.8
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/LICENSE +183 -21
- package/README.md +2 -65
- package/dist/cli.js +278 -121
- package/dist/cli.mjs +278 -121
- package/dist/index.js +139 -31
- package/dist/index.mjs +139 -31
- package/package.json +4 -2
package/dist/cli.mjs
CHANGED
|
@@ -7,13 +7,14 @@ import { Command } from "commander";
|
|
|
7
7
|
import chalk2 from "chalk";
|
|
8
8
|
import { confirm } from "@inquirer/prompts";
|
|
9
9
|
import fs from "fs";
|
|
10
|
-
import
|
|
10
|
+
import path2 from "path";
|
|
11
11
|
import os from "os";
|
|
12
12
|
import pm from "picomatch";
|
|
13
13
|
import { parse } from "sh-syntax";
|
|
14
14
|
|
|
15
15
|
// src/ui/native.ts
|
|
16
16
|
import { spawn } from "child_process";
|
|
17
|
+
import path from "path";
|
|
17
18
|
import chalk from "chalk";
|
|
18
19
|
var isTestEnv = () => {
|
|
19
20
|
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";
|
|
@@ -23,8 +24,29 @@ function smartTruncate(str, maxLen = 500) {
|
|
|
23
24
|
const edge = Math.floor(maxLen / 2) - 3;
|
|
24
25
|
return `${str.slice(0, edge)} ... ${str.slice(-edge)}`;
|
|
25
26
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
27
|
+
function extractContext(text, matchedWord) {
|
|
28
|
+
const lines = text.split("\n");
|
|
29
|
+
if (lines.length <= 7 || !matchedWord) return smartTruncate(text, 500);
|
|
30
|
+
const escaped = matchedWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
31
|
+
const pattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
32
|
+
const allHits = lines.map((line, i) => ({ i, line })).filter(({ line }) => pattern.test(line));
|
|
33
|
+
if (allHits.length === 0) return smartTruncate(text, 500);
|
|
34
|
+
const nonComment = allHits.find(({ line }) => {
|
|
35
|
+
const trimmed = line.trim();
|
|
36
|
+
return !trimmed.startsWith("//") && !trimmed.startsWith("#");
|
|
37
|
+
});
|
|
38
|
+
const hitIndex = (nonComment ?? allHits[0]).i;
|
|
39
|
+
const start = Math.max(0, hitIndex - 3);
|
|
40
|
+
const end = Math.min(lines.length, hitIndex + 4);
|
|
41
|
+
const snippet = lines.slice(start, end).map((line, i) => `${start + i === hitIndex ? "\u{1F6D1} " : " "}${line}`).join("\n");
|
|
42
|
+
const head = start > 0 ? `... [${start} lines hidden] ...
|
|
43
|
+
` : "";
|
|
44
|
+
const tail = end < lines.length ? `
|
|
45
|
+
... [${lines.length - end} lines hidden] ...` : "";
|
|
46
|
+
return `${head}${snippet}${tail}`;
|
|
47
|
+
}
|
|
48
|
+
function formatArgs(args, matchedField, matchedWord) {
|
|
49
|
+
if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
|
|
28
50
|
let parsed = args;
|
|
29
51
|
if (typeof args === "string") {
|
|
30
52
|
const trimmed = args.trim();
|
|
@@ -35,11 +57,39 @@ function formatArgs(args) {
|
|
|
35
57
|
parsed = args;
|
|
36
58
|
}
|
|
37
59
|
} else {
|
|
38
|
-
return smartTruncate(args, 600);
|
|
60
|
+
return { message: smartTruncate(args, 600), intent: "EXEC" };
|
|
39
61
|
}
|
|
40
62
|
}
|
|
41
63
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
42
64
|
const obj = parsed;
|
|
65
|
+
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
66
|
+
const file = obj.file_path ? path.basename(String(obj.file_path)) : "file";
|
|
67
|
+
const oldPreview = smartTruncate(String(obj.old_string), 120);
|
|
68
|
+
const newPreview = extractContext(String(obj.new_string), matchedWord);
|
|
69
|
+
return {
|
|
70
|
+
intent: "EDIT",
|
|
71
|
+
message: `\u{1F4DD} EDITING: ${file}
|
|
72
|
+
\u{1F4C2} PATH: ${obj.file_path}
|
|
73
|
+
|
|
74
|
+
--- REPLACING ---
|
|
75
|
+
${oldPreview}
|
|
76
|
+
|
|
77
|
+
+++ NEW CODE +++
|
|
78
|
+
${newPreview}`
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (matchedField && obj[matchedField] !== void 0) {
|
|
82
|
+
const otherKeys = Object.keys(obj).filter((k) => k !== matchedField);
|
|
83
|
+
const context = otherKeys.length > 0 ? `\u2699\uFE0F Context: ${otherKeys.map((k) => `${k}=${smartTruncate(typeof obj[k] === "object" ? JSON.stringify(obj[k]) : String(obj[k]), 30)}`).join(", ")}
|
|
84
|
+
|
|
85
|
+
` : "";
|
|
86
|
+
const content = extractContext(String(obj[matchedField]), matchedWord);
|
|
87
|
+
return {
|
|
88
|
+
intent: "EXEC",
|
|
89
|
+
message: `${context}\u{1F6D1} [${matchedField.toUpperCase()}]:
|
|
90
|
+
${content}`
|
|
91
|
+
};
|
|
92
|
+
}
|
|
43
93
|
const codeKeys = [
|
|
44
94
|
"command",
|
|
45
95
|
"cmd",
|
|
@@ -60,14 +110,18 @@ function formatArgs(args) {
|
|
|
60
110
|
if (foundKey) {
|
|
61
111
|
const val = obj[foundKey];
|
|
62
112
|
const str = typeof val === "string" ? val : JSON.stringify(val);
|
|
63
|
-
return
|
|
64
|
-
|
|
113
|
+
return {
|
|
114
|
+
intent: "EXEC",
|
|
115
|
+
message: `[${foundKey.toUpperCase()}]:
|
|
116
|
+
${smartTruncate(str, 500)}`
|
|
117
|
+
};
|
|
65
118
|
}
|
|
66
|
-
|
|
119
|
+
const msg = Object.entries(obj).slice(0, 5).map(
|
|
67
120
|
([k, v]) => ` ${k}: ${smartTruncate(typeof v === "string" ? v : JSON.stringify(v), 300)}`
|
|
68
121
|
).join("\n");
|
|
122
|
+
return { intent: "EXEC", message: msg };
|
|
69
123
|
}
|
|
70
|
-
return smartTruncate(JSON.stringify(parsed), 200);
|
|
124
|
+
return { intent: "EXEC", message: smartTruncate(JSON.stringify(parsed), 200) };
|
|
71
125
|
}
|
|
72
126
|
function sendDesktopNotification(title, body) {
|
|
73
127
|
if (isTestEnv()) return;
|
|
@@ -120,10 +174,11 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
120
174
|
}
|
|
121
175
|
return lines.join("\n");
|
|
122
176
|
}
|
|
123
|
-
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
|
|
177
|
+
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord) {
|
|
124
178
|
if (isTestEnv()) return "deny";
|
|
125
|
-
const formattedArgs = formatArgs(args);
|
|
126
|
-
const
|
|
179
|
+
const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
|
|
180
|
+
const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
|
|
181
|
+
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
|
|
127
182
|
const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
|
|
128
183
|
process.stderr.write(chalk.yellow(`
|
|
129
184
|
\u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
|
|
@@ -201,10 +256,10 @@ end run`;
|
|
|
201
256
|
}
|
|
202
257
|
|
|
203
258
|
// src/core.ts
|
|
204
|
-
var PAUSED_FILE =
|
|
205
|
-
var TRUST_FILE =
|
|
206
|
-
var LOCAL_AUDIT_LOG =
|
|
207
|
-
var HOOK_DEBUG_LOG =
|
|
259
|
+
var PAUSED_FILE = path2.join(os.homedir(), ".node9", "PAUSED");
|
|
260
|
+
var TRUST_FILE = path2.join(os.homedir(), ".node9", "trust.json");
|
|
261
|
+
var LOCAL_AUDIT_LOG = path2.join(os.homedir(), ".node9", "audit.log");
|
|
262
|
+
var HOOK_DEBUG_LOG = path2.join(os.homedir(), ".node9", "hook-debug.log");
|
|
208
263
|
function checkPause() {
|
|
209
264
|
try {
|
|
210
265
|
if (!fs.existsSync(PAUSED_FILE)) return { paused: false };
|
|
@@ -222,7 +277,7 @@ function checkPause() {
|
|
|
222
277
|
}
|
|
223
278
|
}
|
|
224
279
|
function atomicWriteSync(filePath, data, options) {
|
|
225
|
-
const dir =
|
|
280
|
+
const dir = path2.dirname(filePath);
|
|
226
281
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
227
282
|
const tmpPath = `${filePath}.${os.hostname()}.${process.pid}.tmp`;
|
|
228
283
|
fs.writeFileSync(tmpPath, data, options);
|
|
@@ -273,7 +328,7 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
273
328
|
}
|
|
274
329
|
function appendToLog(logPath, entry) {
|
|
275
330
|
try {
|
|
276
|
-
const dir =
|
|
331
|
+
const dir = path2.dirname(logPath);
|
|
277
332
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
278
333
|
fs.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
279
334
|
} catch {
|
|
@@ -317,9 +372,21 @@ function matchesPattern(text, patterns) {
|
|
|
317
372
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
318
373
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
319
374
|
}
|
|
320
|
-
function getNestedValue(obj,
|
|
375
|
+
function getNestedValue(obj, path7) {
|
|
321
376
|
if (!obj || typeof obj !== "object") return null;
|
|
322
|
-
return
|
|
377
|
+
return path7.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
378
|
+
}
|
|
379
|
+
function shouldSnapshot(toolName, args, config) {
|
|
380
|
+
if (!config.settings.enableUndo) return false;
|
|
381
|
+
const snap = config.policy.snapshot;
|
|
382
|
+
if (!snap.tools.includes(toolName.toLowerCase())) return false;
|
|
383
|
+
const a = args && typeof args === "object" ? args : {};
|
|
384
|
+
const filePath = String(a.file_path ?? a.path ?? a.filename ?? "");
|
|
385
|
+
if (filePath) {
|
|
386
|
+
if (snap.ignorePaths.length && pm(snap.ignorePaths)(filePath)) return false;
|
|
387
|
+
if (snap.onlyPaths.length && !pm(snap.onlyPaths)(filePath)) return false;
|
|
388
|
+
}
|
|
389
|
+
return true;
|
|
323
390
|
}
|
|
324
391
|
function evaluateSmartConditions(args, rule) {
|
|
325
392
|
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
@@ -508,6 +575,18 @@ var DEFAULT_CONFIG = {
|
|
|
508
575
|
"terminal.execute": "command",
|
|
509
576
|
"postgres:query": "sql"
|
|
510
577
|
},
|
|
578
|
+
snapshot: {
|
|
579
|
+
tools: [
|
|
580
|
+
"str_replace_based_edit_tool",
|
|
581
|
+
"write_file",
|
|
582
|
+
"edit_file",
|
|
583
|
+
"create_file",
|
|
584
|
+
"edit",
|
|
585
|
+
"replace"
|
|
586
|
+
],
|
|
587
|
+
onlyPaths: [],
|
|
588
|
+
ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
|
|
589
|
+
},
|
|
511
590
|
rules: [
|
|
512
591
|
{
|
|
513
592
|
action: "rm",
|
|
@@ -546,7 +625,7 @@ function _resetConfigCache() {
|
|
|
546
625
|
}
|
|
547
626
|
function getGlobalSettings() {
|
|
548
627
|
try {
|
|
549
|
-
const globalConfigPath =
|
|
628
|
+
const globalConfigPath = path2.join(os.homedir(), ".node9", "config.json");
|
|
550
629
|
if (fs.existsSync(globalConfigPath)) {
|
|
551
630
|
const parsed = JSON.parse(fs.readFileSync(globalConfigPath, "utf-8"));
|
|
552
631
|
const settings = parsed.settings || {};
|
|
@@ -570,7 +649,7 @@ function getGlobalSettings() {
|
|
|
570
649
|
}
|
|
571
650
|
function getInternalToken() {
|
|
572
651
|
try {
|
|
573
|
-
const pidFile =
|
|
652
|
+
const pidFile = path2.join(os.homedir(), ".node9", "daemon.pid");
|
|
574
653
|
if (!fs.existsSync(pidFile)) return null;
|
|
575
654
|
const data = JSON.parse(fs.readFileSync(pidFile, "utf-8"));
|
|
576
655
|
process.kill(data.pid, 0);
|
|
@@ -674,9 +753,29 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
674
753
|
})
|
|
675
754
|
);
|
|
676
755
|
if (isDangerous) {
|
|
756
|
+
let matchedField;
|
|
757
|
+
if (matchedDangerousWord && args && typeof args === "object" && !Array.isArray(args)) {
|
|
758
|
+
const obj = args;
|
|
759
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
760
|
+
if (typeof value === "string") {
|
|
761
|
+
try {
|
|
762
|
+
if (new RegExp(
|
|
763
|
+
`\\b${matchedDangerousWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
|
|
764
|
+
"i"
|
|
765
|
+
).test(value)) {
|
|
766
|
+
matchedField = key;
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
} catch {
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
677
774
|
return {
|
|
678
775
|
decision: "review",
|
|
679
|
-
blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"
|
|
776
|
+
blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`,
|
|
777
|
+
matchedWord: matchedDangerousWord,
|
|
778
|
+
matchedField
|
|
680
779
|
};
|
|
681
780
|
}
|
|
682
781
|
if (config.settings.mode === "strict") {
|
|
@@ -688,9 +787,9 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
688
787
|
}
|
|
689
788
|
async function explainPolicy(toolName, args) {
|
|
690
789
|
const steps = [];
|
|
691
|
-
const globalPath =
|
|
692
|
-
const projectPath =
|
|
693
|
-
const credsPath =
|
|
790
|
+
const globalPath = path2.join(os.homedir(), ".node9", "config.json");
|
|
791
|
+
const projectPath = path2.join(process.cwd(), "node9.config.json");
|
|
792
|
+
const credsPath = path2.join(os.homedir(), ".node9", "credentials.json");
|
|
694
793
|
const waterfall = [
|
|
695
794
|
{
|
|
696
795
|
tier: 1,
|
|
@@ -994,7 +1093,7 @@ var DAEMON_PORT = 7391;
|
|
|
994
1093
|
var DAEMON_HOST = "127.0.0.1";
|
|
995
1094
|
function isDaemonRunning() {
|
|
996
1095
|
try {
|
|
997
|
-
const pidFile =
|
|
1096
|
+
const pidFile = path2.join(os.homedir(), ".node9", "daemon.pid");
|
|
998
1097
|
if (!fs.existsSync(pidFile)) return false;
|
|
999
1098
|
const { pid, port } = JSON.parse(fs.readFileSync(pidFile, "utf-8"));
|
|
1000
1099
|
if (port !== DAEMON_PORT) return false;
|
|
@@ -1006,7 +1105,7 @@ function isDaemonRunning() {
|
|
|
1006
1105
|
}
|
|
1007
1106
|
function getPersistentDecision(toolName) {
|
|
1008
1107
|
try {
|
|
1009
|
-
const file =
|
|
1108
|
+
const file = path2.join(os.homedir(), ".node9", "decisions.json");
|
|
1010
1109
|
if (!fs.existsSync(file)) return null;
|
|
1011
1110
|
const decisions = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
1012
1111
|
const d = decisions[toolName];
|
|
@@ -1077,7 +1176,7 @@ async function resolveViaDaemon(id, decision, internalToken) {
|
|
|
1077
1176
|
signal: AbortSignal.timeout(3e3)
|
|
1078
1177
|
});
|
|
1079
1178
|
}
|
|
1080
|
-
async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta) {
|
|
1179
|
+
async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
|
|
1081
1180
|
if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
|
|
1082
1181
|
const pauseState = checkPause();
|
|
1083
1182
|
if (pauseState.paused) return { approved: true, checkedBy: "paused" };
|
|
@@ -1097,6 +1196,8 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1097
1196
|
}
|
|
1098
1197
|
const isManual = meta?.agent === "Terminal";
|
|
1099
1198
|
let explainableLabel = "Local Config";
|
|
1199
|
+
let policyMatchedField;
|
|
1200
|
+
let policyMatchedWord;
|
|
1100
1201
|
if (config.settings.mode === "audit") {
|
|
1101
1202
|
if (!isIgnoredTool(toolName)) {
|
|
1102
1203
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
@@ -1132,6 +1233,8 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1132
1233
|
};
|
|
1133
1234
|
}
|
|
1134
1235
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
1236
|
+
policyMatchedField = policyResult.matchedField;
|
|
1237
|
+
policyMatchedWord = policyResult.matchedWord;
|
|
1135
1238
|
const persistent = getPersistentDecision(toolName);
|
|
1136
1239
|
if (persistent === "allow") {
|
|
1137
1240
|
if (creds?.apiKey) auditLocalAllow(toolName, args, "persistent", creds, meta);
|
|
@@ -1224,7 +1327,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1224
1327
|
racePromises.push(
|
|
1225
1328
|
(async () => {
|
|
1226
1329
|
try {
|
|
1227
|
-
if (isDaemonRunning() && internalToken) {
|
|
1330
|
+
if (isDaemonRunning() && internalToken && !options?.calledFromDaemon) {
|
|
1228
1331
|
viewerId = await notifyDaemonViewer(toolName, args, meta).catch(() => null);
|
|
1229
1332
|
}
|
|
1230
1333
|
const cloudResult = await pollNode9SaaS(cloudRequestId, creds, signal);
|
|
@@ -1252,7 +1355,9 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1252
1355
|
meta?.agent,
|
|
1253
1356
|
explainableLabel,
|
|
1254
1357
|
isRemoteLocked,
|
|
1255
|
-
signal
|
|
1358
|
+
signal,
|
|
1359
|
+
policyMatchedField,
|
|
1360
|
+
policyMatchedWord
|
|
1256
1361
|
);
|
|
1257
1362
|
if (decision === "always_allow") {
|
|
1258
1363
|
writeTrustSession(toolName, 36e5);
|
|
@@ -1269,7 +1374,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1269
1374
|
})()
|
|
1270
1375
|
);
|
|
1271
1376
|
}
|
|
1272
|
-
if (approvers.browser && isDaemonRunning()) {
|
|
1377
|
+
if (approvers.browser && isDaemonRunning() && !options?.calledFromDaemon) {
|
|
1273
1378
|
racePromises.push(
|
|
1274
1379
|
(async () => {
|
|
1275
1380
|
try {
|
|
@@ -1405,8 +1510,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
1405
1510
|
}
|
|
1406
1511
|
function getConfig() {
|
|
1407
1512
|
if (cachedConfig) return cachedConfig;
|
|
1408
|
-
const globalPath =
|
|
1409
|
-
const projectPath =
|
|
1513
|
+
const globalPath = path2.join(os.homedir(), ".node9", "config.json");
|
|
1514
|
+
const projectPath = path2.join(process.cwd(), "node9.config.json");
|
|
1410
1515
|
const globalConfig = tryLoadConfig(globalPath);
|
|
1411
1516
|
const projectConfig = tryLoadConfig(projectPath);
|
|
1412
1517
|
const mergedSettings = {
|
|
@@ -1419,7 +1524,12 @@ function getConfig() {
|
|
|
1419
1524
|
ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
|
|
1420
1525
|
toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
|
|
1421
1526
|
rules: [...DEFAULT_CONFIG.policy.rules],
|
|
1422
|
-
smartRules: [...DEFAULT_CONFIG.policy.smartRules]
|
|
1527
|
+
smartRules: [...DEFAULT_CONFIG.policy.smartRules],
|
|
1528
|
+
snapshot: {
|
|
1529
|
+
tools: [...DEFAULT_CONFIG.policy.snapshot.tools],
|
|
1530
|
+
onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
|
|
1531
|
+
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
1532
|
+
}
|
|
1423
1533
|
};
|
|
1424
1534
|
const applyLayer = (source) => {
|
|
1425
1535
|
if (!source) return;
|
|
@@ -1431,6 +1541,7 @@ function getConfig() {
|
|
|
1431
1541
|
if (s.enableHookLogDebug !== void 0)
|
|
1432
1542
|
mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
|
|
1433
1543
|
if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
|
|
1544
|
+
if (s.approvalTimeoutMs !== void 0) mergedSettings.approvalTimeoutMs = s.approvalTimeoutMs;
|
|
1434
1545
|
if (s.environment !== void 0) mergedSettings.environment = s.environment;
|
|
1435
1546
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
1436
1547
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
@@ -1439,6 +1550,12 @@ function getConfig() {
|
|
|
1439
1550
|
mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
1440
1551
|
if (p.rules) mergedPolicy.rules.push(...p.rules);
|
|
1441
1552
|
if (p.smartRules) mergedPolicy.smartRules.push(...p.smartRules);
|
|
1553
|
+
if (p.snapshot) {
|
|
1554
|
+
const s2 = p.snapshot;
|
|
1555
|
+
if (s2.tools) mergedPolicy.snapshot.tools.push(...s2.tools);
|
|
1556
|
+
if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
|
|
1557
|
+
if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
|
|
1558
|
+
}
|
|
1442
1559
|
};
|
|
1443
1560
|
applyLayer(globalConfig);
|
|
1444
1561
|
applyLayer(projectConfig);
|
|
@@ -1446,6 +1563,9 @@ function getConfig() {
|
|
|
1446
1563
|
mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
|
|
1447
1564
|
mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
|
|
1448
1565
|
mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
|
|
1566
|
+
mergedPolicy.snapshot.tools = [...new Set(mergedPolicy.snapshot.tools)];
|
|
1567
|
+
mergedPolicy.snapshot.onlyPaths = [...new Set(mergedPolicy.snapshot.onlyPaths)];
|
|
1568
|
+
mergedPolicy.snapshot.ignorePaths = [...new Set(mergedPolicy.snapshot.ignorePaths)];
|
|
1449
1569
|
cachedConfig = {
|
|
1450
1570
|
settings: mergedSettings,
|
|
1451
1571
|
policy: mergedPolicy,
|
|
@@ -1474,7 +1594,7 @@ function getCredentials() {
|
|
|
1474
1594
|
};
|
|
1475
1595
|
}
|
|
1476
1596
|
try {
|
|
1477
|
-
const credPath =
|
|
1597
|
+
const credPath = path2.join(os.homedir(), ".node9", "credentials.json");
|
|
1478
1598
|
if (fs.existsSync(credPath)) {
|
|
1479
1599
|
const creds = JSON.parse(fs.readFileSync(credPath, "utf-8"));
|
|
1480
1600
|
const profileName = process.env.NODE9_PROFILE || "default";
|
|
@@ -1592,7 +1712,7 @@ async function resolveNode9SaaS(requestId, creds, approved) {
|
|
|
1592
1712
|
|
|
1593
1713
|
// src/setup.ts
|
|
1594
1714
|
import fs2 from "fs";
|
|
1595
|
-
import
|
|
1715
|
+
import path3 from "path";
|
|
1596
1716
|
import os2 from "os";
|
|
1597
1717
|
import chalk3 from "chalk";
|
|
1598
1718
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
@@ -1617,14 +1737,14 @@ function readJson(filePath) {
|
|
|
1617
1737
|
return null;
|
|
1618
1738
|
}
|
|
1619
1739
|
function writeJson(filePath, data) {
|
|
1620
|
-
const dir =
|
|
1740
|
+
const dir = path3.dirname(filePath);
|
|
1621
1741
|
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
|
|
1622
1742
|
fs2.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
1623
1743
|
}
|
|
1624
1744
|
async function setupClaude() {
|
|
1625
1745
|
const homeDir2 = os2.homedir();
|
|
1626
|
-
const mcpPath =
|
|
1627
|
-
const hooksPath =
|
|
1746
|
+
const mcpPath = path3.join(homeDir2, ".claude.json");
|
|
1747
|
+
const hooksPath = path3.join(homeDir2, ".claude", "settings.json");
|
|
1628
1748
|
const claudeConfig = readJson(mcpPath) ?? {};
|
|
1629
1749
|
const settings = readJson(hooksPath) ?? {};
|
|
1630
1750
|
const servers = claudeConfig.mcpServers ?? {};
|
|
@@ -1699,7 +1819,7 @@ async function setupClaude() {
|
|
|
1699
1819
|
}
|
|
1700
1820
|
async function setupGemini() {
|
|
1701
1821
|
const homeDir2 = os2.homedir();
|
|
1702
|
-
const settingsPath =
|
|
1822
|
+
const settingsPath = path3.join(homeDir2, ".gemini", "settings.json");
|
|
1703
1823
|
const settings = readJson(settingsPath) ?? {};
|
|
1704
1824
|
const servers = settings.mcpServers ?? {};
|
|
1705
1825
|
let anythingChanged = false;
|
|
@@ -1782,8 +1902,8 @@ async function setupGemini() {
|
|
|
1782
1902
|
}
|
|
1783
1903
|
async function setupCursor() {
|
|
1784
1904
|
const homeDir2 = os2.homedir();
|
|
1785
|
-
const mcpPath =
|
|
1786
|
-
const hooksPath =
|
|
1905
|
+
const mcpPath = path3.join(homeDir2, ".cursor", "mcp.json");
|
|
1906
|
+
const hooksPath = path3.join(homeDir2, ".cursor", "hooks.json");
|
|
1787
1907
|
const mcpConfig = readJson(mcpPath) ?? {};
|
|
1788
1908
|
const hooksFile = readJson(hooksPath) ?? { version: 1 };
|
|
1789
1909
|
const servers = mcpConfig.mcpServers ?? {};
|
|
@@ -2821,7 +2941,7 @@ var UI_HTML_TEMPLATE = ui_default;
|
|
|
2821
2941
|
// src/daemon/index.ts
|
|
2822
2942
|
import http from "http";
|
|
2823
2943
|
import fs3 from "fs";
|
|
2824
|
-
import
|
|
2944
|
+
import path4 from "path";
|
|
2825
2945
|
import os3 from "os";
|
|
2826
2946
|
import { spawn as spawn2 } from "child_process";
|
|
2827
2947
|
import { randomUUID } from "crypto";
|
|
@@ -2829,14 +2949,14 @@ import chalk4 from "chalk";
|
|
|
2829
2949
|
var DAEMON_PORT2 = 7391;
|
|
2830
2950
|
var DAEMON_HOST2 = "127.0.0.1";
|
|
2831
2951
|
var homeDir = os3.homedir();
|
|
2832
|
-
var DAEMON_PID_FILE =
|
|
2833
|
-
var DECISIONS_FILE =
|
|
2834
|
-
var GLOBAL_CONFIG_FILE =
|
|
2835
|
-
var CREDENTIALS_FILE =
|
|
2836
|
-
var AUDIT_LOG_FILE =
|
|
2837
|
-
var TRUST_FILE2 =
|
|
2952
|
+
var DAEMON_PID_FILE = path4.join(homeDir, ".node9", "daemon.pid");
|
|
2953
|
+
var DECISIONS_FILE = path4.join(homeDir, ".node9", "decisions.json");
|
|
2954
|
+
var GLOBAL_CONFIG_FILE = path4.join(homeDir, ".node9", "config.json");
|
|
2955
|
+
var CREDENTIALS_FILE = path4.join(homeDir, ".node9", "credentials.json");
|
|
2956
|
+
var AUDIT_LOG_FILE = path4.join(homeDir, ".node9", "audit.log");
|
|
2957
|
+
var TRUST_FILE2 = path4.join(homeDir, ".node9", "trust.json");
|
|
2838
2958
|
function atomicWriteSync2(filePath, data, options) {
|
|
2839
|
-
const dir =
|
|
2959
|
+
const dir = path4.dirname(filePath);
|
|
2840
2960
|
if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
|
|
2841
2961
|
const tmpPath = `${filePath}.${randomUUID()}.tmp`;
|
|
2842
2962
|
fs3.writeFileSync(tmpPath, data, options);
|
|
@@ -2880,7 +3000,7 @@ function appendAuditLog(data) {
|
|
|
2880
3000
|
decision: data.decision,
|
|
2881
3001
|
source: "daemon"
|
|
2882
3002
|
};
|
|
2883
|
-
const dir =
|
|
3003
|
+
const dir = path4.dirname(AUDIT_LOG_FILE);
|
|
2884
3004
|
if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
|
|
2885
3005
|
fs3.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
|
|
2886
3006
|
} catch {
|
|
@@ -3085,25 +3205,70 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3085
3205
|
args: e.args,
|
|
3086
3206
|
decision: "auto-deny"
|
|
3087
3207
|
});
|
|
3088
|
-
if (e.waiter) e.waiter("deny");
|
|
3089
|
-
else
|
|
3208
|
+
if (e.waiter) e.waiter("deny", "No response \u2014 auto-denied after timeout");
|
|
3209
|
+
else {
|
|
3210
|
+
e.earlyDecision = "deny";
|
|
3211
|
+
e.earlyReason = "No response \u2014 auto-denied after timeout";
|
|
3212
|
+
}
|
|
3090
3213
|
pending.delete(id);
|
|
3091
3214
|
broadcast("remove", { id });
|
|
3092
3215
|
}
|
|
3093
3216
|
}, AUTO_DENY_MS)
|
|
3094
3217
|
};
|
|
3095
3218
|
pending.set(id, entry);
|
|
3096
|
-
|
|
3097
|
-
|
|
3219
|
+
const browserEnabled = getConfig().settings.approvers?.browser !== false;
|
|
3220
|
+
if (browserEnabled) {
|
|
3221
|
+
broadcast("add", {
|
|
3222
|
+
id,
|
|
3223
|
+
toolName,
|
|
3224
|
+
args,
|
|
3225
|
+
slackDelegated: entry.slackDelegated,
|
|
3226
|
+
agent: entry.agent,
|
|
3227
|
+
mcpServer: entry.mcpServer
|
|
3228
|
+
});
|
|
3229
|
+
if (sseClients.size === 0 && !autoStarted)
|
|
3230
|
+
openBrowser(`http://127.0.0.1:${DAEMON_PORT2}/`);
|
|
3231
|
+
}
|
|
3232
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3233
|
+
res.end(JSON.stringify({ id }));
|
|
3234
|
+
authorizeHeadless(
|
|
3098
3235
|
toolName,
|
|
3099
3236
|
args,
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3237
|
+
false,
|
|
3238
|
+
{
|
|
3239
|
+
agent: typeof agent === "string" ? agent : void 0,
|
|
3240
|
+
mcpServer: typeof mcpServer === "string" ? mcpServer : void 0
|
|
3241
|
+
},
|
|
3242
|
+
{ calledFromDaemon: true }
|
|
3243
|
+
).then((result) => {
|
|
3244
|
+
const e = pending.get(id);
|
|
3245
|
+
if (!e) return;
|
|
3246
|
+
if (result.noApprovalMechanism) return;
|
|
3247
|
+
clearTimeout(e.timer);
|
|
3248
|
+
const decision = result.approved ? "allow" : "deny";
|
|
3249
|
+
appendAuditLog({ toolName: e.toolName, args: e.args, decision });
|
|
3250
|
+
if (e.waiter) {
|
|
3251
|
+
e.waiter(decision, result.reason);
|
|
3252
|
+
pending.delete(id);
|
|
3253
|
+
broadcast("remove", { id });
|
|
3254
|
+
} else {
|
|
3255
|
+
e.earlyDecision = decision;
|
|
3256
|
+
e.earlyReason = result.reason;
|
|
3257
|
+
}
|
|
3258
|
+
}).catch((err) => {
|
|
3259
|
+
const e = pending.get(id);
|
|
3260
|
+
if (!e) return;
|
|
3261
|
+
clearTimeout(e.timer);
|
|
3262
|
+
const reason = err?.reason || "No response \u2014 request timed out";
|
|
3263
|
+
if (e.waiter) e.waiter("deny", reason);
|
|
3264
|
+
else {
|
|
3265
|
+
e.earlyDecision = "deny";
|
|
3266
|
+
e.earlyReason = reason;
|
|
3267
|
+
}
|
|
3268
|
+
pending.delete(id);
|
|
3269
|
+
broadcast("remove", { id });
|
|
3103
3270
|
});
|
|
3104
|
-
|
|
3105
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3106
|
-
return res.end(JSON.stringify({ id }));
|
|
3271
|
+
return;
|
|
3107
3272
|
} catch {
|
|
3108
3273
|
res.writeHead(400).end();
|
|
3109
3274
|
}
|
|
@@ -3113,12 +3278,18 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3113
3278
|
const entry = pending.get(id);
|
|
3114
3279
|
if (!entry) return res.writeHead(404).end();
|
|
3115
3280
|
if (entry.earlyDecision) {
|
|
3281
|
+
pending.delete(id);
|
|
3282
|
+
broadcast("remove", { id });
|
|
3116
3283
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3117
|
-
|
|
3284
|
+
const body = { decision: entry.earlyDecision };
|
|
3285
|
+
if (entry.earlyReason) body.reason = entry.earlyReason;
|
|
3286
|
+
return res.end(JSON.stringify(body));
|
|
3118
3287
|
}
|
|
3119
|
-
entry.waiter = (d) => {
|
|
3288
|
+
entry.waiter = (d, reason) => {
|
|
3120
3289
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3121
|
-
|
|
3290
|
+
const body = { decision: d };
|
|
3291
|
+
if (reason) body.reason = reason;
|
|
3292
|
+
res.end(JSON.stringify(body));
|
|
3122
3293
|
};
|
|
3123
3294
|
return;
|
|
3124
3295
|
}
|
|
@@ -3128,7 +3299,7 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3128
3299
|
const id = pathname.split("/").pop();
|
|
3129
3300
|
const entry = pending.get(id);
|
|
3130
3301
|
if (!entry) return res.writeHead(404).end();
|
|
3131
|
-
const { decision, persist, trustDuration } = JSON.parse(await readBody(req));
|
|
3302
|
+
const { decision, persist, trustDuration, reason } = JSON.parse(await readBody(req));
|
|
3132
3303
|
if (decision === "trust" && trustDuration) {
|
|
3133
3304
|
const ms = TRUST_DURATIONS[trustDuration] ?? 60 * 6e4;
|
|
3134
3305
|
writeTrustEntry(entry.toolName, ms);
|
|
@@ -3153,8 +3324,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3153
3324
|
decision: resolvedDecision
|
|
3154
3325
|
});
|
|
3155
3326
|
clearTimeout(entry.timer);
|
|
3156
|
-
if (entry.waiter) entry.waiter(resolvedDecision);
|
|
3157
|
-
else
|
|
3327
|
+
if (entry.waiter) entry.waiter(resolvedDecision, reason);
|
|
3328
|
+
else {
|
|
3329
|
+
entry.earlyDecision = resolvedDecision;
|
|
3330
|
+
entry.earlyReason = reason;
|
|
3331
|
+
}
|
|
3158
3332
|
pending.delete(id);
|
|
3159
3333
|
broadcast("remove", { id });
|
|
3160
3334
|
res.writeHead(200);
|
|
@@ -3317,16 +3491,16 @@ import { execa } from "execa";
|
|
|
3317
3491
|
import chalk5 from "chalk";
|
|
3318
3492
|
import readline from "readline";
|
|
3319
3493
|
import fs5 from "fs";
|
|
3320
|
-
import
|
|
3494
|
+
import path6 from "path";
|
|
3321
3495
|
import os5 from "os";
|
|
3322
3496
|
|
|
3323
3497
|
// src/undo.ts
|
|
3324
3498
|
import { spawnSync } from "child_process";
|
|
3325
3499
|
import fs4 from "fs";
|
|
3326
|
-
import
|
|
3500
|
+
import path5 from "path";
|
|
3327
3501
|
import os4 from "os";
|
|
3328
|
-
var SNAPSHOT_STACK_PATH =
|
|
3329
|
-
var UNDO_LATEST_PATH =
|
|
3502
|
+
var SNAPSHOT_STACK_PATH = path5.join(os4.homedir(), ".node9", "snapshots.json");
|
|
3503
|
+
var UNDO_LATEST_PATH = path5.join(os4.homedir(), ".node9", "undo_latest.txt");
|
|
3330
3504
|
var MAX_SNAPSHOTS = 10;
|
|
3331
3505
|
function readStack() {
|
|
3332
3506
|
try {
|
|
@@ -3337,7 +3511,7 @@ function readStack() {
|
|
|
3337
3511
|
return [];
|
|
3338
3512
|
}
|
|
3339
3513
|
function writeStack(stack) {
|
|
3340
|
-
const dir =
|
|
3514
|
+
const dir = path5.dirname(SNAPSHOT_STACK_PATH);
|
|
3341
3515
|
if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
|
|
3342
3516
|
fs4.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
3343
3517
|
}
|
|
@@ -3355,8 +3529,8 @@ function buildArgsSummary(tool, args) {
|
|
|
3355
3529
|
async function createShadowSnapshot(tool = "unknown", args = {}) {
|
|
3356
3530
|
try {
|
|
3357
3531
|
const cwd = process.cwd();
|
|
3358
|
-
if (!fs4.existsSync(
|
|
3359
|
-
const tempIndex =
|
|
3532
|
+
if (!fs4.existsSync(path5.join(cwd, ".git"))) return null;
|
|
3533
|
+
const tempIndex = path5.join(cwd, ".git", `node9_index_${Date.now()}`);
|
|
3360
3534
|
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
3361
3535
|
spawnSync("git", ["add", "-A"], { env });
|
|
3362
3536
|
const treeRes = spawnSync("git", ["write-tree"], { env });
|
|
@@ -3420,7 +3594,7 @@ function applyUndo(hash, cwd) {
|
|
|
3420
3594
|
const tracked = spawnSync("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
3421
3595
|
const untracked = spawnSync("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
3422
3596
|
for (const file of [...tracked, ...untracked]) {
|
|
3423
|
-
const fullPath =
|
|
3597
|
+
const fullPath = path5.join(dir, file);
|
|
3424
3598
|
if (!snapshotFiles.has(file) && fs4.existsSync(fullPath)) {
|
|
3425
3599
|
fs4.unlinkSync(fullPath);
|
|
3426
3600
|
}
|
|
@@ -3434,7 +3608,7 @@ function applyUndo(hash, cwd) {
|
|
|
3434
3608
|
// src/cli.ts
|
|
3435
3609
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
3436
3610
|
var { version } = JSON.parse(
|
|
3437
|
-
fs5.readFileSync(
|
|
3611
|
+
fs5.readFileSync(path6.join(__dirname, "../package.json"), "utf-8")
|
|
3438
3612
|
);
|
|
3439
3613
|
function parseDuration(str) {
|
|
3440
3614
|
const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
|
|
@@ -3627,9 +3801,9 @@ async function runProxy(targetCommand) {
|
|
|
3627
3801
|
}
|
|
3628
3802
|
program.command("login").argument("<apiKey>").option("--local", "Save key for audit/logging only \u2014 local config still controls all decisions").option("--profile <name>", 'Save as a named profile (default: "default")').action((apiKey, options) => {
|
|
3629
3803
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
3630
|
-
const credPath =
|
|
3631
|
-
if (!fs5.existsSync(
|
|
3632
|
-
fs5.mkdirSync(
|
|
3804
|
+
const credPath = path6.join(os5.homedir(), ".node9", "credentials.json");
|
|
3805
|
+
if (!fs5.existsSync(path6.dirname(credPath)))
|
|
3806
|
+
fs5.mkdirSync(path6.dirname(credPath), { recursive: true });
|
|
3633
3807
|
const profileName = options.profile || "default";
|
|
3634
3808
|
let existingCreds = {};
|
|
3635
3809
|
try {
|
|
@@ -3648,7 +3822,7 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
3648
3822
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
3649
3823
|
fs5.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
3650
3824
|
if (profileName === "default") {
|
|
3651
|
-
const configPath =
|
|
3825
|
+
const configPath = path6.join(os5.homedir(), ".node9", "config.json");
|
|
3652
3826
|
let config = {};
|
|
3653
3827
|
try {
|
|
3654
3828
|
if (fs5.existsSync(configPath))
|
|
@@ -3665,8 +3839,8 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
3665
3839
|
};
|
|
3666
3840
|
approvers.cloud = !options.local;
|
|
3667
3841
|
s.approvers = approvers;
|
|
3668
|
-
if (!fs5.existsSync(
|
|
3669
|
-
fs5.mkdirSync(
|
|
3842
|
+
if (!fs5.existsSync(path6.dirname(configPath)))
|
|
3843
|
+
fs5.mkdirSync(path6.dirname(configPath), { recursive: true });
|
|
3670
3844
|
fs5.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
3671
3845
|
}
|
|
3672
3846
|
if (options.profile && profileName !== "default") {
|
|
@@ -3752,7 +3926,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3752
3926
|
);
|
|
3753
3927
|
}
|
|
3754
3928
|
section("Configuration");
|
|
3755
|
-
const globalConfigPath =
|
|
3929
|
+
const globalConfigPath = path6.join(homeDir2, ".node9", "config.json");
|
|
3756
3930
|
if (fs5.existsSync(globalConfigPath)) {
|
|
3757
3931
|
try {
|
|
3758
3932
|
JSON.parse(fs5.readFileSync(globalConfigPath, "utf-8"));
|
|
@@ -3763,7 +3937,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3763
3937
|
} else {
|
|
3764
3938
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
3765
3939
|
}
|
|
3766
|
-
const projectConfigPath =
|
|
3940
|
+
const projectConfigPath = path6.join(process.cwd(), "node9.config.json");
|
|
3767
3941
|
if (fs5.existsSync(projectConfigPath)) {
|
|
3768
3942
|
try {
|
|
3769
3943
|
JSON.parse(fs5.readFileSync(projectConfigPath, "utf-8"));
|
|
@@ -3772,7 +3946,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3772
3946
|
fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
|
|
3773
3947
|
}
|
|
3774
3948
|
}
|
|
3775
|
-
const credsPath =
|
|
3949
|
+
const credsPath = path6.join(homeDir2, ".node9", "credentials.json");
|
|
3776
3950
|
if (fs5.existsSync(credsPath)) {
|
|
3777
3951
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
3778
3952
|
} else {
|
|
@@ -3782,7 +3956,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3782
3956
|
);
|
|
3783
3957
|
}
|
|
3784
3958
|
section("Agent Hooks");
|
|
3785
|
-
const claudeSettingsPath =
|
|
3959
|
+
const claudeSettingsPath = path6.join(homeDir2, ".claude", "settings.json");
|
|
3786
3960
|
if (fs5.existsSync(claudeSettingsPath)) {
|
|
3787
3961
|
try {
|
|
3788
3962
|
const cs = JSON.parse(fs5.readFileSync(claudeSettingsPath, "utf-8"));
|
|
@@ -3798,7 +3972,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3798
3972
|
} else {
|
|
3799
3973
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
3800
3974
|
}
|
|
3801
|
-
const geminiSettingsPath =
|
|
3975
|
+
const geminiSettingsPath = path6.join(homeDir2, ".gemini", "settings.json");
|
|
3802
3976
|
if (fs5.existsSync(geminiSettingsPath)) {
|
|
3803
3977
|
try {
|
|
3804
3978
|
const gs = JSON.parse(fs5.readFileSync(geminiSettingsPath, "utf-8"));
|
|
@@ -3814,7 +3988,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3814
3988
|
} else {
|
|
3815
3989
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
3816
3990
|
}
|
|
3817
|
-
const cursorHooksPath =
|
|
3991
|
+
const cursorHooksPath = path6.join(homeDir2, ".cursor", "hooks.json");
|
|
3818
3992
|
if (fs5.existsSync(cursorHooksPath)) {
|
|
3819
3993
|
try {
|
|
3820
3994
|
const cur = JSON.parse(fs5.readFileSync(cursorHooksPath, "utf-8"));
|
|
@@ -3919,7 +4093,7 @@ program.command("explain").description(
|
|
|
3919
4093
|
console.log("");
|
|
3920
4094
|
});
|
|
3921
4095
|
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) => {
|
|
3922
|
-
const configPath =
|
|
4096
|
+
const configPath = path6.join(os5.homedir(), ".node9", "config.json");
|
|
3923
4097
|
if (fs5.existsSync(configPath) && !options.force) {
|
|
3924
4098
|
console.log(chalk5.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
3925
4099
|
console.log(chalk5.gray(` Run with --force to overwrite.`));
|
|
@@ -3934,7 +4108,7 @@ program.command("init").description("Create ~/.node9/config.json with default po
|
|
|
3934
4108
|
mode: safeMode
|
|
3935
4109
|
}
|
|
3936
4110
|
};
|
|
3937
|
-
const dir =
|
|
4111
|
+
const dir = path6.dirname(configPath);
|
|
3938
4112
|
if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
|
|
3939
4113
|
fs5.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
3940
4114
|
console.log(chalk5.green(`\u2705 Global config created: ${configPath}`));
|
|
@@ -3954,7 +4128,7 @@ function formatRelativeTime(timestamp) {
|
|
|
3954
4128
|
return new Date(timestamp).toLocaleDateString();
|
|
3955
4129
|
}
|
|
3956
4130
|
program.command("audit").description("View local execution audit log").option("--tail <n>", "Number of entries to show", "20").option("--tool <pattern>", "Filter by tool name (substring match)").option("--deny", "Show only denied actions").option("--json", "Output raw JSON").action((options) => {
|
|
3957
|
-
const logPath =
|
|
4131
|
+
const logPath = path6.join(os5.homedir(), ".node9", "audit.log");
|
|
3958
4132
|
if (!fs5.existsSync(logPath)) {
|
|
3959
4133
|
console.log(
|
|
3960
4134
|
chalk5.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
@@ -4044,8 +4218,8 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
4044
4218
|
console.log("");
|
|
4045
4219
|
const modeLabel = settings.mode === "audit" ? chalk5.blue("audit") : settings.mode === "strict" ? chalk5.red("strict") : chalk5.white("standard");
|
|
4046
4220
|
console.log(` Mode: ${modeLabel}`);
|
|
4047
|
-
const projectConfig =
|
|
4048
|
-
const globalConfig =
|
|
4221
|
+
const projectConfig = path6.join(process.cwd(), "node9.config.json");
|
|
4222
|
+
const globalConfig = path6.join(os5.homedir(), ".node9", "config.json");
|
|
4049
4223
|
console.log(
|
|
4050
4224
|
` Local: ${fs5.existsSync(projectConfig) ? chalk5.green("Active (node9.config.json)") : chalk5.gray("Not present")}`
|
|
4051
4225
|
);
|
|
@@ -4113,7 +4287,7 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
|
|
|
4113
4287
|
} catch (err) {
|
|
4114
4288
|
const tempConfig = getConfig();
|
|
4115
4289
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
4116
|
-
const logPath =
|
|
4290
|
+
const logPath = path6.join(os5.homedir(), ".node9", "hook-debug.log");
|
|
4117
4291
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4118
4292
|
fs5.appendFileSync(
|
|
4119
4293
|
logPath,
|
|
@@ -4133,9 +4307,9 @@ RAW: ${raw}
|
|
|
4133
4307
|
}
|
|
4134
4308
|
const config = getConfig();
|
|
4135
4309
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
4136
|
-
const logPath =
|
|
4137
|
-
if (!fs5.existsSync(
|
|
4138
|
-
fs5.mkdirSync(
|
|
4310
|
+
const logPath = path6.join(os5.homedir(), ".node9", "hook-debug.log");
|
|
4311
|
+
if (!fs5.existsSync(path6.dirname(logPath)))
|
|
4312
|
+
fs5.mkdirSync(path6.dirname(logPath), { recursive: true });
|
|
4139
4313
|
fs5.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
4140
4314
|
`);
|
|
4141
4315
|
}
|
|
@@ -4173,16 +4347,7 @@ RAW: ${raw}
|
|
|
4173
4347
|
return;
|
|
4174
4348
|
}
|
|
4175
4349
|
const meta = { agent, mcpServer };
|
|
4176
|
-
|
|
4177
|
-
"write_file",
|
|
4178
|
-
"edit_file",
|
|
4179
|
-
"edit",
|
|
4180
|
-
"replace",
|
|
4181
|
-
"terminal.execute",
|
|
4182
|
-
"str_replace_based_edit_tool",
|
|
4183
|
-
"create_file"
|
|
4184
|
-
];
|
|
4185
|
-
if (config.settings.enableUndo && STATE_CHANGING_TOOLS_PRE.includes(toolName.toLowerCase())) {
|
|
4350
|
+
if (shouldSnapshot(toolName, toolInput, config)) {
|
|
4186
4351
|
await createShadowSnapshot(toolName, toolInput);
|
|
4187
4352
|
}
|
|
4188
4353
|
const result = await authorizeHeadless(toolName, toolInput, false, meta);
|
|
@@ -4216,7 +4381,7 @@ RAW: ${raw}
|
|
|
4216
4381
|
});
|
|
4217
4382
|
} catch (err) {
|
|
4218
4383
|
if (process.env.NODE9_DEBUG === "1") {
|
|
4219
|
-
const logPath =
|
|
4384
|
+
const logPath = path6.join(os5.homedir(), ".node9", "hook-debug.log");
|
|
4220
4385
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4221
4386
|
fs5.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
4222
4387
|
`);
|
|
@@ -4263,20 +4428,12 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
|
|
|
4263
4428
|
decision: "allowed",
|
|
4264
4429
|
source: "post-hook"
|
|
4265
4430
|
};
|
|
4266
|
-
const logPath =
|
|
4267
|
-
if (!fs5.existsSync(
|
|
4268
|
-
fs5.mkdirSync(
|
|
4431
|
+
const logPath = path6.join(os5.homedir(), ".node9", "audit.log");
|
|
4432
|
+
if (!fs5.existsSync(path6.dirname(logPath)))
|
|
4433
|
+
fs5.mkdirSync(path6.dirname(logPath), { recursive: true });
|
|
4269
4434
|
fs5.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
4270
4435
|
const config = getConfig();
|
|
4271
|
-
|
|
4272
|
-
"bash",
|
|
4273
|
-
"shell",
|
|
4274
|
-
"write_file",
|
|
4275
|
-
"edit_file",
|
|
4276
|
-
"replace",
|
|
4277
|
-
"terminal.execute"
|
|
4278
|
-
];
|
|
4279
|
-
if (config.settings.enableUndo && STATE_CHANGING_TOOLS.includes(tool.toLowerCase())) {
|
|
4436
|
+
if (shouldSnapshot(tool, {}, config)) {
|
|
4280
4437
|
await createShadowSnapshot();
|
|
4281
4438
|
}
|
|
4282
4439
|
} catch {
|
|
@@ -4448,7 +4605,7 @@ process.on("unhandledRejection", (reason) => {
|
|
|
4448
4605
|
const isCheckHook = process.argv[2] === "check";
|
|
4449
4606
|
if (isCheckHook) {
|
|
4450
4607
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
4451
|
-
const logPath =
|
|
4608
|
+
const logPath = path6.join(os5.homedir(), ".node9", "hook-debug.log");
|
|
4452
4609
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
4453
4610
|
fs5.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
4454
4611
|
`);
|