@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.js
CHANGED
|
@@ -30,13 +30,14 @@ var import_commander = require("commander");
|
|
|
30
30
|
var import_chalk2 = __toESM(require("chalk"));
|
|
31
31
|
var import_prompts = require("@inquirer/prompts");
|
|
32
32
|
var import_fs = __toESM(require("fs"));
|
|
33
|
-
var
|
|
33
|
+
var import_path2 = __toESM(require("path"));
|
|
34
34
|
var import_os = __toESM(require("os"));
|
|
35
35
|
var import_picomatch = __toESM(require("picomatch"));
|
|
36
36
|
var import_sh_syntax = require("sh-syntax");
|
|
37
37
|
|
|
38
38
|
// src/ui/native.ts
|
|
39
39
|
var import_child_process = require("child_process");
|
|
40
|
+
var import_path = __toESM(require("path"));
|
|
40
41
|
var import_chalk = __toESM(require("chalk"));
|
|
41
42
|
var isTestEnv = () => {
|
|
42
43
|
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";
|
|
@@ -46,8 +47,29 @@ function smartTruncate(str, maxLen = 500) {
|
|
|
46
47
|
const edge = Math.floor(maxLen / 2) - 3;
|
|
47
48
|
return `${str.slice(0, edge)} ... ${str.slice(-edge)}`;
|
|
48
49
|
}
|
|
49
|
-
function
|
|
50
|
-
|
|
50
|
+
function extractContext(text, matchedWord) {
|
|
51
|
+
const lines = text.split("\n");
|
|
52
|
+
if (lines.length <= 7 || !matchedWord) return smartTruncate(text, 500);
|
|
53
|
+
const escaped = matchedWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
54
|
+
const pattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
55
|
+
const allHits = lines.map((line, i) => ({ i, line })).filter(({ line }) => pattern.test(line));
|
|
56
|
+
if (allHits.length === 0) return smartTruncate(text, 500);
|
|
57
|
+
const nonComment = allHits.find(({ line }) => {
|
|
58
|
+
const trimmed = line.trim();
|
|
59
|
+
return !trimmed.startsWith("//") && !trimmed.startsWith("#");
|
|
60
|
+
});
|
|
61
|
+
const hitIndex = (nonComment ?? allHits[0]).i;
|
|
62
|
+
const start = Math.max(0, hitIndex - 3);
|
|
63
|
+
const end = Math.min(lines.length, hitIndex + 4);
|
|
64
|
+
const snippet = lines.slice(start, end).map((line, i) => `${start + i === hitIndex ? "\u{1F6D1} " : " "}${line}`).join("\n");
|
|
65
|
+
const head = start > 0 ? `... [${start} lines hidden] ...
|
|
66
|
+
` : "";
|
|
67
|
+
const tail = end < lines.length ? `
|
|
68
|
+
... [${lines.length - end} lines hidden] ...` : "";
|
|
69
|
+
return `${head}${snippet}${tail}`;
|
|
70
|
+
}
|
|
71
|
+
function formatArgs(args, matchedField, matchedWord) {
|
|
72
|
+
if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
|
|
51
73
|
let parsed = args;
|
|
52
74
|
if (typeof args === "string") {
|
|
53
75
|
const trimmed = args.trim();
|
|
@@ -58,11 +80,39 @@ function formatArgs(args) {
|
|
|
58
80
|
parsed = args;
|
|
59
81
|
}
|
|
60
82
|
} else {
|
|
61
|
-
return smartTruncate(args, 600);
|
|
83
|
+
return { message: smartTruncate(args, 600), intent: "EXEC" };
|
|
62
84
|
}
|
|
63
85
|
}
|
|
64
86
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
65
87
|
const obj = parsed;
|
|
88
|
+
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
89
|
+
const file = obj.file_path ? import_path.default.basename(String(obj.file_path)) : "file";
|
|
90
|
+
const oldPreview = smartTruncate(String(obj.old_string), 120);
|
|
91
|
+
const newPreview = extractContext(String(obj.new_string), matchedWord);
|
|
92
|
+
return {
|
|
93
|
+
intent: "EDIT",
|
|
94
|
+
message: `\u{1F4DD} EDITING: ${file}
|
|
95
|
+
\u{1F4C2} PATH: ${obj.file_path}
|
|
96
|
+
|
|
97
|
+
--- REPLACING ---
|
|
98
|
+
${oldPreview}
|
|
99
|
+
|
|
100
|
+
+++ NEW CODE +++
|
|
101
|
+
${newPreview}`
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (matchedField && obj[matchedField] !== void 0) {
|
|
105
|
+
const otherKeys = Object.keys(obj).filter((k) => k !== matchedField);
|
|
106
|
+
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(", ")}
|
|
107
|
+
|
|
108
|
+
` : "";
|
|
109
|
+
const content = extractContext(String(obj[matchedField]), matchedWord);
|
|
110
|
+
return {
|
|
111
|
+
intent: "EXEC",
|
|
112
|
+
message: `${context}\u{1F6D1} [${matchedField.toUpperCase()}]:
|
|
113
|
+
${content}`
|
|
114
|
+
};
|
|
115
|
+
}
|
|
66
116
|
const codeKeys = [
|
|
67
117
|
"command",
|
|
68
118
|
"cmd",
|
|
@@ -83,14 +133,18 @@ function formatArgs(args) {
|
|
|
83
133
|
if (foundKey) {
|
|
84
134
|
const val = obj[foundKey];
|
|
85
135
|
const str = typeof val === "string" ? val : JSON.stringify(val);
|
|
86
|
-
return
|
|
87
|
-
|
|
136
|
+
return {
|
|
137
|
+
intent: "EXEC",
|
|
138
|
+
message: `[${foundKey.toUpperCase()}]:
|
|
139
|
+
${smartTruncate(str, 500)}`
|
|
140
|
+
};
|
|
88
141
|
}
|
|
89
|
-
|
|
142
|
+
const msg = Object.entries(obj).slice(0, 5).map(
|
|
90
143
|
([k, v]) => ` ${k}: ${smartTruncate(typeof v === "string" ? v : JSON.stringify(v), 300)}`
|
|
91
144
|
).join("\n");
|
|
145
|
+
return { intent: "EXEC", message: msg };
|
|
92
146
|
}
|
|
93
|
-
return smartTruncate(JSON.stringify(parsed), 200);
|
|
147
|
+
return { intent: "EXEC", message: smartTruncate(JSON.stringify(parsed), 200) };
|
|
94
148
|
}
|
|
95
149
|
function sendDesktopNotification(title, body) {
|
|
96
150
|
if (isTestEnv()) return;
|
|
@@ -143,10 +197,11 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
143
197
|
}
|
|
144
198
|
return lines.join("\n");
|
|
145
199
|
}
|
|
146
|
-
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
|
|
200
|
+
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord) {
|
|
147
201
|
if (isTestEnv()) return "deny";
|
|
148
|
-
const formattedArgs = formatArgs(args);
|
|
149
|
-
const
|
|
202
|
+
const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
|
|
203
|
+
const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
|
|
204
|
+
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
|
|
150
205
|
const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
|
|
151
206
|
process.stderr.write(import_chalk.default.yellow(`
|
|
152
207
|
\u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
|
|
@@ -224,10 +279,10 @@ end run`;
|
|
|
224
279
|
}
|
|
225
280
|
|
|
226
281
|
// src/core.ts
|
|
227
|
-
var PAUSED_FILE =
|
|
228
|
-
var TRUST_FILE =
|
|
229
|
-
var LOCAL_AUDIT_LOG =
|
|
230
|
-
var HOOK_DEBUG_LOG =
|
|
282
|
+
var PAUSED_FILE = import_path2.default.join(import_os.default.homedir(), ".node9", "PAUSED");
|
|
283
|
+
var TRUST_FILE = import_path2.default.join(import_os.default.homedir(), ".node9", "trust.json");
|
|
284
|
+
var LOCAL_AUDIT_LOG = import_path2.default.join(import_os.default.homedir(), ".node9", "audit.log");
|
|
285
|
+
var HOOK_DEBUG_LOG = import_path2.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
|
|
231
286
|
function checkPause() {
|
|
232
287
|
try {
|
|
233
288
|
if (!import_fs.default.existsSync(PAUSED_FILE)) return { paused: false };
|
|
@@ -245,7 +300,7 @@ function checkPause() {
|
|
|
245
300
|
}
|
|
246
301
|
}
|
|
247
302
|
function atomicWriteSync(filePath, data, options) {
|
|
248
|
-
const dir =
|
|
303
|
+
const dir = import_path2.default.dirname(filePath);
|
|
249
304
|
if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
|
|
250
305
|
const tmpPath = `${filePath}.${import_os.default.hostname()}.${process.pid}.tmp`;
|
|
251
306
|
import_fs.default.writeFileSync(tmpPath, data, options);
|
|
@@ -296,7 +351,7 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
296
351
|
}
|
|
297
352
|
function appendToLog(logPath, entry) {
|
|
298
353
|
try {
|
|
299
|
-
const dir =
|
|
354
|
+
const dir = import_path2.default.dirname(logPath);
|
|
300
355
|
if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
|
|
301
356
|
import_fs.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
302
357
|
} catch {
|
|
@@ -340,9 +395,21 @@ function matchesPattern(text, patterns) {
|
|
|
340
395
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
341
396
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
342
397
|
}
|
|
343
|
-
function getNestedValue(obj,
|
|
398
|
+
function getNestedValue(obj, path7) {
|
|
344
399
|
if (!obj || typeof obj !== "object") return null;
|
|
345
|
-
return
|
|
400
|
+
return path7.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
401
|
+
}
|
|
402
|
+
function shouldSnapshot(toolName, args, config) {
|
|
403
|
+
if (!config.settings.enableUndo) return false;
|
|
404
|
+
const snap = config.policy.snapshot;
|
|
405
|
+
if (!snap.tools.includes(toolName.toLowerCase())) return false;
|
|
406
|
+
const a = args && typeof args === "object" ? args : {};
|
|
407
|
+
const filePath = String(a.file_path ?? a.path ?? a.filename ?? "");
|
|
408
|
+
if (filePath) {
|
|
409
|
+
if (snap.ignorePaths.length && (0, import_picomatch.default)(snap.ignorePaths)(filePath)) return false;
|
|
410
|
+
if (snap.onlyPaths.length && !(0, import_picomatch.default)(snap.onlyPaths)(filePath)) return false;
|
|
411
|
+
}
|
|
412
|
+
return true;
|
|
346
413
|
}
|
|
347
414
|
function evaluateSmartConditions(args, rule) {
|
|
348
415
|
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
@@ -531,6 +598,18 @@ var DEFAULT_CONFIG = {
|
|
|
531
598
|
"terminal.execute": "command",
|
|
532
599
|
"postgres:query": "sql"
|
|
533
600
|
},
|
|
601
|
+
snapshot: {
|
|
602
|
+
tools: [
|
|
603
|
+
"str_replace_based_edit_tool",
|
|
604
|
+
"write_file",
|
|
605
|
+
"edit_file",
|
|
606
|
+
"create_file",
|
|
607
|
+
"edit",
|
|
608
|
+
"replace"
|
|
609
|
+
],
|
|
610
|
+
onlyPaths: [],
|
|
611
|
+
ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
|
|
612
|
+
},
|
|
534
613
|
rules: [
|
|
535
614
|
{
|
|
536
615
|
action: "rm",
|
|
@@ -569,7 +648,7 @@ function _resetConfigCache() {
|
|
|
569
648
|
}
|
|
570
649
|
function getGlobalSettings() {
|
|
571
650
|
try {
|
|
572
|
-
const globalConfigPath =
|
|
651
|
+
const globalConfigPath = import_path2.default.join(import_os.default.homedir(), ".node9", "config.json");
|
|
573
652
|
if (import_fs.default.existsSync(globalConfigPath)) {
|
|
574
653
|
const parsed = JSON.parse(import_fs.default.readFileSync(globalConfigPath, "utf-8"));
|
|
575
654
|
const settings = parsed.settings || {};
|
|
@@ -593,7 +672,7 @@ function getGlobalSettings() {
|
|
|
593
672
|
}
|
|
594
673
|
function getInternalToken() {
|
|
595
674
|
try {
|
|
596
|
-
const pidFile =
|
|
675
|
+
const pidFile = import_path2.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
|
|
597
676
|
if (!import_fs.default.existsSync(pidFile)) return null;
|
|
598
677
|
const data = JSON.parse(import_fs.default.readFileSync(pidFile, "utf-8"));
|
|
599
678
|
process.kill(data.pid, 0);
|
|
@@ -697,9 +776,29 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
697
776
|
})
|
|
698
777
|
);
|
|
699
778
|
if (isDangerous) {
|
|
779
|
+
let matchedField;
|
|
780
|
+
if (matchedDangerousWord && args && typeof args === "object" && !Array.isArray(args)) {
|
|
781
|
+
const obj = args;
|
|
782
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
783
|
+
if (typeof value === "string") {
|
|
784
|
+
try {
|
|
785
|
+
if (new RegExp(
|
|
786
|
+
`\\b${matchedDangerousWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
|
|
787
|
+
"i"
|
|
788
|
+
).test(value)) {
|
|
789
|
+
matchedField = key;
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
} catch {
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
700
797
|
return {
|
|
701
798
|
decision: "review",
|
|
702
|
-
blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"
|
|
799
|
+
blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`,
|
|
800
|
+
matchedWord: matchedDangerousWord,
|
|
801
|
+
matchedField
|
|
703
802
|
};
|
|
704
803
|
}
|
|
705
804
|
if (config.settings.mode === "strict") {
|
|
@@ -711,9 +810,9 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
711
810
|
}
|
|
712
811
|
async function explainPolicy(toolName, args) {
|
|
713
812
|
const steps = [];
|
|
714
|
-
const globalPath =
|
|
715
|
-
const projectPath =
|
|
716
|
-
const credsPath =
|
|
813
|
+
const globalPath = import_path2.default.join(import_os.default.homedir(), ".node9", "config.json");
|
|
814
|
+
const projectPath = import_path2.default.join(process.cwd(), "node9.config.json");
|
|
815
|
+
const credsPath = import_path2.default.join(import_os.default.homedir(), ".node9", "credentials.json");
|
|
717
816
|
const waterfall = [
|
|
718
817
|
{
|
|
719
818
|
tier: 1,
|
|
@@ -1017,7 +1116,7 @@ var DAEMON_PORT = 7391;
|
|
|
1017
1116
|
var DAEMON_HOST = "127.0.0.1";
|
|
1018
1117
|
function isDaemonRunning() {
|
|
1019
1118
|
try {
|
|
1020
|
-
const pidFile =
|
|
1119
|
+
const pidFile = import_path2.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
|
|
1021
1120
|
if (!import_fs.default.existsSync(pidFile)) return false;
|
|
1022
1121
|
const { pid, port } = JSON.parse(import_fs.default.readFileSync(pidFile, "utf-8"));
|
|
1023
1122
|
if (port !== DAEMON_PORT) return false;
|
|
@@ -1029,7 +1128,7 @@ function isDaemonRunning() {
|
|
|
1029
1128
|
}
|
|
1030
1129
|
function getPersistentDecision(toolName) {
|
|
1031
1130
|
try {
|
|
1032
|
-
const file =
|
|
1131
|
+
const file = import_path2.default.join(import_os.default.homedir(), ".node9", "decisions.json");
|
|
1033
1132
|
if (!import_fs.default.existsSync(file)) return null;
|
|
1034
1133
|
const decisions = JSON.parse(import_fs.default.readFileSync(file, "utf-8"));
|
|
1035
1134
|
const d = decisions[toolName];
|
|
@@ -1100,7 +1199,7 @@ async function resolveViaDaemon(id, decision, internalToken) {
|
|
|
1100
1199
|
signal: AbortSignal.timeout(3e3)
|
|
1101
1200
|
});
|
|
1102
1201
|
}
|
|
1103
|
-
async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta) {
|
|
1202
|
+
async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
|
|
1104
1203
|
if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
|
|
1105
1204
|
const pauseState = checkPause();
|
|
1106
1205
|
if (pauseState.paused) return { approved: true, checkedBy: "paused" };
|
|
@@ -1120,6 +1219,8 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1120
1219
|
}
|
|
1121
1220
|
const isManual = meta?.agent === "Terminal";
|
|
1122
1221
|
let explainableLabel = "Local Config";
|
|
1222
|
+
let policyMatchedField;
|
|
1223
|
+
let policyMatchedWord;
|
|
1123
1224
|
if (config.settings.mode === "audit") {
|
|
1124
1225
|
if (!isIgnoredTool(toolName)) {
|
|
1125
1226
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
@@ -1155,6 +1256,8 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1155
1256
|
};
|
|
1156
1257
|
}
|
|
1157
1258
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
1259
|
+
policyMatchedField = policyResult.matchedField;
|
|
1260
|
+
policyMatchedWord = policyResult.matchedWord;
|
|
1158
1261
|
const persistent = getPersistentDecision(toolName);
|
|
1159
1262
|
if (persistent === "allow") {
|
|
1160
1263
|
if (creds?.apiKey) auditLocalAllow(toolName, args, "persistent", creds, meta);
|
|
@@ -1247,7 +1350,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1247
1350
|
racePromises.push(
|
|
1248
1351
|
(async () => {
|
|
1249
1352
|
try {
|
|
1250
|
-
if (isDaemonRunning() && internalToken) {
|
|
1353
|
+
if (isDaemonRunning() && internalToken && !options?.calledFromDaemon) {
|
|
1251
1354
|
viewerId = await notifyDaemonViewer(toolName, args, meta).catch(() => null);
|
|
1252
1355
|
}
|
|
1253
1356
|
const cloudResult = await pollNode9SaaS(cloudRequestId, creds, signal);
|
|
@@ -1275,7 +1378,9 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1275
1378
|
meta?.agent,
|
|
1276
1379
|
explainableLabel,
|
|
1277
1380
|
isRemoteLocked,
|
|
1278
|
-
signal
|
|
1381
|
+
signal,
|
|
1382
|
+
policyMatchedField,
|
|
1383
|
+
policyMatchedWord
|
|
1279
1384
|
);
|
|
1280
1385
|
if (decision === "always_allow") {
|
|
1281
1386
|
writeTrustSession(toolName, 36e5);
|
|
@@ -1292,7 +1397,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1292
1397
|
})()
|
|
1293
1398
|
);
|
|
1294
1399
|
}
|
|
1295
|
-
if (approvers.browser && isDaemonRunning()) {
|
|
1400
|
+
if (approvers.browser && isDaemonRunning() && !options?.calledFromDaemon) {
|
|
1296
1401
|
racePromises.push(
|
|
1297
1402
|
(async () => {
|
|
1298
1403
|
try {
|
|
@@ -1428,8 +1533,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
1428
1533
|
}
|
|
1429
1534
|
function getConfig() {
|
|
1430
1535
|
if (cachedConfig) return cachedConfig;
|
|
1431
|
-
const globalPath =
|
|
1432
|
-
const projectPath =
|
|
1536
|
+
const globalPath = import_path2.default.join(import_os.default.homedir(), ".node9", "config.json");
|
|
1537
|
+
const projectPath = import_path2.default.join(process.cwd(), "node9.config.json");
|
|
1433
1538
|
const globalConfig = tryLoadConfig(globalPath);
|
|
1434
1539
|
const projectConfig = tryLoadConfig(projectPath);
|
|
1435
1540
|
const mergedSettings = {
|
|
@@ -1442,7 +1547,12 @@ function getConfig() {
|
|
|
1442
1547
|
ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
|
|
1443
1548
|
toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
|
|
1444
1549
|
rules: [...DEFAULT_CONFIG.policy.rules],
|
|
1445
|
-
smartRules: [...DEFAULT_CONFIG.policy.smartRules]
|
|
1550
|
+
smartRules: [...DEFAULT_CONFIG.policy.smartRules],
|
|
1551
|
+
snapshot: {
|
|
1552
|
+
tools: [...DEFAULT_CONFIG.policy.snapshot.tools],
|
|
1553
|
+
onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
|
|
1554
|
+
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
1555
|
+
}
|
|
1446
1556
|
};
|
|
1447
1557
|
const applyLayer = (source) => {
|
|
1448
1558
|
if (!source) return;
|
|
@@ -1454,6 +1564,7 @@ function getConfig() {
|
|
|
1454
1564
|
if (s.enableHookLogDebug !== void 0)
|
|
1455
1565
|
mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
|
|
1456
1566
|
if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
|
|
1567
|
+
if (s.approvalTimeoutMs !== void 0) mergedSettings.approvalTimeoutMs = s.approvalTimeoutMs;
|
|
1457
1568
|
if (s.environment !== void 0) mergedSettings.environment = s.environment;
|
|
1458
1569
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
1459
1570
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
@@ -1462,6 +1573,12 @@ function getConfig() {
|
|
|
1462
1573
|
mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
1463
1574
|
if (p.rules) mergedPolicy.rules.push(...p.rules);
|
|
1464
1575
|
if (p.smartRules) mergedPolicy.smartRules.push(...p.smartRules);
|
|
1576
|
+
if (p.snapshot) {
|
|
1577
|
+
const s2 = p.snapshot;
|
|
1578
|
+
if (s2.tools) mergedPolicy.snapshot.tools.push(...s2.tools);
|
|
1579
|
+
if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
|
|
1580
|
+
if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
|
|
1581
|
+
}
|
|
1465
1582
|
};
|
|
1466
1583
|
applyLayer(globalConfig);
|
|
1467
1584
|
applyLayer(projectConfig);
|
|
@@ -1469,6 +1586,9 @@ function getConfig() {
|
|
|
1469
1586
|
mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
|
|
1470
1587
|
mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
|
|
1471
1588
|
mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
|
|
1589
|
+
mergedPolicy.snapshot.tools = [...new Set(mergedPolicy.snapshot.tools)];
|
|
1590
|
+
mergedPolicy.snapshot.onlyPaths = [...new Set(mergedPolicy.snapshot.onlyPaths)];
|
|
1591
|
+
mergedPolicy.snapshot.ignorePaths = [...new Set(mergedPolicy.snapshot.ignorePaths)];
|
|
1472
1592
|
cachedConfig = {
|
|
1473
1593
|
settings: mergedSettings,
|
|
1474
1594
|
policy: mergedPolicy,
|
|
@@ -1497,7 +1617,7 @@ function getCredentials() {
|
|
|
1497
1617
|
};
|
|
1498
1618
|
}
|
|
1499
1619
|
try {
|
|
1500
|
-
const credPath =
|
|
1620
|
+
const credPath = import_path2.default.join(import_os.default.homedir(), ".node9", "credentials.json");
|
|
1501
1621
|
if (import_fs.default.existsSync(credPath)) {
|
|
1502
1622
|
const creds = JSON.parse(import_fs.default.readFileSync(credPath, "utf-8"));
|
|
1503
1623
|
const profileName = process.env.NODE9_PROFILE || "default";
|
|
@@ -1615,7 +1735,7 @@ async function resolveNode9SaaS(requestId, creds, approved) {
|
|
|
1615
1735
|
|
|
1616
1736
|
// src/setup.ts
|
|
1617
1737
|
var import_fs2 = __toESM(require("fs"));
|
|
1618
|
-
var
|
|
1738
|
+
var import_path3 = __toESM(require("path"));
|
|
1619
1739
|
var import_os2 = __toESM(require("os"));
|
|
1620
1740
|
var import_chalk3 = __toESM(require("chalk"));
|
|
1621
1741
|
var import_prompts2 = require("@inquirer/prompts");
|
|
@@ -1640,14 +1760,14 @@ function readJson(filePath) {
|
|
|
1640
1760
|
return null;
|
|
1641
1761
|
}
|
|
1642
1762
|
function writeJson(filePath, data) {
|
|
1643
|
-
const dir =
|
|
1763
|
+
const dir = import_path3.default.dirname(filePath);
|
|
1644
1764
|
if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
|
|
1645
1765
|
import_fs2.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
1646
1766
|
}
|
|
1647
1767
|
async function setupClaude() {
|
|
1648
1768
|
const homeDir2 = import_os2.default.homedir();
|
|
1649
|
-
const mcpPath =
|
|
1650
|
-
const hooksPath =
|
|
1769
|
+
const mcpPath = import_path3.default.join(homeDir2, ".claude.json");
|
|
1770
|
+
const hooksPath = import_path3.default.join(homeDir2, ".claude", "settings.json");
|
|
1651
1771
|
const claudeConfig = readJson(mcpPath) ?? {};
|
|
1652
1772
|
const settings = readJson(hooksPath) ?? {};
|
|
1653
1773
|
const servers = claudeConfig.mcpServers ?? {};
|
|
@@ -1722,7 +1842,7 @@ async function setupClaude() {
|
|
|
1722
1842
|
}
|
|
1723
1843
|
async function setupGemini() {
|
|
1724
1844
|
const homeDir2 = import_os2.default.homedir();
|
|
1725
|
-
const settingsPath =
|
|
1845
|
+
const settingsPath = import_path3.default.join(homeDir2, ".gemini", "settings.json");
|
|
1726
1846
|
const settings = readJson(settingsPath) ?? {};
|
|
1727
1847
|
const servers = settings.mcpServers ?? {};
|
|
1728
1848
|
let anythingChanged = false;
|
|
@@ -1805,8 +1925,8 @@ async function setupGemini() {
|
|
|
1805
1925
|
}
|
|
1806
1926
|
async function setupCursor() {
|
|
1807
1927
|
const homeDir2 = import_os2.default.homedir();
|
|
1808
|
-
const mcpPath =
|
|
1809
|
-
const hooksPath =
|
|
1928
|
+
const mcpPath = import_path3.default.join(homeDir2, ".cursor", "mcp.json");
|
|
1929
|
+
const hooksPath = import_path3.default.join(homeDir2, ".cursor", "hooks.json");
|
|
1810
1930
|
const mcpConfig = readJson(mcpPath) ?? {};
|
|
1811
1931
|
const hooksFile = readJson(hooksPath) ?? { version: 1 };
|
|
1812
1932
|
const servers = mcpConfig.mcpServers ?? {};
|
|
@@ -2844,7 +2964,7 @@ var UI_HTML_TEMPLATE = ui_default;
|
|
|
2844
2964
|
// src/daemon/index.ts
|
|
2845
2965
|
var import_http = __toESM(require("http"));
|
|
2846
2966
|
var import_fs3 = __toESM(require("fs"));
|
|
2847
|
-
var
|
|
2967
|
+
var import_path4 = __toESM(require("path"));
|
|
2848
2968
|
var import_os3 = __toESM(require("os"));
|
|
2849
2969
|
var import_child_process2 = require("child_process");
|
|
2850
2970
|
var import_crypto = require("crypto");
|
|
@@ -2852,14 +2972,14 @@ var import_chalk4 = __toESM(require("chalk"));
|
|
|
2852
2972
|
var DAEMON_PORT2 = 7391;
|
|
2853
2973
|
var DAEMON_HOST2 = "127.0.0.1";
|
|
2854
2974
|
var homeDir = import_os3.default.homedir();
|
|
2855
|
-
var DAEMON_PID_FILE =
|
|
2856
|
-
var DECISIONS_FILE =
|
|
2857
|
-
var GLOBAL_CONFIG_FILE =
|
|
2858
|
-
var CREDENTIALS_FILE =
|
|
2859
|
-
var AUDIT_LOG_FILE =
|
|
2860
|
-
var TRUST_FILE2 =
|
|
2975
|
+
var DAEMON_PID_FILE = import_path4.default.join(homeDir, ".node9", "daemon.pid");
|
|
2976
|
+
var DECISIONS_FILE = import_path4.default.join(homeDir, ".node9", "decisions.json");
|
|
2977
|
+
var GLOBAL_CONFIG_FILE = import_path4.default.join(homeDir, ".node9", "config.json");
|
|
2978
|
+
var CREDENTIALS_FILE = import_path4.default.join(homeDir, ".node9", "credentials.json");
|
|
2979
|
+
var AUDIT_LOG_FILE = import_path4.default.join(homeDir, ".node9", "audit.log");
|
|
2980
|
+
var TRUST_FILE2 = import_path4.default.join(homeDir, ".node9", "trust.json");
|
|
2861
2981
|
function atomicWriteSync2(filePath, data, options) {
|
|
2862
|
-
const dir =
|
|
2982
|
+
const dir = import_path4.default.dirname(filePath);
|
|
2863
2983
|
if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
|
|
2864
2984
|
const tmpPath = `${filePath}.${(0, import_crypto.randomUUID)()}.tmp`;
|
|
2865
2985
|
import_fs3.default.writeFileSync(tmpPath, data, options);
|
|
@@ -2903,7 +3023,7 @@ function appendAuditLog(data) {
|
|
|
2903
3023
|
decision: data.decision,
|
|
2904
3024
|
source: "daemon"
|
|
2905
3025
|
};
|
|
2906
|
-
const dir =
|
|
3026
|
+
const dir = import_path4.default.dirname(AUDIT_LOG_FILE);
|
|
2907
3027
|
if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
|
|
2908
3028
|
import_fs3.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
|
|
2909
3029
|
} catch {
|
|
@@ -3108,25 +3228,70 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3108
3228
|
args: e.args,
|
|
3109
3229
|
decision: "auto-deny"
|
|
3110
3230
|
});
|
|
3111
|
-
if (e.waiter) e.waiter("deny");
|
|
3112
|
-
else
|
|
3231
|
+
if (e.waiter) e.waiter("deny", "No response \u2014 auto-denied after timeout");
|
|
3232
|
+
else {
|
|
3233
|
+
e.earlyDecision = "deny";
|
|
3234
|
+
e.earlyReason = "No response \u2014 auto-denied after timeout";
|
|
3235
|
+
}
|
|
3113
3236
|
pending.delete(id);
|
|
3114
3237
|
broadcast("remove", { id });
|
|
3115
3238
|
}
|
|
3116
3239
|
}, AUTO_DENY_MS)
|
|
3117
3240
|
};
|
|
3118
3241
|
pending.set(id, entry);
|
|
3119
|
-
|
|
3120
|
-
|
|
3242
|
+
const browserEnabled = getConfig().settings.approvers?.browser !== false;
|
|
3243
|
+
if (browserEnabled) {
|
|
3244
|
+
broadcast("add", {
|
|
3245
|
+
id,
|
|
3246
|
+
toolName,
|
|
3247
|
+
args,
|
|
3248
|
+
slackDelegated: entry.slackDelegated,
|
|
3249
|
+
agent: entry.agent,
|
|
3250
|
+
mcpServer: entry.mcpServer
|
|
3251
|
+
});
|
|
3252
|
+
if (sseClients.size === 0 && !autoStarted)
|
|
3253
|
+
openBrowser(`http://127.0.0.1:${DAEMON_PORT2}/`);
|
|
3254
|
+
}
|
|
3255
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3256
|
+
res.end(JSON.stringify({ id }));
|
|
3257
|
+
authorizeHeadless(
|
|
3121
3258
|
toolName,
|
|
3122
3259
|
args,
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3260
|
+
false,
|
|
3261
|
+
{
|
|
3262
|
+
agent: typeof agent === "string" ? agent : void 0,
|
|
3263
|
+
mcpServer: typeof mcpServer === "string" ? mcpServer : void 0
|
|
3264
|
+
},
|
|
3265
|
+
{ calledFromDaemon: true }
|
|
3266
|
+
).then((result) => {
|
|
3267
|
+
const e = pending.get(id);
|
|
3268
|
+
if (!e) return;
|
|
3269
|
+
if (result.noApprovalMechanism) return;
|
|
3270
|
+
clearTimeout(e.timer);
|
|
3271
|
+
const decision = result.approved ? "allow" : "deny";
|
|
3272
|
+
appendAuditLog({ toolName: e.toolName, args: e.args, decision });
|
|
3273
|
+
if (e.waiter) {
|
|
3274
|
+
e.waiter(decision, result.reason);
|
|
3275
|
+
pending.delete(id);
|
|
3276
|
+
broadcast("remove", { id });
|
|
3277
|
+
} else {
|
|
3278
|
+
e.earlyDecision = decision;
|
|
3279
|
+
e.earlyReason = result.reason;
|
|
3280
|
+
}
|
|
3281
|
+
}).catch((err) => {
|
|
3282
|
+
const e = pending.get(id);
|
|
3283
|
+
if (!e) return;
|
|
3284
|
+
clearTimeout(e.timer);
|
|
3285
|
+
const reason = err?.reason || "No response \u2014 request timed out";
|
|
3286
|
+
if (e.waiter) e.waiter("deny", reason);
|
|
3287
|
+
else {
|
|
3288
|
+
e.earlyDecision = "deny";
|
|
3289
|
+
e.earlyReason = reason;
|
|
3290
|
+
}
|
|
3291
|
+
pending.delete(id);
|
|
3292
|
+
broadcast("remove", { id });
|
|
3126
3293
|
});
|
|
3127
|
-
|
|
3128
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3129
|
-
return res.end(JSON.stringify({ id }));
|
|
3294
|
+
return;
|
|
3130
3295
|
} catch {
|
|
3131
3296
|
res.writeHead(400).end();
|
|
3132
3297
|
}
|
|
@@ -3136,12 +3301,18 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3136
3301
|
const entry = pending.get(id);
|
|
3137
3302
|
if (!entry) return res.writeHead(404).end();
|
|
3138
3303
|
if (entry.earlyDecision) {
|
|
3304
|
+
pending.delete(id);
|
|
3305
|
+
broadcast("remove", { id });
|
|
3139
3306
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3140
|
-
|
|
3307
|
+
const body = { decision: entry.earlyDecision };
|
|
3308
|
+
if (entry.earlyReason) body.reason = entry.earlyReason;
|
|
3309
|
+
return res.end(JSON.stringify(body));
|
|
3141
3310
|
}
|
|
3142
|
-
entry.waiter = (d) => {
|
|
3311
|
+
entry.waiter = (d, reason) => {
|
|
3143
3312
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3144
|
-
|
|
3313
|
+
const body = { decision: d };
|
|
3314
|
+
if (reason) body.reason = reason;
|
|
3315
|
+
res.end(JSON.stringify(body));
|
|
3145
3316
|
};
|
|
3146
3317
|
return;
|
|
3147
3318
|
}
|
|
@@ -3151,7 +3322,7 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3151
3322
|
const id = pathname.split("/").pop();
|
|
3152
3323
|
const entry = pending.get(id);
|
|
3153
3324
|
if (!entry) return res.writeHead(404).end();
|
|
3154
|
-
const { decision, persist, trustDuration } = JSON.parse(await readBody(req));
|
|
3325
|
+
const { decision, persist, trustDuration, reason } = JSON.parse(await readBody(req));
|
|
3155
3326
|
if (decision === "trust" && trustDuration) {
|
|
3156
3327
|
const ms = TRUST_DURATIONS[trustDuration] ?? 60 * 6e4;
|
|
3157
3328
|
writeTrustEntry(entry.toolName, ms);
|
|
@@ -3176,8 +3347,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3176
3347
|
decision: resolvedDecision
|
|
3177
3348
|
});
|
|
3178
3349
|
clearTimeout(entry.timer);
|
|
3179
|
-
if (entry.waiter) entry.waiter(resolvedDecision);
|
|
3180
|
-
else
|
|
3350
|
+
if (entry.waiter) entry.waiter(resolvedDecision, reason);
|
|
3351
|
+
else {
|
|
3352
|
+
entry.earlyDecision = resolvedDecision;
|
|
3353
|
+
entry.earlyReason = reason;
|
|
3354
|
+
}
|
|
3181
3355
|
pending.delete(id);
|
|
3182
3356
|
broadcast("remove", { id });
|
|
3183
3357
|
res.writeHead(200);
|
|
@@ -3340,16 +3514,16 @@ var import_execa2 = require("execa");
|
|
|
3340
3514
|
var import_chalk5 = __toESM(require("chalk"));
|
|
3341
3515
|
var import_readline = __toESM(require("readline"));
|
|
3342
3516
|
var import_fs5 = __toESM(require("fs"));
|
|
3343
|
-
var
|
|
3517
|
+
var import_path6 = __toESM(require("path"));
|
|
3344
3518
|
var import_os5 = __toESM(require("os"));
|
|
3345
3519
|
|
|
3346
3520
|
// src/undo.ts
|
|
3347
3521
|
var import_child_process3 = require("child_process");
|
|
3348
3522
|
var import_fs4 = __toESM(require("fs"));
|
|
3349
|
-
var
|
|
3523
|
+
var import_path5 = __toESM(require("path"));
|
|
3350
3524
|
var import_os4 = __toESM(require("os"));
|
|
3351
|
-
var SNAPSHOT_STACK_PATH =
|
|
3352
|
-
var UNDO_LATEST_PATH =
|
|
3525
|
+
var SNAPSHOT_STACK_PATH = import_path5.default.join(import_os4.default.homedir(), ".node9", "snapshots.json");
|
|
3526
|
+
var UNDO_LATEST_PATH = import_path5.default.join(import_os4.default.homedir(), ".node9", "undo_latest.txt");
|
|
3353
3527
|
var MAX_SNAPSHOTS = 10;
|
|
3354
3528
|
function readStack() {
|
|
3355
3529
|
try {
|
|
@@ -3360,7 +3534,7 @@ function readStack() {
|
|
|
3360
3534
|
return [];
|
|
3361
3535
|
}
|
|
3362
3536
|
function writeStack(stack) {
|
|
3363
|
-
const dir =
|
|
3537
|
+
const dir = import_path5.default.dirname(SNAPSHOT_STACK_PATH);
|
|
3364
3538
|
if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
|
|
3365
3539
|
import_fs4.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
3366
3540
|
}
|
|
@@ -3378,8 +3552,8 @@ function buildArgsSummary(tool, args) {
|
|
|
3378
3552
|
async function createShadowSnapshot(tool = "unknown", args = {}) {
|
|
3379
3553
|
try {
|
|
3380
3554
|
const cwd = process.cwd();
|
|
3381
|
-
if (!import_fs4.default.existsSync(
|
|
3382
|
-
const tempIndex =
|
|
3555
|
+
if (!import_fs4.default.existsSync(import_path5.default.join(cwd, ".git"))) return null;
|
|
3556
|
+
const tempIndex = import_path5.default.join(cwd, ".git", `node9_index_${Date.now()}`);
|
|
3383
3557
|
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
3384
3558
|
(0, import_child_process3.spawnSync)("git", ["add", "-A"], { env });
|
|
3385
3559
|
const treeRes = (0, import_child_process3.spawnSync)("git", ["write-tree"], { env });
|
|
@@ -3443,7 +3617,7 @@ function applyUndo(hash, cwd) {
|
|
|
3443
3617
|
const tracked = (0, import_child_process3.spawnSync)("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
3444
3618
|
const untracked = (0, import_child_process3.spawnSync)("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
3445
3619
|
for (const file of [...tracked, ...untracked]) {
|
|
3446
|
-
const fullPath =
|
|
3620
|
+
const fullPath = import_path5.default.join(dir, file);
|
|
3447
3621
|
if (!snapshotFiles.has(file) && import_fs4.default.existsSync(fullPath)) {
|
|
3448
3622
|
import_fs4.default.unlinkSync(fullPath);
|
|
3449
3623
|
}
|
|
@@ -3457,7 +3631,7 @@ function applyUndo(hash, cwd) {
|
|
|
3457
3631
|
// src/cli.ts
|
|
3458
3632
|
var import_prompts3 = require("@inquirer/prompts");
|
|
3459
3633
|
var { version } = JSON.parse(
|
|
3460
|
-
import_fs5.default.readFileSync(
|
|
3634
|
+
import_fs5.default.readFileSync(import_path6.default.join(__dirname, "../package.json"), "utf-8")
|
|
3461
3635
|
);
|
|
3462
3636
|
function parseDuration(str) {
|
|
3463
3637
|
const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
|
|
@@ -3650,9 +3824,9 @@ async function runProxy(targetCommand) {
|
|
|
3650
3824
|
}
|
|
3651
3825
|
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) => {
|
|
3652
3826
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
3653
|
-
const credPath =
|
|
3654
|
-
if (!import_fs5.default.existsSync(
|
|
3655
|
-
import_fs5.default.mkdirSync(
|
|
3827
|
+
const credPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "credentials.json");
|
|
3828
|
+
if (!import_fs5.default.existsSync(import_path6.default.dirname(credPath)))
|
|
3829
|
+
import_fs5.default.mkdirSync(import_path6.default.dirname(credPath), { recursive: true });
|
|
3656
3830
|
const profileName = options.profile || "default";
|
|
3657
3831
|
let existingCreds = {};
|
|
3658
3832
|
try {
|
|
@@ -3671,7 +3845,7 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
3671
3845
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
3672
3846
|
import_fs5.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
3673
3847
|
if (profileName === "default") {
|
|
3674
|
-
const configPath =
|
|
3848
|
+
const configPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "config.json");
|
|
3675
3849
|
let config = {};
|
|
3676
3850
|
try {
|
|
3677
3851
|
if (import_fs5.default.existsSync(configPath))
|
|
@@ -3688,8 +3862,8 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
3688
3862
|
};
|
|
3689
3863
|
approvers.cloud = !options.local;
|
|
3690
3864
|
s.approvers = approvers;
|
|
3691
|
-
if (!import_fs5.default.existsSync(
|
|
3692
|
-
import_fs5.default.mkdirSync(
|
|
3865
|
+
if (!import_fs5.default.existsSync(import_path6.default.dirname(configPath)))
|
|
3866
|
+
import_fs5.default.mkdirSync(import_path6.default.dirname(configPath), { recursive: true });
|
|
3693
3867
|
import_fs5.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
3694
3868
|
}
|
|
3695
3869
|
if (options.profile && profileName !== "default") {
|
|
@@ -3775,7 +3949,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3775
3949
|
);
|
|
3776
3950
|
}
|
|
3777
3951
|
section("Configuration");
|
|
3778
|
-
const globalConfigPath =
|
|
3952
|
+
const globalConfigPath = import_path6.default.join(homeDir2, ".node9", "config.json");
|
|
3779
3953
|
if (import_fs5.default.existsSync(globalConfigPath)) {
|
|
3780
3954
|
try {
|
|
3781
3955
|
JSON.parse(import_fs5.default.readFileSync(globalConfigPath, "utf-8"));
|
|
@@ -3786,7 +3960,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3786
3960
|
} else {
|
|
3787
3961
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
3788
3962
|
}
|
|
3789
|
-
const projectConfigPath =
|
|
3963
|
+
const projectConfigPath = import_path6.default.join(process.cwd(), "node9.config.json");
|
|
3790
3964
|
if (import_fs5.default.existsSync(projectConfigPath)) {
|
|
3791
3965
|
try {
|
|
3792
3966
|
JSON.parse(import_fs5.default.readFileSync(projectConfigPath, "utf-8"));
|
|
@@ -3795,7 +3969,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3795
3969
|
fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
|
|
3796
3970
|
}
|
|
3797
3971
|
}
|
|
3798
|
-
const credsPath =
|
|
3972
|
+
const credsPath = import_path6.default.join(homeDir2, ".node9", "credentials.json");
|
|
3799
3973
|
if (import_fs5.default.existsSync(credsPath)) {
|
|
3800
3974
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
3801
3975
|
} else {
|
|
@@ -3805,7 +3979,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3805
3979
|
);
|
|
3806
3980
|
}
|
|
3807
3981
|
section("Agent Hooks");
|
|
3808
|
-
const claudeSettingsPath =
|
|
3982
|
+
const claudeSettingsPath = import_path6.default.join(homeDir2, ".claude", "settings.json");
|
|
3809
3983
|
if (import_fs5.default.existsSync(claudeSettingsPath)) {
|
|
3810
3984
|
try {
|
|
3811
3985
|
const cs = JSON.parse(import_fs5.default.readFileSync(claudeSettingsPath, "utf-8"));
|
|
@@ -3821,7 +3995,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3821
3995
|
} else {
|
|
3822
3996
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
3823
3997
|
}
|
|
3824
|
-
const geminiSettingsPath =
|
|
3998
|
+
const geminiSettingsPath = import_path6.default.join(homeDir2, ".gemini", "settings.json");
|
|
3825
3999
|
if (import_fs5.default.existsSync(geminiSettingsPath)) {
|
|
3826
4000
|
try {
|
|
3827
4001
|
const gs = JSON.parse(import_fs5.default.readFileSync(geminiSettingsPath, "utf-8"));
|
|
@@ -3837,7 +4011,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
3837
4011
|
} else {
|
|
3838
4012
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
3839
4013
|
}
|
|
3840
|
-
const cursorHooksPath =
|
|
4014
|
+
const cursorHooksPath = import_path6.default.join(homeDir2, ".cursor", "hooks.json");
|
|
3841
4015
|
if (import_fs5.default.existsSync(cursorHooksPath)) {
|
|
3842
4016
|
try {
|
|
3843
4017
|
const cur = JSON.parse(import_fs5.default.readFileSync(cursorHooksPath, "utf-8"));
|
|
@@ -3942,7 +4116,7 @@ program.command("explain").description(
|
|
|
3942
4116
|
console.log("");
|
|
3943
4117
|
});
|
|
3944
4118
|
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) => {
|
|
3945
|
-
const configPath =
|
|
4119
|
+
const configPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "config.json");
|
|
3946
4120
|
if (import_fs5.default.existsSync(configPath) && !options.force) {
|
|
3947
4121
|
console.log(import_chalk5.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
3948
4122
|
console.log(import_chalk5.default.gray(` Run with --force to overwrite.`));
|
|
@@ -3957,7 +4131,7 @@ program.command("init").description("Create ~/.node9/config.json with default po
|
|
|
3957
4131
|
mode: safeMode
|
|
3958
4132
|
}
|
|
3959
4133
|
};
|
|
3960
|
-
const dir =
|
|
4134
|
+
const dir = import_path6.default.dirname(configPath);
|
|
3961
4135
|
if (!import_fs5.default.existsSync(dir)) import_fs5.default.mkdirSync(dir, { recursive: true });
|
|
3962
4136
|
import_fs5.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
3963
4137
|
console.log(import_chalk5.default.green(`\u2705 Global config created: ${configPath}`));
|
|
@@ -3977,7 +4151,7 @@ function formatRelativeTime(timestamp) {
|
|
|
3977
4151
|
return new Date(timestamp).toLocaleDateString();
|
|
3978
4152
|
}
|
|
3979
4153
|
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) => {
|
|
3980
|
-
const logPath =
|
|
4154
|
+
const logPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "audit.log");
|
|
3981
4155
|
if (!import_fs5.default.existsSync(logPath)) {
|
|
3982
4156
|
console.log(
|
|
3983
4157
|
import_chalk5.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
@@ -4067,8 +4241,8 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
4067
4241
|
console.log("");
|
|
4068
4242
|
const modeLabel = settings.mode === "audit" ? import_chalk5.default.blue("audit") : settings.mode === "strict" ? import_chalk5.default.red("strict") : import_chalk5.default.white("standard");
|
|
4069
4243
|
console.log(` Mode: ${modeLabel}`);
|
|
4070
|
-
const projectConfig =
|
|
4071
|
-
const globalConfig =
|
|
4244
|
+
const projectConfig = import_path6.default.join(process.cwd(), "node9.config.json");
|
|
4245
|
+
const globalConfig = import_path6.default.join(import_os5.default.homedir(), ".node9", "config.json");
|
|
4072
4246
|
console.log(
|
|
4073
4247
|
` Local: ${import_fs5.default.existsSync(projectConfig) ? import_chalk5.default.green("Active (node9.config.json)") : import_chalk5.default.gray("Not present")}`
|
|
4074
4248
|
);
|
|
@@ -4136,7 +4310,7 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
|
|
|
4136
4310
|
} catch (err) {
|
|
4137
4311
|
const tempConfig = getConfig();
|
|
4138
4312
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
4139
|
-
const logPath =
|
|
4313
|
+
const logPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
|
|
4140
4314
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4141
4315
|
import_fs5.default.appendFileSync(
|
|
4142
4316
|
logPath,
|
|
@@ -4156,9 +4330,9 @@ RAW: ${raw}
|
|
|
4156
4330
|
}
|
|
4157
4331
|
const config = getConfig();
|
|
4158
4332
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
4159
|
-
const logPath =
|
|
4160
|
-
if (!import_fs5.default.existsSync(
|
|
4161
|
-
import_fs5.default.mkdirSync(
|
|
4333
|
+
const logPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
|
|
4334
|
+
if (!import_fs5.default.existsSync(import_path6.default.dirname(logPath)))
|
|
4335
|
+
import_fs5.default.mkdirSync(import_path6.default.dirname(logPath), { recursive: true });
|
|
4162
4336
|
import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
4163
4337
|
`);
|
|
4164
4338
|
}
|
|
@@ -4196,16 +4370,7 @@ RAW: ${raw}
|
|
|
4196
4370
|
return;
|
|
4197
4371
|
}
|
|
4198
4372
|
const meta = { agent, mcpServer };
|
|
4199
|
-
|
|
4200
|
-
"write_file",
|
|
4201
|
-
"edit_file",
|
|
4202
|
-
"edit",
|
|
4203
|
-
"replace",
|
|
4204
|
-
"terminal.execute",
|
|
4205
|
-
"str_replace_based_edit_tool",
|
|
4206
|
-
"create_file"
|
|
4207
|
-
];
|
|
4208
|
-
if (config.settings.enableUndo && STATE_CHANGING_TOOLS_PRE.includes(toolName.toLowerCase())) {
|
|
4373
|
+
if (shouldSnapshot(toolName, toolInput, config)) {
|
|
4209
4374
|
await createShadowSnapshot(toolName, toolInput);
|
|
4210
4375
|
}
|
|
4211
4376
|
const result = await authorizeHeadless(toolName, toolInput, false, meta);
|
|
@@ -4239,7 +4404,7 @@ RAW: ${raw}
|
|
|
4239
4404
|
});
|
|
4240
4405
|
} catch (err) {
|
|
4241
4406
|
if (process.env.NODE9_DEBUG === "1") {
|
|
4242
|
-
const logPath =
|
|
4407
|
+
const logPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
|
|
4243
4408
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4244
4409
|
import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
4245
4410
|
`);
|
|
@@ -4286,20 +4451,12 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
|
|
|
4286
4451
|
decision: "allowed",
|
|
4287
4452
|
source: "post-hook"
|
|
4288
4453
|
};
|
|
4289
|
-
const logPath =
|
|
4290
|
-
if (!import_fs5.default.existsSync(
|
|
4291
|
-
import_fs5.default.mkdirSync(
|
|
4454
|
+
const logPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "audit.log");
|
|
4455
|
+
if (!import_fs5.default.existsSync(import_path6.default.dirname(logPath)))
|
|
4456
|
+
import_fs5.default.mkdirSync(import_path6.default.dirname(logPath), { recursive: true });
|
|
4292
4457
|
import_fs5.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
4293
4458
|
const config = getConfig();
|
|
4294
|
-
|
|
4295
|
-
"bash",
|
|
4296
|
-
"shell",
|
|
4297
|
-
"write_file",
|
|
4298
|
-
"edit_file",
|
|
4299
|
-
"replace",
|
|
4300
|
-
"terminal.execute"
|
|
4301
|
-
];
|
|
4302
|
-
if (config.settings.enableUndo && STATE_CHANGING_TOOLS.includes(tool.toLowerCase())) {
|
|
4459
|
+
if (shouldSnapshot(tool, {}, config)) {
|
|
4303
4460
|
await createShadowSnapshot();
|
|
4304
4461
|
}
|
|
4305
4462
|
} catch {
|
|
@@ -4471,7 +4628,7 @@ process.on("unhandledRejection", (reason) => {
|
|
|
4471
4628
|
const isCheckHook = process.argv[2] === "check";
|
|
4472
4629
|
if (isCheckHook) {
|
|
4473
4630
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
4474
|
-
const logPath =
|
|
4631
|
+
const logPath = import_path6.default.join(import_os5.default.homedir(), ".node9", "hook-debug.log");
|
|
4475
4632
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
4476
4633
|
import_fs5.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
4477
4634
|
`);
|