@node9/proxy 1.0.18 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/cli.js +528 -238
- package/dist/cli.mjs +534 -244
- package/dist/index.js +207 -57
- package/dist/index.mjs +207 -57
- package/package.json +2 -1
package/dist/cli.mjs
CHANGED
|
@@ -360,8 +360,8 @@ function sanitizeConfig(raw) {
|
|
|
360
360
|
}
|
|
361
361
|
}
|
|
362
362
|
const lines = result.error.issues.map((issue) => {
|
|
363
|
-
const
|
|
364
|
-
return ` \u2022 ${
|
|
363
|
+
const path11 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
364
|
+
return ` \u2022 ${path11}: ${issue.message}`;
|
|
365
365
|
});
|
|
366
366
|
return {
|
|
367
367
|
sanitized,
|
|
@@ -675,6 +675,41 @@ var init_shields = __esm({
|
|
|
675
675
|
});
|
|
676
676
|
|
|
677
677
|
// src/dlp.ts
|
|
678
|
+
import fs2 from "fs";
|
|
679
|
+
import path4 from "path";
|
|
680
|
+
function scanFilePath(filePath, cwd = process.cwd()) {
|
|
681
|
+
if (!filePath) return null;
|
|
682
|
+
let resolved;
|
|
683
|
+
try {
|
|
684
|
+
const absolute = path4.resolve(cwd, filePath);
|
|
685
|
+
resolved = fs2.realpathSync.native(absolute);
|
|
686
|
+
} catch (err) {
|
|
687
|
+
const code = err.code;
|
|
688
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
689
|
+
resolved = path4.resolve(cwd, filePath);
|
|
690
|
+
} else {
|
|
691
|
+
return {
|
|
692
|
+
patternName: "Sensitive File Path",
|
|
693
|
+
fieldPath: "file_path",
|
|
694
|
+
redactedSample: filePath,
|
|
695
|
+
severity: "block"
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
const normalised = resolved.replace(/\\/g, "/");
|
|
700
|
+
for (const pattern of SENSITIVE_PATH_PATTERNS) {
|
|
701
|
+
if (pattern.test(normalised)) {
|
|
702
|
+
return {
|
|
703
|
+
patternName: "Sensitive File Path",
|
|
704
|
+
fieldPath: "file_path",
|
|
705
|
+
redactedSample: filePath,
|
|
706
|
+
// show original path in alert, not resolved
|
|
707
|
+
severity: "block"
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
678
713
|
function maskSecret(raw, pattern) {
|
|
679
714
|
const match = raw.match(pattern);
|
|
680
715
|
if (!match) return "****";
|
|
@@ -727,7 +762,7 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
|
727
762
|
}
|
|
728
763
|
return null;
|
|
729
764
|
}
|
|
730
|
-
var DLP_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
|
|
765
|
+
var DLP_PATTERNS, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
|
|
731
766
|
var init_dlp = __esm({
|
|
732
767
|
"src/dlp.ts"() {
|
|
733
768
|
"use strict";
|
|
@@ -742,8 +777,43 @@ var init_dlp = __esm({
|
|
|
742
777
|
regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
|
743
778
|
severity: "block"
|
|
744
779
|
},
|
|
780
|
+
// GCP service account JSON (detects the type field that uniquely identifies it)
|
|
781
|
+
{
|
|
782
|
+
name: "GCP Service Account",
|
|
783
|
+
regex: /"type"\s*:\s*"service_account"/,
|
|
784
|
+
severity: "block"
|
|
785
|
+
},
|
|
786
|
+
// NPM auth token in .npmrc format
|
|
787
|
+
{
|
|
788
|
+
name: "NPM Auth Token",
|
|
789
|
+
regex: /_authToken\s*=\s*[A-Za-z0-9_\-]{20,}/,
|
|
790
|
+
severity: "block"
|
|
791
|
+
},
|
|
745
792
|
{ name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
|
|
746
793
|
];
|
|
794
|
+
SENSITIVE_PATH_PATTERNS = [
|
|
795
|
+
/[/\\]\.ssh[/\\]/i,
|
|
796
|
+
/[/\\]\.aws[/\\]/i,
|
|
797
|
+
/[/\\]\.config[/\\]gcloud[/\\]/i,
|
|
798
|
+
/[/\\]\.azure[/\\]/i,
|
|
799
|
+
/[/\\]\.kube[/\\]config$/i,
|
|
800
|
+
/[/\\]\.env($|\.)/i,
|
|
801
|
+
// .env, .env.local, .env.production — not .envoy
|
|
802
|
+
/[/\\]\.git-credentials$/i,
|
|
803
|
+
/[/\\]\.npmrc$/i,
|
|
804
|
+
/[/\\]\.docker[/\\]config\.json$/i,
|
|
805
|
+
/[/\\][^/\\]+\.pem$/i,
|
|
806
|
+
/[/\\][^/\\]+\.key$/i,
|
|
807
|
+
/[/\\][^/\\]+\.p12$/i,
|
|
808
|
+
/[/\\][^/\\]+\.pfx$/i,
|
|
809
|
+
/^\/etc\/passwd$/,
|
|
810
|
+
/^\/etc\/shadow$/,
|
|
811
|
+
/^\/etc\/sudoers$/,
|
|
812
|
+
/[/\\]credentials\.json$/i,
|
|
813
|
+
/[/\\]id_rsa$/i,
|
|
814
|
+
/[/\\]id_ed25519$/i,
|
|
815
|
+
/[/\\]id_ecdsa$/i
|
|
816
|
+
];
|
|
747
817
|
MAX_DEPTH = 5;
|
|
748
818
|
MAX_STRING_BYTES = 1e5;
|
|
749
819
|
MAX_JSON_PARSE_BYTES = 1e4;
|
|
@@ -753,21 +823,22 @@ var init_dlp = __esm({
|
|
|
753
823
|
// src/core.ts
|
|
754
824
|
import chalk2 from "chalk";
|
|
755
825
|
import { confirm } from "@inquirer/prompts";
|
|
756
|
-
import
|
|
757
|
-
import
|
|
826
|
+
import fs3 from "fs";
|
|
827
|
+
import path5 from "path";
|
|
758
828
|
import os2 from "os";
|
|
759
829
|
import net from "net";
|
|
760
830
|
import { randomUUID } from "crypto";
|
|
761
831
|
import { spawnSync } from "child_process";
|
|
762
832
|
import pm from "picomatch";
|
|
833
|
+
import safeRegex from "safe-regex2";
|
|
763
834
|
import { parse } from "sh-syntax";
|
|
764
835
|
function checkPause() {
|
|
765
836
|
try {
|
|
766
|
-
if (!
|
|
767
|
-
const state = JSON.parse(
|
|
837
|
+
if (!fs3.existsSync(PAUSED_FILE)) return { paused: false };
|
|
838
|
+
const state = JSON.parse(fs3.readFileSync(PAUSED_FILE, "utf-8"));
|
|
768
839
|
if (state.expiry > 0 && Date.now() >= state.expiry) {
|
|
769
840
|
try {
|
|
770
|
-
|
|
841
|
+
fs3.unlinkSync(PAUSED_FILE);
|
|
771
842
|
} catch {
|
|
772
843
|
}
|
|
773
844
|
return { paused: false };
|
|
@@ -778,11 +849,11 @@ function checkPause() {
|
|
|
778
849
|
}
|
|
779
850
|
}
|
|
780
851
|
function atomicWriteSync(filePath, data, options) {
|
|
781
|
-
const dir =
|
|
782
|
-
if (!
|
|
852
|
+
const dir = path5.dirname(filePath);
|
|
853
|
+
if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
|
|
783
854
|
const tmpPath = `${filePath}.${os2.hostname()}.${process.pid}.tmp`;
|
|
784
|
-
|
|
785
|
-
|
|
855
|
+
fs3.writeFileSync(tmpPath, data, options);
|
|
856
|
+
fs3.renameSync(tmpPath, filePath);
|
|
786
857
|
}
|
|
787
858
|
function pauseNode9(durationMs, durationStr) {
|
|
788
859
|
const state = { expiry: Date.now() + durationMs, duration: durationStr };
|
|
@@ -790,18 +861,88 @@ function pauseNode9(durationMs, durationStr) {
|
|
|
790
861
|
}
|
|
791
862
|
function resumeNode9() {
|
|
792
863
|
try {
|
|
793
|
-
if (
|
|
864
|
+
if (fs3.existsSync(PAUSED_FILE)) fs3.unlinkSync(PAUSED_FILE);
|
|
794
865
|
} catch {
|
|
795
866
|
}
|
|
796
867
|
}
|
|
868
|
+
function validateRegex(pattern) {
|
|
869
|
+
if (!pattern) return "Pattern is required";
|
|
870
|
+
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
871
|
+
let parens = 0, brackets = 0, isEscaped = false, inCharClass = false;
|
|
872
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
873
|
+
const char = pattern[i];
|
|
874
|
+
if (isEscaped) {
|
|
875
|
+
isEscaped = false;
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
if (char === "\\") {
|
|
879
|
+
isEscaped = true;
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
if (char === "[" && !inCharClass) {
|
|
883
|
+
inCharClass = true;
|
|
884
|
+
brackets++;
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
if (char === "]" && inCharClass) {
|
|
888
|
+
inCharClass = false;
|
|
889
|
+
brackets--;
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
if (inCharClass) continue;
|
|
893
|
+
if (char === "(") parens++;
|
|
894
|
+
else if (char === ")") parens--;
|
|
895
|
+
}
|
|
896
|
+
if (parens !== 0) return "Unbalanced parentheses";
|
|
897
|
+
if (brackets !== 0) return "Unbalanced brackets";
|
|
898
|
+
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
899
|
+
if (!safeRegex(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
900
|
+
try {
|
|
901
|
+
new RegExp(pattern);
|
|
902
|
+
} catch (e) {
|
|
903
|
+
return `Invalid regex syntax: ${e.message}`;
|
|
904
|
+
}
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
function getCompiledRegex(pattern, flags = "") {
|
|
908
|
+
if (flags && !/^[gimsuy]+$/.test(flags)) {
|
|
909
|
+
if (process.env.NODE9_DEBUG === "1") console.error(`[Node9] Invalid regex flags: "${flags}"`);
|
|
910
|
+
return null;
|
|
911
|
+
}
|
|
912
|
+
const key = `${pattern}\0${flags}`;
|
|
913
|
+
if (regexCache.has(key)) {
|
|
914
|
+
const cached = regexCache.get(key);
|
|
915
|
+
regexCache.delete(key);
|
|
916
|
+
regexCache.set(key, cached);
|
|
917
|
+
return cached;
|
|
918
|
+
}
|
|
919
|
+
const err = validateRegex(pattern);
|
|
920
|
+
if (err) {
|
|
921
|
+
if (process.env.NODE9_DEBUG === "1")
|
|
922
|
+
console.error(`[Node9] Regex blocked: ${err} \u2014 pattern: "${pattern}"`);
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
try {
|
|
926
|
+
const re = new RegExp(pattern, flags);
|
|
927
|
+
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
928
|
+
const oldest = regexCache.keys().next().value;
|
|
929
|
+
if (oldest) regexCache.delete(oldest);
|
|
930
|
+
}
|
|
931
|
+
regexCache.set(key, re);
|
|
932
|
+
return re;
|
|
933
|
+
} catch (e) {
|
|
934
|
+
if (process.env.NODE9_DEBUG === "1") console.error(`[Node9] Regex compile failed:`, e);
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
797
938
|
function getActiveTrustSession(toolName) {
|
|
798
939
|
try {
|
|
799
|
-
if (!
|
|
800
|
-
const trust = JSON.parse(
|
|
940
|
+
if (!fs3.existsSync(TRUST_FILE)) return false;
|
|
941
|
+
const trust = JSON.parse(fs3.readFileSync(TRUST_FILE, "utf-8"));
|
|
801
942
|
const now = Date.now();
|
|
802
943
|
const active = trust.entries.filter((e) => e.expiry > now);
|
|
803
944
|
if (active.length !== trust.entries.length) {
|
|
804
|
-
|
|
945
|
+
fs3.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
|
|
805
946
|
}
|
|
806
947
|
return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
|
|
807
948
|
} catch {
|
|
@@ -812,8 +953,8 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
812
953
|
try {
|
|
813
954
|
let trust = { entries: [] };
|
|
814
955
|
try {
|
|
815
|
-
if (
|
|
816
|
-
trust = JSON.parse(
|
|
956
|
+
if (fs3.existsSync(TRUST_FILE)) {
|
|
957
|
+
trust = JSON.parse(fs3.readFileSync(TRUST_FILE, "utf-8"));
|
|
817
958
|
}
|
|
818
959
|
} catch {
|
|
819
960
|
}
|
|
@@ -829,9 +970,9 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
829
970
|
}
|
|
830
971
|
function appendToLog(logPath, entry) {
|
|
831
972
|
try {
|
|
832
|
-
const dir =
|
|
833
|
-
if (!
|
|
834
|
-
|
|
973
|
+
const dir = path5.dirname(logPath);
|
|
974
|
+
if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
|
|
975
|
+
fs3.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
835
976
|
} catch {
|
|
836
977
|
}
|
|
837
978
|
}
|
|
@@ -873,9 +1014,9 @@ function matchesPattern(text, patterns) {
|
|
|
873
1014
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
874
1015
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
875
1016
|
}
|
|
876
|
-
function getNestedValue(obj,
|
|
1017
|
+
function getNestedValue(obj, path11) {
|
|
877
1018
|
if (!obj || typeof obj !== "object") return null;
|
|
878
|
-
return
|
|
1019
|
+
return path11.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
879
1020
|
}
|
|
880
1021
|
function shouldSnapshot(toolName, args, config) {
|
|
881
1022
|
if (!config.settings.enableUndo) return false;
|
|
@@ -906,19 +1047,16 @@ function evaluateSmartConditions(args, rule) {
|
|
|
906
1047
|
return val !== null && cond.value ? !val.includes(cond.value) : true;
|
|
907
1048
|
case "matches": {
|
|
908
1049
|
if (val === null || !cond.value) return false;
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
return false;
|
|
913
|
-
}
|
|
1050
|
+
const reM = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
1051
|
+
if (!reM) return false;
|
|
1052
|
+
return reM.test(val);
|
|
914
1053
|
}
|
|
915
1054
|
case "notMatches": {
|
|
916
|
-
if (
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
}
|
|
1055
|
+
if (!cond.value) return false;
|
|
1056
|
+
if (val === null) return true;
|
|
1057
|
+
const reN = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
1058
|
+
if (!reN) return false;
|
|
1059
|
+
return !reN.test(val);
|
|
922
1060
|
}
|
|
923
1061
|
case "matchesGlob":
|
|
924
1062
|
return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
|
|
@@ -1031,9 +1169,9 @@ function _resetConfigCache() {
|
|
|
1031
1169
|
}
|
|
1032
1170
|
function getGlobalSettings() {
|
|
1033
1171
|
try {
|
|
1034
|
-
const globalConfigPath =
|
|
1035
|
-
if (
|
|
1036
|
-
const parsed = JSON.parse(
|
|
1172
|
+
const globalConfigPath = path5.join(os2.homedir(), ".node9", "config.json");
|
|
1173
|
+
if (fs3.existsSync(globalConfigPath)) {
|
|
1174
|
+
const parsed = JSON.parse(fs3.readFileSync(globalConfigPath, "utf-8"));
|
|
1037
1175
|
const settings = parsed.settings || {};
|
|
1038
1176
|
return {
|
|
1039
1177
|
mode: settings.mode || "audit",
|
|
@@ -1055,9 +1193,9 @@ function getGlobalSettings() {
|
|
|
1055
1193
|
}
|
|
1056
1194
|
function getInternalToken() {
|
|
1057
1195
|
try {
|
|
1058
|
-
const pidFile =
|
|
1059
|
-
if (!
|
|
1060
|
-
const data = JSON.parse(
|
|
1196
|
+
const pidFile = path5.join(os2.homedir(), ".node9", "daemon.pid");
|
|
1197
|
+
if (!fs3.existsSync(pidFile)) return null;
|
|
1198
|
+
const data = JSON.parse(fs3.readFileSync(pidFile, "utf-8"));
|
|
1061
1199
|
process.kill(data.pid, 0);
|
|
1062
1200
|
return data.internalToken ?? null;
|
|
1063
1201
|
} catch {
|
|
@@ -1172,9 +1310,9 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
1172
1310
|
}
|
|
1173
1311
|
async function explainPolicy(toolName, args) {
|
|
1174
1312
|
const steps = [];
|
|
1175
|
-
const globalPath =
|
|
1176
|
-
const projectPath =
|
|
1177
|
-
const credsPath =
|
|
1313
|
+
const globalPath = path5.join(os2.homedir(), ".node9", "config.json");
|
|
1314
|
+
const projectPath = path5.join(process.cwd(), "node9.config.json");
|
|
1315
|
+
const credsPath = path5.join(os2.homedir(), ".node9", "credentials.json");
|
|
1178
1316
|
const waterfall = [
|
|
1179
1317
|
{
|
|
1180
1318
|
tier: 1,
|
|
@@ -1185,19 +1323,19 @@ async function explainPolicy(toolName, args) {
|
|
|
1185
1323
|
{
|
|
1186
1324
|
tier: 2,
|
|
1187
1325
|
label: "Cloud policy",
|
|
1188
|
-
status:
|
|
1189
|
-
note:
|
|
1326
|
+
status: fs3.existsSync(credsPath) ? "active" : "missing",
|
|
1327
|
+
note: fs3.existsSync(credsPath) ? "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
|
|
1190
1328
|
},
|
|
1191
1329
|
{
|
|
1192
1330
|
tier: 3,
|
|
1193
1331
|
label: "Project config",
|
|
1194
|
-
status:
|
|
1332
|
+
status: fs3.existsSync(projectPath) ? "active" : "missing",
|
|
1195
1333
|
path: projectPath
|
|
1196
1334
|
},
|
|
1197
1335
|
{
|
|
1198
1336
|
tier: 4,
|
|
1199
1337
|
label: "Global config",
|
|
1200
|
-
status:
|
|
1338
|
+
status: fs3.existsSync(globalPath) ? "active" : "missing",
|
|
1201
1339
|
path: globalPath
|
|
1202
1340
|
},
|
|
1203
1341
|
{
|
|
@@ -1210,7 +1348,9 @@ async function explainPolicy(toolName, args) {
|
|
|
1210
1348
|
const config = getConfig();
|
|
1211
1349
|
const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
|
|
1212
1350
|
if (config.policy.dlp.enabled && (!wouldBeIgnored || config.policy.dlp.scanIgnoredTools)) {
|
|
1213
|
-
const
|
|
1351
|
+
const argsObjE = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
1352
|
+
const filePathE = String(argsObjE.file_path ?? argsObjE.path ?? argsObjE.filename ?? "");
|
|
1353
|
+
const dlpMatch = (filePathE ? scanFilePath(filePathE) : null) ?? (args !== void 0 ? scanArgs(args) : null);
|
|
1214
1354
|
if (dlpMatch) {
|
|
1215
1355
|
steps.push({
|
|
1216
1356
|
name: "DLP Content Scanner",
|
|
@@ -1433,10 +1573,10 @@ function isIgnoredTool(toolName) {
|
|
|
1433
1573
|
return matchesPattern(toolName, config.policy.ignoredTools);
|
|
1434
1574
|
}
|
|
1435
1575
|
function isDaemonRunning() {
|
|
1436
|
-
const pidFile =
|
|
1437
|
-
if (
|
|
1576
|
+
const pidFile = path5.join(os2.homedir(), ".node9", "daemon.pid");
|
|
1577
|
+
if (fs3.existsSync(pidFile)) {
|
|
1438
1578
|
try {
|
|
1439
|
-
const { pid, port } = JSON.parse(
|
|
1579
|
+
const { pid, port } = JSON.parse(fs3.readFileSync(pidFile, "utf-8"));
|
|
1440
1580
|
if (port !== DAEMON_PORT) return false;
|
|
1441
1581
|
process.kill(pid, 0);
|
|
1442
1582
|
return true;
|
|
@@ -1456,9 +1596,9 @@ function isDaemonRunning() {
|
|
|
1456
1596
|
}
|
|
1457
1597
|
function getPersistentDecision(toolName) {
|
|
1458
1598
|
try {
|
|
1459
|
-
const file =
|
|
1460
|
-
if (!
|
|
1461
|
-
const decisions = JSON.parse(
|
|
1599
|
+
const file = path5.join(os2.homedir(), ".node9", "decisions.json");
|
|
1600
|
+
if (!fs3.existsSync(file)) return null;
|
|
1601
|
+
const decisions = JSON.parse(fs3.readFileSync(file, "utf-8"));
|
|
1462
1602
|
const d = decisions[toolName];
|
|
1463
1603
|
if (d === "allow" || d === "deny") return d;
|
|
1464
1604
|
} catch {
|
|
@@ -1601,7 +1741,9 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
|
|
|
1601
1741
|
let policyMatchedWord;
|
|
1602
1742
|
let riskMetadata;
|
|
1603
1743
|
if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
|
|
1604
|
-
const
|
|
1744
|
+
const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
1745
|
+
const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
|
|
1746
|
+
const dlpMatch = (filePath ? scanFilePath(filePath) : null) ?? scanArgs(args);
|
|
1605
1747
|
if (dlpMatch) {
|
|
1606
1748
|
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
1607
1749
|
if (dlpMatch.severity === "block") {
|
|
@@ -1975,8 +2117,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
1975
2117
|
}
|
|
1976
2118
|
function getConfig() {
|
|
1977
2119
|
if (cachedConfig) return cachedConfig;
|
|
1978
|
-
const globalPath =
|
|
1979
|
-
const projectPath =
|
|
2120
|
+
const globalPath = path5.join(os2.homedir(), ".node9", "config.json");
|
|
2121
|
+
const projectPath = path5.join(process.cwd(), "node9.config.json");
|
|
1980
2122
|
const globalConfig = tryLoadConfig(globalPath);
|
|
1981
2123
|
const projectConfig = tryLoadConfig(projectPath);
|
|
1982
2124
|
const mergedSettings = {
|
|
@@ -2071,10 +2213,10 @@ function getConfig() {
|
|
|
2071
2213
|
return cachedConfig;
|
|
2072
2214
|
}
|
|
2073
2215
|
function tryLoadConfig(filePath) {
|
|
2074
|
-
if (!
|
|
2216
|
+
if (!fs3.existsSync(filePath)) return null;
|
|
2075
2217
|
let raw;
|
|
2076
2218
|
try {
|
|
2077
|
-
raw = JSON.parse(
|
|
2219
|
+
raw = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
|
|
2078
2220
|
} catch (err) {
|
|
2079
2221
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2080
2222
|
process.stderr.write(
|
|
@@ -2136,9 +2278,9 @@ function getCredentials() {
|
|
|
2136
2278
|
};
|
|
2137
2279
|
}
|
|
2138
2280
|
try {
|
|
2139
|
-
const credPath =
|
|
2140
|
-
if (
|
|
2141
|
-
const creds = JSON.parse(
|
|
2281
|
+
const credPath = path5.join(os2.homedir(), ".node9", "credentials.json");
|
|
2282
|
+
if (fs3.existsSync(credPath)) {
|
|
2283
|
+
const creds = JSON.parse(fs3.readFileSync(credPath, "utf-8"));
|
|
2142
2284
|
const profileName = process.env.NODE9_PROFILE || "default";
|
|
2143
2285
|
const profile = creds[profileName];
|
|
2144
2286
|
if (profile?.apiKey) {
|
|
@@ -2251,7 +2393,7 @@ async function resolveNode9SaaS(requestId, creds, approved) {
|
|
|
2251
2393
|
} catch {
|
|
2252
2394
|
}
|
|
2253
2395
|
}
|
|
2254
|
-
var PAUSED_FILE, TRUST_FILE, LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG, SQL_DML_KEYWORDS, DANGEROUS_WORDS, DEFAULT_CONFIG, ADVISORY_SMART_RULES, cachedConfig, DAEMON_PORT, DAEMON_HOST, ACTIVITY_SOCKET_PATH;
|
|
2396
|
+
var PAUSED_FILE, TRUST_FILE, LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, SQL_DML_KEYWORDS, DANGEROUS_WORDS, DEFAULT_CONFIG, ADVISORY_SMART_RULES, cachedConfig, DAEMON_PORT, DAEMON_HOST, ACTIVITY_SOCKET_PATH;
|
|
2255
2397
|
var init_core = __esm({
|
|
2256
2398
|
"src/core.ts"() {
|
|
2257
2399
|
"use strict";
|
|
@@ -2260,10 +2402,13 @@ var init_core = __esm({
|
|
|
2260
2402
|
init_config_schema();
|
|
2261
2403
|
init_shields();
|
|
2262
2404
|
init_dlp();
|
|
2263
|
-
PAUSED_FILE =
|
|
2264
|
-
TRUST_FILE =
|
|
2265
|
-
LOCAL_AUDIT_LOG =
|
|
2266
|
-
HOOK_DEBUG_LOG =
|
|
2405
|
+
PAUSED_FILE = path5.join(os2.homedir(), ".node9", "PAUSED");
|
|
2406
|
+
TRUST_FILE = path5.join(os2.homedir(), ".node9", "trust.json");
|
|
2407
|
+
LOCAL_AUDIT_LOG = path5.join(os2.homedir(), ".node9", "audit.log");
|
|
2408
|
+
HOOK_DEBUG_LOG = path5.join(os2.homedir(), ".node9", "hook-debug.log");
|
|
2409
|
+
MAX_REGEX_LENGTH = 100;
|
|
2410
|
+
REGEX_CACHE_MAX = 500;
|
|
2411
|
+
regexCache = /* @__PURE__ */ new Map();
|
|
2267
2412
|
SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
2268
2413
|
DANGEROUS_WORDS = [
|
|
2269
2414
|
"mkfs",
|
|
@@ -2383,7 +2528,7 @@ var init_core = __esm({
|
|
|
2383
2528
|
{
|
|
2384
2529
|
field: "command",
|
|
2385
2530
|
op: "matches",
|
|
2386
|
-
value: "git
|
|
2531
|
+
value: "^\\s*git\\b.*\\bpush\\b.*(--force|--force-with-lease|-f\\b)",
|
|
2387
2532
|
flags: "i"
|
|
2388
2533
|
}
|
|
2389
2534
|
],
|
|
@@ -2394,7 +2539,14 @@ var init_core = __esm({
|
|
|
2394
2539
|
{
|
|
2395
2540
|
name: "review-git-push",
|
|
2396
2541
|
tool: "bash",
|
|
2397
|
-
conditions: [
|
|
2542
|
+
conditions: [
|
|
2543
|
+
{
|
|
2544
|
+
field: "command",
|
|
2545
|
+
op: "matches",
|
|
2546
|
+
value: "^\\s*git\\b.*\\bpush\\b(?!.*(-f\\b|--force|--force-with-lease))",
|
|
2547
|
+
flags: "i"
|
|
2548
|
+
}
|
|
2549
|
+
],
|
|
2398
2550
|
conditionMode: "all",
|
|
2399
2551
|
verdict: "review",
|
|
2400
2552
|
reason: "git push sends changes to a shared remote"
|
|
@@ -2406,7 +2558,7 @@ var init_core = __esm({
|
|
|
2406
2558
|
{
|
|
2407
2559
|
field: "command",
|
|
2408
2560
|
op: "matches",
|
|
2409
|
-
value: "git\\
|
|
2561
|
+
value: "^\\s*git\\b.*(reset\\s+--hard|clean\\s+-[fdxX]|\\brebase\\b|tag\\s+-d|branch\\s+-[dD])",
|
|
2410
2562
|
flags: "i"
|
|
2411
2563
|
}
|
|
2412
2564
|
],
|
|
@@ -2471,7 +2623,7 @@ var init_core = __esm({
|
|
|
2471
2623
|
cachedConfig = null;
|
|
2472
2624
|
DAEMON_PORT = 7391;
|
|
2473
2625
|
DAEMON_HOST = "127.0.0.1";
|
|
2474
|
-
ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
2626
|
+
ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path5.join(os2.tmpdir(), "node9-activity.sock");
|
|
2475
2627
|
}
|
|
2476
2628
|
});
|
|
2477
2629
|
|
|
@@ -3930,25 +4082,25 @@ var init_ui2 = __esm({
|
|
|
3930
4082
|
// src/daemon/index.ts
|
|
3931
4083
|
import http from "http";
|
|
3932
4084
|
import net2 from "net";
|
|
3933
|
-
import
|
|
3934
|
-
import
|
|
4085
|
+
import fs5 from "fs";
|
|
4086
|
+
import path7 from "path";
|
|
3935
4087
|
import os4 from "os";
|
|
3936
4088
|
import { spawn as spawn2, spawnSync as spawnSync2 } from "child_process";
|
|
3937
4089
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3938
4090
|
import chalk4 from "chalk";
|
|
3939
4091
|
function atomicWriteSync2(filePath, data, options) {
|
|
3940
|
-
const dir =
|
|
3941
|
-
if (!
|
|
4092
|
+
const dir = path7.dirname(filePath);
|
|
4093
|
+
if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
|
|
3942
4094
|
const tmpPath = `${filePath}.${randomUUID2()}.tmp`;
|
|
3943
|
-
|
|
3944
|
-
|
|
4095
|
+
fs5.writeFileSync(tmpPath, data, options);
|
|
4096
|
+
fs5.renameSync(tmpPath, filePath);
|
|
3945
4097
|
}
|
|
3946
4098
|
function writeTrustEntry(toolName, durationMs) {
|
|
3947
4099
|
try {
|
|
3948
4100
|
let trust = { entries: [] };
|
|
3949
4101
|
try {
|
|
3950
|
-
if (
|
|
3951
|
-
trust = JSON.parse(
|
|
4102
|
+
if (fs5.existsSync(TRUST_FILE2))
|
|
4103
|
+
trust = JSON.parse(fs5.readFileSync(TRUST_FILE2, "utf-8"));
|
|
3952
4104
|
} catch {
|
|
3953
4105
|
}
|
|
3954
4106
|
trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
|
|
@@ -3975,16 +4127,16 @@ function appendAuditLog(data) {
|
|
|
3975
4127
|
decision: data.decision,
|
|
3976
4128
|
source: "daemon"
|
|
3977
4129
|
};
|
|
3978
|
-
const dir =
|
|
3979
|
-
if (!
|
|
3980
|
-
|
|
4130
|
+
const dir = path7.dirname(AUDIT_LOG_FILE);
|
|
4131
|
+
if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
|
|
4132
|
+
fs5.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
|
|
3981
4133
|
} catch {
|
|
3982
4134
|
}
|
|
3983
4135
|
}
|
|
3984
4136
|
function getAuditHistory(limit = 20) {
|
|
3985
4137
|
try {
|
|
3986
|
-
if (!
|
|
3987
|
-
const lines =
|
|
4138
|
+
if (!fs5.existsSync(AUDIT_LOG_FILE)) return [];
|
|
4139
|
+
const lines = fs5.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
|
|
3988
4140
|
if (lines.length === 1 && lines[0] === "") return [];
|
|
3989
4141
|
return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
|
|
3990
4142
|
} catch {
|
|
@@ -3993,7 +4145,7 @@ function getAuditHistory(limit = 20) {
|
|
|
3993
4145
|
}
|
|
3994
4146
|
function getOrgName() {
|
|
3995
4147
|
try {
|
|
3996
|
-
if (
|
|
4148
|
+
if (fs5.existsSync(CREDENTIALS_FILE)) {
|
|
3997
4149
|
return "Node9 Cloud";
|
|
3998
4150
|
}
|
|
3999
4151
|
} catch {
|
|
@@ -4001,13 +4153,13 @@ function getOrgName() {
|
|
|
4001
4153
|
return null;
|
|
4002
4154
|
}
|
|
4003
4155
|
function hasStoredSlackKey() {
|
|
4004
|
-
return
|
|
4156
|
+
return fs5.existsSync(CREDENTIALS_FILE);
|
|
4005
4157
|
}
|
|
4006
4158
|
function writeGlobalSetting(key, value) {
|
|
4007
4159
|
let config = {};
|
|
4008
4160
|
try {
|
|
4009
|
-
if (
|
|
4010
|
-
config = JSON.parse(
|
|
4161
|
+
if (fs5.existsSync(GLOBAL_CONFIG_FILE)) {
|
|
4162
|
+
config = JSON.parse(fs5.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
|
|
4011
4163
|
}
|
|
4012
4164
|
} catch {
|
|
4013
4165
|
}
|
|
@@ -4026,7 +4178,7 @@ function abandonPending() {
|
|
|
4026
4178
|
});
|
|
4027
4179
|
if (autoStarted) {
|
|
4028
4180
|
try {
|
|
4029
|
-
|
|
4181
|
+
fs5.unlinkSync(DAEMON_PID_FILE);
|
|
4030
4182
|
} catch {
|
|
4031
4183
|
}
|
|
4032
4184
|
setTimeout(() => {
|
|
@@ -4076,8 +4228,8 @@ function readBody(req) {
|
|
|
4076
4228
|
}
|
|
4077
4229
|
function readPersistentDecisions() {
|
|
4078
4230
|
try {
|
|
4079
|
-
if (
|
|
4080
|
-
return JSON.parse(
|
|
4231
|
+
if (fs5.existsSync(DECISIONS_FILE)) {
|
|
4232
|
+
return JSON.parse(fs5.readFileSync(DECISIONS_FILE, "utf-8"));
|
|
4081
4233
|
}
|
|
4082
4234
|
} catch {
|
|
4083
4235
|
}
|
|
@@ -4106,7 +4258,7 @@ function startDaemon() {
|
|
|
4106
4258
|
idleTimer = setTimeout(() => {
|
|
4107
4259
|
if (autoStarted) {
|
|
4108
4260
|
try {
|
|
4109
|
-
|
|
4261
|
+
fs5.unlinkSync(DAEMON_PID_FILE);
|
|
4110
4262
|
} catch {
|
|
4111
4263
|
}
|
|
4112
4264
|
}
|
|
@@ -4491,14 +4643,14 @@ data: ${JSON.stringify(item.data)}
|
|
|
4491
4643
|
server.on("error", (e) => {
|
|
4492
4644
|
if (e.code === "EADDRINUSE") {
|
|
4493
4645
|
try {
|
|
4494
|
-
if (
|
|
4495
|
-
const { pid } = JSON.parse(
|
|
4646
|
+
if (fs5.existsSync(DAEMON_PID_FILE)) {
|
|
4647
|
+
const { pid } = JSON.parse(fs5.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4496
4648
|
process.kill(pid, 0);
|
|
4497
4649
|
return process.exit(0);
|
|
4498
4650
|
}
|
|
4499
4651
|
} catch {
|
|
4500
4652
|
try {
|
|
4501
|
-
|
|
4653
|
+
fs5.unlinkSync(DAEMON_PID_FILE);
|
|
4502
4654
|
} catch {
|
|
4503
4655
|
}
|
|
4504
4656
|
server.listen(DAEMON_PORT2, DAEMON_HOST2);
|
|
@@ -4549,7 +4701,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
4549
4701
|
console.log(chalk4.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
|
|
4550
4702
|
}
|
|
4551
4703
|
try {
|
|
4552
|
-
|
|
4704
|
+
fs5.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
4553
4705
|
} catch {
|
|
4554
4706
|
}
|
|
4555
4707
|
const ACTIVITY_MAX_BYTES = 1024 * 1024;
|
|
@@ -4591,30 +4743,30 @@ data: ${JSON.stringify(item.data)}
|
|
|
4591
4743
|
unixServer.listen(ACTIVITY_SOCKET_PATH2);
|
|
4592
4744
|
process.on("exit", () => {
|
|
4593
4745
|
try {
|
|
4594
|
-
|
|
4746
|
+
fs5.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
4595
4747
|
} catch {
|
|
4596
4748
|
}
|
|
4597
4749
|
});
|
|
4598
4750
|
}
|
|
4599
4751
|
function stopDaemon() {
|
|
4600
|
-
if (!
|
|
4752
|
+
if (!fs5.existsSync(DAEMON_PID_FILE)) return console.log(chalk4.yellow("Not running."));
|
|
4601
4753
|
try {
|
|
4602
|
-
const { pid } = JSON.parse(
|
|
4754
|
+
const { pid } = JSON.parse(fs5.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4603
4755
|
process.kill(pid, "SIGTERM");
|
|
4604
4756
|
console.log(chalk4.green("\u2705 Stopped."));
|
|
4605
4757
|
} catch {
|
|
4606
4758
|
console.log(chalk4.gray("Cleaned up stale PID file."));
|
|
4607
4759
|
} finally {
|
|
4608
4760
|
try {
|
|
4609
|
-
|
|
4761
|
+
fs5.unlinkSync(DAEMON_PID_FILE);
|
|
4610
4762
|
} catch {
|
|
4611
4763
|
}
|
|
4612
4764
|
}
|
|
4613
4765
|
}
|
|
4614
4766
|
function daemonStatus() {
|
|
4615
|
-
if (
|
|
4767
|
+
if (fs5.existsSync(DAEMON_PID_FILE)) {
|
|
4616
4768
|
try {
|
|
4617
|
-
const { pid } = JSON.parse(
|
|
4769
|
+
const { pid } = JSON.parse(fs5.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4618
4770
|
process.kill(pid, 0);
|
|
4619
4771
|
console.log(chalk4.green("Node9 daemon: running"));
|
|
4620
4772
|
return;
|
|
@@ -4640,16 +4792,16 @@ var init_daemon = __esm({
|
|
|
4640
4792
|
init_ui2();
|
|
4641
4793
|
init_core();
|
|
4642
4794
|
init_shields();
|
|
4643
|
-
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
4795
|
+
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path7.join(os4.tmpdir(), "node9-activity.sock");
|
|
4644
4796
|
DAEMON_PORT2 = 7391;
|
|
4645
4797
|
DAEMON_HOST2 = "127.0.0.1";
|
|
4646
4798
|
homeDir = os4.homedir();
|
|
4647
|
-
DAEMON_PID_FILE =
|
|
4648
|
-
DECISIONS_FILE =
|
|
4649
|
-
GLOBAL_CONFIG_FILE =
|
|
4650
|
-
CREDENTIALS_FILE =
|
|
4651
|
-
AUDIT_LOG_FILE =
|
|
4652
|
-
TRUST_FILE2 =
|
|
4799
|
+
DAEMON_PID_FILE = path7.join(homeDir, ".node9", "daemon.pid");
|
|
4800
|
+
DECISIONS_FILE = path7.join(homeDir, ".node9", "decisions.json");
|
|
4801
|
+
GLOBAL_CONFIG_FILE = path7.join(homeDir, ".node9", "config.json");
|
|
4802
|
+
CREDENTIALS_FILE = path7.join(homeDir, ".node9", "credentials.json");
|
|
4803
|
+
AUDIT_LOG_FILE = path7.join(homeDir, ".node9", "audit.log");
|
|
4804
|
+
TRUST_FILE2 = path7.join(homeDir, ".node9", "trust.json");
|
|
4653
4805
|
TRUST_DURATIONS = {
|
|
4654
4806
|
"30m": 30 * 6e4,
|
|
4655
4807
|
"1h": 60 * 6e4,
|
|
@@ -4675,11 +4827,11 @@ __export(tail_exports, {
|
|
|
4675
4827
|
});
|
|
4676
4828
|
import http2 from "http";
|
|
4677
4829
|
import chalk5 from "chalk";
|
|
4678
|
-
import
|
|
4830
|
+
import fs7 from "fs";
|
|
4679
4831
|
import os6 from "os";
|
|
4680
|
-
import
|
|
4832
|
+
import path9 from "path";
|
|
4681
4833
|
import readline from "readline";
|
|
4682
|
-
import { spawn as
|
|
4834
|
+
import { spawn as spawn4 } from "child_process";
|
|
4683
4835
|
function getIcon(tool) {
|
|
4684
4836
|
const t = tool.toLowerCase();
|
|
4685
4837
|
for (const [k, v] of Object.entries(ICONS)) {
|
|
@@ -4717,9 +4869,9 @@ function renderPending(activity) {
|
|
|
4717
4869
|
}
|
|
4718
4870
|
async function ensureDaemon() {
|
|
4719
4871
|
let pidPort = null;
|
|
4720
|
-
if (
|
|
4872
|
+
if (fs7.existsSync(PID_FILE)) {
|
|
4721
4873
|
try {
|
|
4722
|
-
const { port } = JSON.parse(
|
|
4874
|
+
const { port } = JSON.parse(fs7.readFileSync(PID_FILE, "utf-8"));
|
|
4723
4875
|
pidPort = port;
|
|
4724
4876
|
} catch {
|
|
4725
4877
|
console.error(chalk5.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
@@ -4734,7 +4886,7 @@ async function ensureDaemon() {
|
|
|
4734
4886
|
} catch {
|
|
4735
4887
|
}
|
|
4736
4888
|
console.log(chalk5.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
4737
|
-
const child =
|
|
4889
|
+
const child = spawn4(process.execPath, [process.argv[1], "daemon"], {
|
|
4738
4890
|
detached: true,
|
|
4739
4891
|
stdio: "ignore",
|
|
4740
4892
|
env: { ...process.env, NODE9_AUTO_STARTED: "1" }
|
|
@@ -4879,7 +5031,7 @@ var init_tail = __esm({
|
|
|
4879
5031
|
"src/tui/tail.ts"() {
|
|
4880
5032
|
"use strict";
|
|
4881
5033
|
init_daemon();
|
|
4882
|
-
PID_FILE =
|
|
5034
|
+
PID_FILE = path9.join(os6.homedir(), ".node9", "daemon.pid");
|
|
4883
5035
|
ICONS = {
|
|
4884
5036
|
bash: "\u{1F4BB}",
|
|
4885
5037
|
shell: "\u{1F4BB}",
|
|
@@ -4905,8 +5057,8 @@ init_core();
|
|
|
4905
5057
|
import { Command } from "commander";
|
|
4906
5058
|
|
|
4907
5059
|
// src/setup.ts
|
|
4908
|
-
import
|
|
4909
|
-
import
|
|
5060
|
+
import fs4 from "fs";
|
|
5061
|
+
import path6 from "path";
|
|
4910
5062
|
import os3 from "os";
|
|
4911
5063
|
import chalk3 from "chalk";
|
|
4912
5064
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
@@ -4924,17 +5076,17 @@ function fullPathCommand(subcommand) {
|
|
|
4924
5076
|
}
|
|
4925
5077
|
function readJson(filePath) {
|
|
4926
5078
|
try {
|
|
4927
|
-
if (
|
|
4928
|
-
return JSON.parse(
|
|
5079
|
+
if (fs4.existsSync(filePath)) {
|
|
5080
|
+
return JSON.parse(fs4.readFileSync(filePath, "utf-8"));
|
|
4929
5081
|
}
|
|
4930
5082
|
} catch {
|
|
4931
5083
|
}
|
|
4932
5084
|
return null;
|
|
4933
5085
|
}
|
|
4934
5086
|
function writeJson(filePath, data) {
|
|
4935
|
-
const dir =
|
|
4936
|
-
if (!
|
|
4937
|
-
|
|
5087
|
+
const dir = path6.dirname(filePath);
|
|
5088
|
+
if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
|
|
5089
|
+
fs4.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
4938
5090
|
}
|
|
4939
5091
|
function isNode9Hook(cmd) {
|
|
4940
5092
|
if (!cmd) return false;
|
|
@@ -4942,8 +5094,8 @@ function isNode9Hook(cmd) {
|
|
|
4942
5094
|
}
|
|
4943
5095
|
function teardownClaude() {
|
|
4944
5096
|
const homeDir2 = os3.homedir();
|
|
4945
|
-
const hooksPath =
|
|
4946
|
-
const mcpPath =
|
|
5097
|
+
const hooksPath = path6.join(homeDir2, ".claude", "settings.json");
|
|
5098
|
+
const mcpPath = path6.join(homeDir2, ".claude.json");
|
|
4947
5099
|
let changed = false;
|
|
4948
5100
|
const settings = readJson(hooksPath);
|
|
4949
5101
|
if (settings?.hooks) {
|
|
@@ -4992,7 +5144,7 @@ function teardownClaude() {
|
|
|
4992
5144
|
}
|
|
4993
5145
|
function teardownGemini() {
|
|
4994
5146
|
const homeDir2 = os3.homedir();
|
|
4995
|
-
const settingsPath =
|
|
5147
|
+
const settingsPath = path6.join(homeDir2, ".gemini", "settings.json");
|
|
4996
5148
|
const settings = readJson(settingsPath);
|
|
4997
5149
|
if (!settings) {
|
|
4998
5150
|
console.log(chalk3.blue(" \u2139\uFE0F ~/.gemini/settings.json not found \u2014 nothing to remove"));
|
|
@@ -5031,7 +5183,7 @@ function teardownGemini() {
|
|
|
5031
5183
|
}
|
|
5032
5184
|
function teardownCursor() {
|
|
5033
5185
|
const homeDir2 = os3.homedir();
|
|
5034
|
-
const mcpPath =
|
|
5186
|
+
const mcpPath = path6.join(homeDir2, ".cursor", "mcp.json");
|
|
5035
5187
|
const mcpConfig = readJson(mcpPath);
|
|
5036
5188
|
if (!mcpConfig?.mcpServers) {
|
|
5037
5189
|
console.log(chalk3.blue(" \u2139\uFE0F ~/.cursor/mcp.json not found \u2014 nothing to remove"));
|
|
@@ -5058,8 +5210,8 @@ function teardownCursor() {
|
|
|
5058
5210
|
}
|
|
5059
5211
|
async function setupClaude() {
|
|
5060
5212
|
const homeDir2 = os3.homedir();
|
|
5061
|
-
const mcpPath =
|
|
5062
|
-
const hooksPath =
|
|
5213
|
+
const mcpPath = path6.join(homeDir2, ".claude.json");
|
|
5214
|
+
const hooksPath = path6.join(homeDir2, ".claude", "settings.json");
|
|
5063
5215
|
const claudeConfig = readJson(mcpPath) ?? {};
|
|
5064
5216
|
const settings = readJson(hooksPath) ?? {};
|
|
5065
5217
|
const servers = claudeConfig.mcpServers ?? {};
|
|
@@ -5134,7 +5286,7 @@ async function setupClaude() {
|
|
|
5134
5286
|
}
|
|
5135
5287
|
async function setupGemini() {
|
|
5136
5288
|
const homeDir2 = os3.homedir();
|
|
5137
|
-
const settingsPath =
|
|
5289
|
+
const settingsPath = path6.join(homeDir2, ".gemini", "settings.json");
|
|
5138
5290
|
const settings = readJson(settingsPath) ?? {};
|
|
5139
5291
|
const servers = settings.mcpServers ?? {};
|
|
5140
5292
|
let anythingChanged = false;
|
|
@@ -5217,7 +5369,7 @@ async function setupGemini() {
|
|
|
5217
5369
|
}
|
|
5218
5370
|
async function setupCursor() {
|
|
5219
5371
|
const homeDir2 = os3.homedir();
|
|
5220
|
-
const mcpPath =
|
|
5372
|
+
const mcpPath = path6.join(homeDir2, ".cursor", "mcp.json");
|
|
5221
5373
|
const mcpConfig = readJson(mcpPath) ?? {};
|
|
5222
5374
|
const servers = mcpConfig.mcpServers ?? {};
|
|
5223
5375
|
let anythingChanged = false;
|
|
@@ -5273,35 +5425,37 @@ async function setupCursor() {
|
|
|
5273
5425
|
|
|
5274
5426
|
// src/cli.ts
|
|
5275
5427
|
init_daemon();
|
|
5276
|
-
import { spawn as
|
|
5428
|
+
import { spawn as spawn5, execSync } from "child_process";
|
|
5277
5429
|
import { parseCommandString } from "execa";
|
|
5278
5430
|
import { execa } from "execa";
|
|
5279
5431
|
import chalk6 from "chalk";
|
|
5280
5432
|
import readline2 from "readline";
|
|
5281
|
-
import
|
|
5282
|
-
import
|
|
5433
|
+
import fs8 from "fs";
|
|
5434
|
+
import path10 from "path";
|
|
5283
5435
|
import os7 from "os";
|
|
5284
5436
|
|
|
5285
5437
|
// src/undo.ts
|
|
5286
|
-
import { spawnSync as spawnSync3 } from "child_process";
|
|
5287
|
-
import
|
|
5288
|
-
import
|
|
5438
|
+
import { spawnSync as spawnSync3, spawn as spawn3 } from "child_process";
|
|
5439
|
+
import crypto2 from "crypto";
|
|
5440
|
+
import fs6 from "fs";
|
|
5441
|
+
import path8 from "path";
|
|
5289
5442
|
import os5 from "os";
|
|
5290
|
-
var SNAPSHOT_STACK_PATH =
|
|
5291
|
-
var UNDO_LATEST_PATH =
|
|
5443
|
+
var SNAPSHOT_STACK_PATH = path8.join(os5.homedir(), ".node9", "snapshots.json");
|
|
5444
|
+
var UNDO_LATEST_PATH = path8.join(os5.homedir(), ".node9", "undo_latest.txt");
|
|
5292
5445
|
var MAX_SNAPSHOTS = 10;
|
|
5446
|
+
var GIT_TIMEOUT = 15e3;
|
|
5293
5447
|
function readStack() {
|
|
5294
5448
|
try {
|
|
5295
|
-
if (
|
|
5296
|
-
return JSON.parse(
|
|
5449
|
+
if (fs6.existsSync(SNAPSHOT_STACK_PATH))
|
|
5450
|
+
return JSON.parse(fs6.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
|
|
5297
5451
|
} catch {
|
|
5298
5452
|
}
|
|
5299
5453
|
return [];
|
|
5300
5454
|
}
|
|
5301
5455
|
function writeStack(stack) {
|
|
5302
|
-
const dir =
|
|
5303
|
-
if (!
|
|
5304
|
-
|
|
5456
|
+
const dir = path8.dirname(SNAPSHOT_STACK_PATH);
|
|
5457
|
+
if (!fs6.existsSync(dir)) fs6.mkdirSync(dir, { recursive: true });
|
|
5458
|
+
fs6.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
5305
5459
|
}
|
|
5306
5460
|
function buildArgsSummary(tool, args) {
|
|
5307
5461
|
if (!args || typeof args !== "object") return "";
|
|
@@ -5314,54 +5468,177 @@ function buildArgsSummary(tool, args) {
|
|
|
5314
5468
|
if (typeof sql === "string") return sql.slice(0, 80);
|
|
5315
5469
|
return tool;
|
|
5316
5470
|
}
|
|
5317
|
-
|
|
5471
|
+
function normalizeCwdForHash(cwd) {
|
|
5472
|
+
let normalized;
|
|
5473
|
+
try {
|
|
5474
|
+
normalized = fs6.realpathSync(cwd);
|
|
5475
|
+
} catch {
|
|
5476
|
+
normalized = cwd;
|
|
5477
|
+
}
|
|
5478
|
+
normalized = normalized.replace(/\\/g, "/");
|
|
5479
|
+
if (process.platform === "win32") normalized = normalized.toLowerCase();
|
|
5480
|
+
return normalized;
|
|
5481
|
+
}
|
|
5482
|
+
function getShadowRepoDir(cwd) {
|
|
5483
|
+
const hash = crypto2.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
|
|
5484
|
+
return path8.join(os5.homedir(), ".node9", "snapshots", hash);
|
|
5485
|
+
}
|
|
5486
|
+
function cleanOrphanedIndexFiles(shadowDir) {
|
|
5487
|
+
try {
|
|
5488
|
+
const cutoff = Date.now() - 6e4;
|
|
5489
|
+
for (const f of fs6.readdirSync(shadowDir)) {
|
|
5490
|
+
if (f.startsWith("index_")) {
|
|
5491
|
+
const fp = path8.join(shadowDir, f);
|
|
5492
|
+
try {
|
|
5493
|
+
if (fs6.statSync(fp).mtimeMs < cutoff) fs6.unlinkSync(fp);
|
|
5494
|
+
} catch {
|
|
5495
|
+
}
|
|
5496
|
+
}
|
|
5497
|
+
}
|
|
5498
|
+
} catch {
|
|
5499
|
+
}
|
|
5500
|
+
}
|
|
5501
|
+
function writeShadowExcludes(shadowDir, ignorePaths) {
|
|
5502
|
+
const hardcoded = [".git", ".node9"];
|
|
5503
|
+
const lines = [...hardcoded, ...ignorePaths].join("\n");
|
|
5504
|
+
try {
|
|
5505
|
+
fs6.writeFileSync(path8.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
|
|
5506
|
+
} catch {
|
|
5507
|
+
}
|
|
5508
|
+
}
|
|
5509
|
+
function ensureShadowRepo(shadowDir, cwd) {
|
|
5510
|
+
cleanOrphanedIndexFiles(shadowDir);
|
|
5511
|
+
const normalizedCwd = normalizeCwdForHash(cwd);
|
|
5512
|
+
const shadowEnvBase = { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd };
|
|
5513
|
+
const check = spawnSync3("git", ["rev-parse", "--git-dir"], {
|
|
5514
|
+
env: shadowEnvBase,
|
|
5515
|
+
timeout: 3e3
|
|
5516
|
+
});
|
|
5517
|
+
if (check.status === 0) {
|
|
5518
|
+
const ptPath = path8.join(shadowDir, "project-path.txt");
|
|
5519
|
+
try {
|
|
5520
|
+
const stored = fs6.readFileSync(ptPath, "utf8").trim();
|
|
5521
|
+
if (stored === normalizedCwd) return true;
|
|
5522
|
+
if (process.env.NODE9_DEBUG === "1")
|
|
5523
|
+
console.error(
|
|
5524
|
+
`[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
|
|
5525
|
+
);
|
|
5526
|
+
fs6.rmSync(shadowDir, { recursive: true, force: true });
|
|
5527
|
+
} catch {
|
|
5528
|
+
try {
|
|
5529
|
+
fs6.writeFileSync(ptPath, normalizedCwd, "utf8");
|
|
5530
|
+
} catch {
|
|
5531
|
+
}
|
|
5532
|
+
return true;
|
|
5533
|
+
}
|
|
5534
|
+
}
|
|
5535
|
+
try {
|
|
5536
|
+
fs6.mkdirSync(shadowDir, { recursive: true });
|
|
5537
|
+
} catch {
|
|
5538
|
+
}
|
|
5539
|
+
const init = spawnSync3("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
|
|
5540
|
+
if (init.status !== 0) {
|
|
5541
|
+
if (process.env.NODE9_DEBUG === "1")
|
|
5542
|
+
console.error("[Node9] git init --bare failed:", init.stderr?.toString());
|
|
5543
|
+
return false;
|
|
5544
|
+
}
|
|
5545
|
+
const configFile = path8.join(shadowDir, "config");
|
|
5546
|
+
spawnSync3("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
|
|
5547
|
+
timeout: 3e3
|
|
5548
|
+
});
|
|
5549
|
+
spawnSync3("git", ["config", "--file", configFile, "core.fsmonitor", "true"], {
|
|
5550
|
+
timeout: 3e3
|
|
5551
|
+
});
|
|
5552
|
+
try {
|
|
5553
|
+
fs6.writeFileSync(path8.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
|
|
5554
|
+
} catch {
|
|
5555
|
+
}
|
|
5556
|
+
return true;
|
|
5557
|
+
}
|
|
5558
|
+
function buildGitEnv(cwd) {
|
|
5559
|
+
const shadowDir = getShadowRepoDir(cwd);
|
|
5560
|
+
const check = spawnSync3("git", ["rev-parse", "--git-dir"], {
|
|
5561
|
+
env: { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd },
|
|
5562
|
+
timeout: 2e3
|
|
5563
|
+
});
|
|
5564
|
+
if (check.status === 0) {
|
|
5565
|
+
return { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd };
|
|
5566
|
+
}
|
|
5567
|
+
return { ...process.env };
|
|
5568
|
+
}
|
|
5569
|
+
async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = []) {
|
|
5570
|
+
let indexFile = null;
|
|
5318
5571
|
try {
|
|
5319
5572
|
const cwd = process.cwd();
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
const
|
|
5325
|
-
|
|
5326
|
-
|
|
5573
|
+
const shadowDir = getShadowRepoDir(cwd);
|
|
5574
|
+
if (!ensureShadowRepo(shadowDir, cwd)) return null;
|
|
5575
|
+
writeShadowExcludes(shadowDir, ignorePaths);
|
|
5576
|
+
indexFile = path8.join(shadowDir, `index_${process.pid}_${Date.now()}`);
|
|
5577
|
+
const shadowEnv = {
|
|
5578
|
+
...process.env,
|
|
5579
|
+
GIT_DIR: shadowDir,
|
|
5580
|
+
GIT_WORK_TREE: cwd,
|
|
5581
|
+
GIT_INDEX_FILE: indexFile
|
|
5582
|
+
};
|
|
5583
|
+
spawnSync3("git", ["add", "-A"], { env: shadowEnv, timeout: GIT_TIMEOUT });
|
|
5584
|
+
const treeRes = spawnSync3("git", ["write-tree"], { env: shadowEnv, timeout: GIT_TIMEOUT });
|
|
5585
|
+
const treeHash = treeRes.stdout?.toString().trim();
|
|
5327
5586
|
if (!treeHash || treeRes.status !== 0) return null;
|
|
5328
|
-
const commitRes = spawnSync3(
|
|
5329
|
-
"
|
|
5330
|
-
treeHash,
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
const commitHash = commitRes.stdout.toString().trim();
|
|
5587
|
+
const commitRes = spawnSync3(
|
|
5588
|
+
"git",
|
|
5589
|
+
["commit-tree", treeHash, "-m", `Node9 AI Snapshot: ${(/* @__PURE__ */ new Date()).toISOString()}`],
|
|
5590
|
+
{ env: shadowEnv, timeout: GIT_TIMEOUT }
|
|
5591
|
+
);
|
|
5592
|
+
const commitHash = commitRes.stdout?.toString().trim();
|
|
5335
5593
|
if (!commitHash || commitRes.status !== 0) return null;
|
|
5336
5594
|
const stack = readStack();
|
|
5337
|
-
|
|
5595
|
+
stack.push({
|
|
5338
5596
|
hash: commitHash,
|
|
5339
5597
|
tool,
|
|
5340
5598
|
argsSummary: buildArgsSummary(tool, args),
|
|
5341
5599
|
cwd,
|
|
5342
5600
|
timestamp: Date.now()
|
|
5343
|
-
};
|
|
5344
|
-
stack.
|
|
5601
|
+
});
|
|
5602
|
+
const shouldGc = stack.length % 5 === 0;
|
|
5345
5603
|
if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
|
|
5346
5604
|
writeStack(stack);
|
|
5347
|
-
|
|
5605
|
+
fs6.writeFileSync(UNDO_LATEST_PATH, commitHash);
|
|
5606
|
+
if (shouldGc) {
|
|
5607
|
+
spawn3("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
|
|
5608
|
+
}
|
|
5348
5609
|
return commitHash;
|
|
5349
5610
|
} catch (err) {
|
|
5350
5611
|
if (process.env.NODE9_DEBUG === "1") console.error("[Node9 Undo Engine Error]:", err);
|
|
5612
|
+
return null;
|
|
5613
|
+
} finally {
|
|
5614
|
+
if (indexFile) {
|
|
5615
|
+
try {
|
|
5616
|
+
fs6.unlinkSync(indexFile);
|
|
5617
|
+
} catch {
|
|
5618
|
+
}
|
|
5619
|
+
}
|
|
5351
5620
|
}
|
|
5352
|
-
return null;
|
|
5353
5621
|
}
|
|
5354
5622
|
function getSnapshotHistory() {
|
|
5355
5623
|
return readStack();
|
|
5356
5624
|
}
|
|
5357
5625
|
function computeUndoDiff(hash, cwd) {
|
|
5358
5626
|
try {
|
|
5359
|
-
const
|
|
5360
|
-
const
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5627
|
+
const env = buildGitEnv(cwd);
|
|
5628
|
+
const statRes = spawnSync3("git", ["diff", hash, "--stat", "--", "."], {
|
|
5629
|
+
cwd,
|
|
5630
|
+
env,
|
|
5631
|
+
timeout: GIT_TIMEOUT
|
|
5632
|
+
});
|
|
5633
|
+
const stat = statRes.stdout?.toString().trim();
|
|
5634
|
+
if (!stat || statRes.status !== 0) return null;
|
|
5635
|
+
const diffRes = spawnSync3("git", ["diff", hash, "--", "."], {
|
|
5636
|
+
cwd,
|
|
5637
|
+
env,
|
|
5638
|
+
timeout: GIT_TIMEOUT
|
|
5639
|
+
});
|
|
5640
|
+
const raw = diffRes.stdout?.toString();
|
|
5641
|
+
if (!raw || diffRes.status !== 0) return null;
|
|
5365
5642
|
const lines = raw.split("\n").filter(
|
|
5366
5643
|
(l) => !l.startsWith("diff --git") && !l.startsWith("index ") && !l.startsWith("Binary")
|
|
5367
5644
|
);
|
|
@@ -5373,18 +5650,31 @@ function computeUndoDiff(hash, cwd) {
|
|
|
5373
5650
|
function applyUndo(hash, cwd) {
|
|
5374
5651
|
try {
|
|
5375
5652
|
const dir = cwd ?? process.cwd();
|
|
5653
|
+
const env = buildGitEnv(dir);
|
|
5376
5654
|
const restore = spawnSync3("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
|
|
5377
|
-
cwd: dir
|
|
5655
|
+
cwd: dir,
|
|
5656
|
+
env,
|
|
5657
|
+
timeout: GIT_TIMEOUT
|
|
5378
5658
|
});
|
|
5379
5659
|
if (restore.status !== 0) return false;
|
|
5380
|
-
const lsTree = spawnSync3("git", ["ls-tree", "-r", "--name-only", hash], {
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5660
|
+
const lsTree = spawnSync3("git", ["ls-tree", "-r", "--name-only", hash], {
|
|
5661
|
+
cwd: dir,
|
|
5662
|
+
env,
|
|
5663
|
+
timeout: GIT_TIMEOUT
|
|
5664
|
+
});
|
|
5665
|
+
const snapshotFiles = new Set(
|
|
5666
|
+
lsTree.stdout?.toString().trim().split("\n").filter(Boolean) ?? []
|
|
5667
|
+
);
|
|
5668
|
+
const tracked = spawnSync3("git", ["ls-files"], { cwd: dir, env, timeout: GIT_TIMEOUT }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
5669
|
+
const untracked = spawnSync3("git", ["ls-files", "--others", "--exclude-standard"], {
|
|
5670
|
+
cwd: dir,
|
|
5671
|
+
env,
|
|
5672
|
+
timeout: GIT_TIMEOUT
|
|
5673
|
+
}).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
5384
5674
|
for (const file of [...tracked, ...untracked]) {
|
|
5385
|
-
const fullPath =
|
|
5386
|
-
if (!snapshotFiles.has(file) &&
|
|
5387
|
-
|
|
5675
|
+
const fullPath = path8.join(dir, file);
|
|
5676
|
+
if (!snapshotFiles.has(file) && fs6.existsSync(fullPath)) {
|
|
5677
|
+
fs6.unlinkSync(fullPath);
|
|
5388
5678
|
}
|
|
5389
5679
|
}
|
|
5390
5680
|
return true;
|
|
@@ -5397,7 +5687,7 @@ function applyUndo(hash, cwd) {
|
|
|
5397
5687
|
init_shields();
|
|
5398
5688
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
5399
5689
|
var { version } = JSON.parse(
|
|
5400
|
-
|
|
5690
|
+
fs8.readFileSync(path10.join(__dirname, "../package.json"), "utf-8")
|
|
5401
5691
|
);
|
|
5402
5692
|
function parseDuration(str) {
|
|
5403
5693
|
const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
|
|
@@ -5493,7 +5783,7 @@ function openBrowserLocal() {
|
|
|
5493
5783
|
}
|
|
5494
5784
|
async function autoStartDaemonAndWait() {
|
|
5495
5785
|
try {
|
|
5496
|
-
const child =
|
|
5786
|
+
const child = spawn5("node9", ["daemon"], {
|
|
5497
5787
|
detached: true,
|
|
5498
5788
|
stdio: "ignore",
|
|
5499
5789
|
env: { ...process.env, NODE9_AUTO_STARTED: "1" }
|
|
@@ -5530,7 +5820,7 @@ async function runProxy(targetCommand) {
|
|
|
5530
5820
|
} catch {
|
|
5531
5821
|
}
|
|
5532
5822
|
console.log(chalk6.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
|
|
5533
|
-
const child =
|
|
5823
|
+
const child = spawn5(executable, args, {
|
|
5534
5824
|
stdio: ["pipe", "pipe", "inherit"],
|
|
5535
5825
|
// We control STDIN and STDOUT
|
|
5536
5826
|
shell: false,
|
|
@@ -5599,14 +5889,14 @@ async function runProxy(targetCommand) {
|
|
|
5599
5889
|
}
|
|
5600
5890
|
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) => {
|
|
5601
5891
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
5602
|
-
const credPath =
|
|
5603
|
-
if (!
|
|
5604
|
-
|
|
5892
|
+
const credPath = path10.join(os7.homedir(), ".node9", "credentials.json");
|
|
5893
|
+
if (!fs8.existsSync(path10.dirname(credPath)))
|
|
5894
|
+
fs8.mkdirSync(path10.dirname(credPath), { recursive: true });
|
|
5605
5895
|
const profileName = options.profile || "default";
|
|
5606
5896
|
let existingCreds = {};
|
|
5607
5897
|
try {
|
|
5608
|
-
if (
|
|
5609
|
-
const raw = JSON.parse(
|
|
5898
|
+
if (fs8.existsSync(credPath)) {
|
|
5899
|
+
const raw = JSON.parse(fs8.readFileSync(credPath, "utf-8"));
|
|
5610
5900
|
if (raw.apiKey) {
|
|
5611
5901
|
existingCreds = {
|
|
5612
5902
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
|
|
@@ -5618,13 +5908,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
5618
5908
|
} catch {
|
|
5619
5909
|
}
|
|
5620
5910
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
5621
|
-
|
|
5911
|
+
fs8.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
5622
5912
|
if (profileName === "default") {
|
|
5623
|
-
const configPath =
|
|
5913
|
+
const configPath = path10.join(os7.homedir(), ".node9", "config.json");
|
|
5624
5914
|
let config = {};
|
|
5625
5915
|
try {
|
|
5626
|
-
if (
|
|
5627
|
-
config = JSON.parse(
|
|
5916
|
+
if (fs8.existsSync(configPath))
|
|
5917
|
+
config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
|
|
5628
5918
|
} catch {
|
|
5629
5919
|
}
|
|
5630
5920
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -5639,9 +5929,9 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
5639
5929
|
approvers.cloud = false;
|
|
5640
5930
|
}
|
|
5641
5931
|
s.approvers = approvers;
|
|
5642
|
-
if (!
|
|
5643
|
-
|
|
5644
|
-
|
|
5932
|
+
if (!fs8.existsSync(path10.dirname(configPath)))
|
|
5933
|
+
fs8.mkdirSync(path10.dirname(configPath), { recursive: true });
|
|
5934
|
+
fs8.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
5645
5935
|
}
|
|
5646
5936
|
if (options.profile && profileName !== "default") {
|
|
5647
5937
|
console.log(chalk6.green(`\u2705 Profile "${profileName}" saved`));
|
|
@@ -5727,15 +6017,15 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
5727
6017
|
}
|
|
5728
6018
|
}
|
|
5729
6019
|
if (options.purge) {
|
|
5730
|
-
const node9Dir =
|
|
5731
|
-
if (
|
|
6020
|
+
const node9Dir = path10.join(os7.homedir(), ".node9");
|
|
6021
|
+
if (fs8.existsSync(node9Dir)) {
|
|
5732
6022
|
const confirmed = await confirm3({
|
|
5733
6023
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
5734
6024
|
default: false
|
|
5735
6025
|
});
|
|
5736
6026
|
if (confirmed) {
|
|
5737
|
-
|
|
5738
|
-
if (
|
|
6027
|
+
fs8.rmSync(node9Dir, { recursive: true });
|
|
6028
|
+
if (fs8.existsSync(node9Dir)) {
|
|
5739
6029
|
console.error(
|
|
5740
6030
|
chalk6.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
5741
6031
|
);
|
|
@@ -5807,10 +6097,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
5807
6097
|
);
|
|
5808
6098
|
}
|
|
5809
6099
|
section("Configuration");
|
|
5810
|
-
const globalConfigPath =
|
|
5811
|
-
if (
|
|
6100
|
+
const globalConfigPath = path10.join(homeDir2, ".node9", "config.json");
|
|
6101
|
+
if (fs8.existsSync(globalConfigPath)) {
|
|
5812
6102
|
try {
|
|
5813
|
-
JSON.parse(
|
|
6103
|
+
JSON.parse(fs8.readFileSync(globalConfigPath, "utf-8"));
|
|
5814
6104
|
pass("~/.node9/config.json found and valid");
|
|
5815
6105
|
} catch {
|
|
5816
6106
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -5818,17 +6108,17 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
5818
6108
|
} else {
|
|
5819
6109
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
5820
6110
|
}
|
|
5821
|
-
const projectConfigPath =
|
|
5822
|
-
if (
|
|
6111
|
+
const projectConfigPath = path10.join(process.cwd(), "node9.config.json");
|
|
6112
|
+
if (fs8.existsSync(projectConfigPath)) {
|
|
5823
6113
|
try {
|
|
5824
|
-
JSON.parse(
|
|
6114
|
+
JSON.parse(fs8.readFileSync(projectConfigPath, "utf-8"));
|
|
5825
6115
|
pass("node9.config.json found and valid (project)");
|
|
5826
6116
|
} catch {
|
|
5827
6117
|
fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
|
|
5828
6118
|
}
|
|
5829
6119
|
}
|
|
5830
|
-
const credsPath =
|
|
5831
|
-
if (
|
|
6120
|
+
const credsPath = path10.join(homeDir2, ".node9", "credentials.json");
|
|
6121
|
+
if (fs8.existsSync(credsPath)) {
|
|
5832
6122
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
5833
6123
|
} else {
|
|
5834
6124
|
warn(
|
|
@@ -5837,10 +6127,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
5837
6127
|
);
|
|
5838
6128
|
}
|
|
5839
6129
|
section("Agent Hooks");
|
|
5840
|
-
const claudeSettingsPath =
|
|
5841
|
-
if (
|
|
6130
|
+
const claudeSettingsPath = path10.join(homeDir2, ".claude", "settings.json");
|
|
6131
|
+
if (fs8.existsSync(claudeSettingsPath)) {
|
|
5842
6132
|
try {
|
|
5843
|
-
const cs = JSON.parse(
|
|
6133
|
+
const cs = JSON.parse(fs8.readFileSync(claudeSettingsPath, "utf-8"));
|
|
5844
6134
|
const hasHook = cs.hooks?.PreToolUse?.some(
|
|
5845
6135
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
5846
6136
|
);
|
|
@@ -5853,10 +6143,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
5853
6143
|
} else {
|
|
5854
6144
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
5855
6145
|
}
|
|
5856
|
-
const geminiSettingsPath =
|
|
5857
|
-
if (
|
|
6146
|
+
const geminiSettingsPath = path10.join(homeDir2, ".gemini", "settings.json");
|
|
6147
|
+
if (fs8.existsSync(geminiSettingsPath)) {
|
|
5858
6148
|
try {
|
|
5859
|
-
const gs = JSON.parse(
|
|
6149
|
+
const gs = JSON.parse(fs8.readFileSync(geminiSettingsPath, "utf-8"));
|
|
5860
6150
|
const hasHook = gs.hooks?.BeforeTool?.some(
|
|
5861
6151
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
5862
6152
|
);
|
|
@@ -5869,10 +6159,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
5869
6159
|
} else {
|
|
5870
6160
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
5871
6161
|
}
|
|
5872
|
-
const cursorHooksPath =
|
|
5873
|
-
if (
|
|
6162
|
+
const cursorHooksPath = path10.join(homeDir2, ".cursor", "hooks.json");
|
|
6163
|
+
if (fs8.existsSync(cursorHooksPath)) {
|
|
5874
6164
|
try {
|
|
5875
|
-
const cur = JSON.parse(
|
|
6165
|
+
const cur = JSON.parse(fs8.readFileSync(cursorHooksPath, "utf-8"));
|
|
5876
6166
|
const hasHook = cur.hooks?.preToolUse?.some(
|
|
5877
6167
|
(h) => h.command?.includes("node9") || h.command?.includes("cli.js")
|
|
5878
6168
|
);
|
|
@@ -5974,8 +6264,8 @@ program.command("explain").description(
|
|
|
5974
6264
|
console.log("");
|
|
5975
6265
|
});
|
|
5976
6266
|
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) => {
|
|
5977
|
-
const configPath =
|
|
5978
|
-
if (
|
|
6267
|
+
const configPath = path10.join(os7.homedir(), ".node9", "config.json");
|
|
6268
|
+
if (fs8.existsSync(configPath) && !options.force) {
|
|
5979
6269
|
console.log(chalk6.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
5980
6270
|
console.log(chalk6.gray(` Run with --force to overwrite.`));
|
|
5981
6271
|
return;
|
|
@@ -5989,9 +6279,9 @@ program.command("init").description("Create ~/.node9/config.json with default po
|
|
|
5989
6279
|
mode: safeMode
|
|
5990
6280
|
}
|
|
5991
6281
|
};
|
|
5992
|
-
const dir =
|
|
5993
|
-
if (!
|
|
5994
|
-
|
|
6282
|
+
const dir = path10.dirname(configPath);
|
|
6283
|
+
if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
|
|
6284
|
+
fs8.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
5995
6285
|
console.log(chalk6.green(`\u2705 Global config created: ${configPath}`));
|
|
5996
6286
|
console.log(chalk6.cyan(` Mode set to: ${safeMode}`));
|
|
5997
6287
|
console.log(
|
|
@@ -6009,14 +6299,14 @@ function formatRelativeTime(timestamp) {
|
|
|
6009
6299
|
return new Date(timestamp).toLocaleDateString();
|
|
6010
6300
|
}
|
|
6011
6301
|
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) => {
|
|
6012
|
-
const logPath =
|
|
6013
|
-
if (!
|
|
6302
|
+
const logPath = path10.join(os7.homedir(), ".node9", "audit.log");
|
|
6303
|
+
if (!fs8.existsSync(logPath)) {
|
|
6014
6304
|
console.log(
|
|
6015
6305
|
chalk6.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
6016
6306
|
);
|
|
6017
6307
|
return;
|
|
6018
6308
|
}
|
|
6019
|
-
const raw =
|
|
6309
|
+
const raw = fs8.readFileSync(logPath, "utf-8");
|
|
6020
6310
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
6021
6311
|
let entries = lines.flatMap((line) => {
|
|
6022
6312
|
try {
|
|
@@ -6099,13 +6389,13 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
6099
6389
|
console.log("");
|
|
6100
6390
|
const modeLabel = settings.mode === "audit" ? chalk6.blue("audit") : settings.mode === "strict" ? chalk6.red("strict") : chalk6.white("standard");
|
|
6101
6391
|
console.log(` Mode: ${modeLabel}`);
|
|
6102
|
-
const projectConfig =
|
|
6103
|
-
const globalConfig =
|
|
6392
|
+
const projectConfig = path10.join(process.cwd(), "node9.config.json");
|
|
6393
|
+
const globalConfig = path10.join(os7.homedir(), ".node9", "config.json");
|
|
6104
6394
|
console.log(
|
|
6105
|
-
` Local: ${
|
|
6395
|
+
` Local: ${fs8.existsSync(projectConfig) ? chalk6.green("Active (node9.config.json)") : chalk6.gray("Not present")}`
|
|
6106
6396
|
);
|
|
6107
6397
|
console.log(
|
|
6108
|
-
` Global: ${
|
|
6398
|
+
` Global: ${fs8.existsSync(globalConfig) ? chalk6.green("Active (~/.node9/config.json)") : chalk6.gray("Not present")}`
|
|
6109
6399
|
);
|
|
6110
6400
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
6111
6401
|
console.log(
|
|
@@ -6149,7 +6439,7 @@ program.command("daemon").description("Run the local approval server").argument(
|
|
|
6149
6439
|
console.log(chalk6.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
|
|
6150
6440
|
process.exit(0);
|
|
6151
6441
|
}
|
|
6152
|
-
const child =
|
|
6442
|
+
const child = spawn5("node9", ["daemon"], { detached: true, stdio: "ignore" });
|
|
6153
6443
|
child.unref();
|
|
6154
6444
|
for (let i = 0; i < 12; i++) {
|
|
6155
6445
|
await new Promise((r) => setTimeout(r, 250));
|
|
@@ -6161,7 +6451,7 @@ program.command("daemon").description("Run the local approval server").argument(
|
|
|
6161
6451
|
process.exit(0);
|
|
6162
6452
|
}
|
|
6163
6453
|
if (options.background) {
|
|
6164
|
-
const child =
|
|
6454
|
+
const child = spawn5("node9", ["daemon"], { detached: true, stdio: "ignore" });
|
|
6165
6455
|
child.unref();
|
|
6166
6456
|
console.log(chalk6.green(`
|
|
6167
6457
|
\u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
|
|
@@ -6189,9 +6479,9 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
|
|
|
6189
6479
|
} catch (err) {
|
|
6190
6480
|
const tempConfig = getConfig();
|
|
6191
6481
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
6192
|
-
const logPath =
|
|
6482
|
+
const logPath = path10.join(os7.homedir(), ".node9", "hook-debug.log");
|
|
6193
6483
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
6194
|
-
|
|
6484
|
+
fs8.appendFileSync(
|
|
6195
6485
|
logPath,
|
|
6196
6486
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
6197
6487
|
RAW: ${raw}
|
|
@@ -6209,10 +6499,10 @@ RAW: ${raw}
|
|
|
6209
6499
|
}
|
|
6210
6500
|
const config = getConfig();
|
|
6211
6501
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
6212
|
-
const logPath =
|
|
6213
|
-
if (!
|
|
6214
|
-
|
|
6215
|
-
|
|
6502
|
+
const logPath = path10.join(os7.homedir(), ".node9", "hook-debug.log");
|
|
6503
|
+
if (!fs8.existsSync(path10.dirname(logPath)))
|
|
6504
|
+
fs8.mkdirSync(path10.dirname(logPath), { recursive: true });
|
|
6505
|
+
fs8.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
6216
6506
|
`);
|
|
6217
6507
|
}
|
|
6218
6508
|
const toolName = sanitize(payload.tool_name ?? payload.name ?? "");
|
|
@@ -6256,7 +6546,7 @@ RAW: ${raw}
|
|
|
6256
6546
|
}
|
|
6257
6547
|
const meta = { agent, mcpServer };
|
|
6258
6548
|
if (shouldSnapshot(toolName, toolInput, config)) {
|
|
6259
|
-
await createShadowSnapshot(toolName, toolInput);
|
|
6549
|
+
await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
|
|
6260
6550
|
}
|
|
6261
6551
|
const result = await authorizeHeadless(toolName, toolInput, false, meta);
|
|
6262
6552
|
if (result.approved) {
|
|
@@ -6289,9 +6579,9 @@ RAW: ${raw}
|
|
|
6289
6579
|
});
|
|
6290
6580
|
} catch (err) {
|
|
6291
6581
|
if (process.env.NODE9_DEBUG === "1") {
|
|
6292
|
-
const logPath =
|
|
6582
|
+
const logPath = path10.join(os7.homedir(), ".node9", "hook-debug.log");
|
|
6293
6583
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
6294
|
-
|
|
6584
|
+
fs8.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
6295
6585
|
`);
|
|
6296
6586
|
}
|
|
6297
6587
|
process.exit(0);
|
|
@@ -6336,13 +6626,13 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
|
|
|
6336
6626
|
decision: "allowed",
|
|
6337
6627
|
source: "post-hook"
|
|
6338
6628
|
};
|
|
6339
|
-
const logPath =
|
|
6340
|
-
if (!
|
|
6341
|
-
|
|
6342
|
-
|
|
6629
|
+
const logPath = path10.join(os7.homedir(), ".node9", "audit.log");
|
|
6630
|
+
if (!fs8.existsSync(path10.dirname(logPath)))
|
|
6631
|
+
fs8.mkdirSync(path10.dirname(logPath), { recursive: true });
|
|
6632
|
+
fs8.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
6343
6633
|
const config = getConfig();
|
|
6344
6634
|
if (shouldSnapshot(tool, {}, config)) {
|
|
6345
|
-
await createShadowSnapshot();
|
|
6635
|
+
await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
|
|
6346
6636
|
}
|
|
6347
6637
|
} catch {
|
|
6348
6638
|
}
|
|
@@ -6615,9 +6905,9 @@ process.on("unhandledRejection", (reason) => {
|
|
|
6615
6905
|
const isCheckHook = process.argv[2] === "check";
|
|
6616
6906
|
if (isCheckHook) {
|
|
6617
6907
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
6618
|
-
const logPath =
|
|
6908
|
+
const logPath = path10.join(os7.homedir(), ".node9", "hook-debug.log");
|
|
6619
6909
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
6620
|
-
|
|
6910
|
+
fs8.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
6621
6911
|
`);
|
|
6622
6912
|
}
|
|
6623
6913
|
process.exit(0);
|