@node9/proxy 1.0.19 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/cli.js +523 -251
- package/dist/cli.mjs +529 -257
- package/dist/index.js +178 -59
- package/dist/index.mjs +178 -59
- 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,14 +762,16 @@ 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";
|
|
734
769
|
DLP_PATTERNS = [
|
|
735
770
|
{ name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
|
|
736
771
|
{ name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
|
|
737
|
-
|
|
772
|
+
// Slack bot tokens: xoxb- + variable segment. Real tokens are ~50–80 chars;
|
|
773
|
+
// lower bound 20 avoids false negatives on partial tokens, upper 100 caps scan cost.
|
|
774
|
+
{ name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]{20,100}\b/, severity: "block" },
|
|
738
775
|
{ name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
|
|
739
776
|
{ name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
|
|
740
777
|
{
|
|
@@ -742,8 +779,43 @@ var init_dlp = __esm({
|
|
|
742
779
|
regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
|
743
780
|
severity: "block"
|
|
744
781
|
},
|
|
782
|
+
// GCP service account JSON (detects the type field that uniquely identifies it)
|
|
783
|
+
{
|
|
784
|
+
name: "GCP Service Account",
|
|
785
|
+
regex: /"type"\s*:\s*"service_account"/,
|
|
786
|
+
severity: "block"
|
|
787
|
+
},
|
|
788
|
+
// NPM auth token in .npmrc format
|
|
789
|
+
{
|
|
790
|
+
name: "NPM Auth Token",
|
|
791
|
+
regex: /_authToken\s*=\s*[A-Za-z0-9_\-]{20,}/,
|
|
792
|
+
severity: "block"
|
|
793
|
+
},
|
|
745
794
|
{ name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
|
|
746
795
|
];
|
|
796
|
+
SENSITIVE_PATH_PATTERNS = [
|
|
797
|
+
/[/\\]\.ssh[/\\]/i,
|
|
798
|
+
/[/\\]\.aws[/\\]/i,
|
|
799
|
+
/[/\\]\.config[/\\]gcloud[/\\]/i,
|
|
800
|
+
/[/\\]\.azure[/\\]/i,
|
|
801
|
+
/[/\\]\.kube[/\\]config$/i,
|
|
802
|
+
/[/\\]\.env($|\.)/i,
|
|
803
|
+
// .env, .env.local, .env.production — not .envoy
|
|
804
|
+
/[/\\]\.git-credentials$/i,
|
|
805
|
+
/[/\\]\.npmrc$/i,
|
|
806
|
+
/[/\\]\.docker[/\\]config\.json$/i,
|
|
807
|
+
/[/\\][^/\\]+\.pem$/i,
|
|
808
|
+
/[/\\][^/\\]+\.key$/i,
|
|
809
|
+
/[/\\][^/\\]+\.p12$/i,
|
|
810
|
+
/[/\\][^/\\]+\.pfx$/i,
|
|
811
|
+
/^\/etc\/passwd$/,
|
|
812
|
+
/^\/etc\/shadow$/,
|
|
813
|
+
/^\/etc\/sudoers$/,
|
|
814
|
+
/[/\\]credentials\.json$/i,
|
|
815
|
+
/[/\\]id_rsa$/i,
|
|
816
|
+
/[/\\]id_ed25519$/i,
|
|
817
|
+
/[/\\]id_ecdsa$/i
|
|
818
|
+
];
|
|
747
819
|
MAX_DEPTH = 5;
|
|
748
820
|
MAX_STRING_BYTES = 1e5;
|
|
749
821
|
MAX_JSON_PARSE_BYTES = 1e4;
|
|
@@ -753,21 +825,22 @@ var init_dlp = __esm({
|
|
|
753
825
|
// src/core.ts
|
|
754
826
|
import chalk2 from "chalk";
|
|
755
827
|
import { confirm } from "@inquirer/prompts";
|
|
756
|
-
import
|
|
757
|
-
import
|
|
828
|
+
import fs3 from "fs";
|
|
829
|
+
import path5 from "path";
|
|
758
830
|
import os2 from "os";
|
|
759
831
|
import net from "net";
|
|
760
832
|
import { randomUUID } from "crypto";
|
|
761
833
|
import { spawnSync } from "child_process";
|
|
762
834
|
import pm from "picomatch";
|
|
835
|
+
import safeRegex from "safe-regex2";
|
|
763
836
|
import { parse } from "sh-syntax";
|
|
764
837
|
function checkPause() {
|
|
765
838
|
try {
|
|
766
|
-
if (!
|
|
767
|
-
const state = JSON.parse(
|
|
839
|
+
if (!fs3.existsSync(PAUSED_FILE)) return { paused: false };
|
|
840
|
+
const state = JSON.parse(fs3.readFileSync(PAUSED_FILE, "utf-8"));
|
|
768
841
|
if (state.expiry > 0 && Date.now() >= state.expiry) {
|
|
769
842
|
try {
|
|
770
|
-
|
|
843
|
+
fs3.unlinkSync(PAUSED_FILE);
|
|
771
844
|
} catch {
|
|
772
845
|
}
|
|
773
846
|
return { paused: false };
|
|
@@ -778,11 +851,11 @@ function checkPause() {
|
|
|
778
851
|
}
|
|
779
852
|
}
|
|
780
853
|
function atomicWriteSync(filePath, data, options) {
|
|
781
|
-
const dir =
|
|
782
|
-
if (!
|
|
854
|
+
const dir = path5.dirname(filePath);
|
|
855
|
+
if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
|
|
783
856
|
const tmpPath = `${filePath}.${os2.hostname()}.${process.pid}.tmp`;
|
|
784
|
-
|
|
785
|
-
|
|
857
|
+
fs3.writeFileSync(tmpPath, data, options);
|
|
858
|
+
fs3.renameSync(tmpPath, filePath);
|
|
786
859
|
}
|
|
787
860
|
function pauseNode9(durationMs, durationStr) {
|
|
788
861
|
const state = { expiry: Date.now() + durationMs, duration: durationStr };
|
|
@@ -790,18 +863,61 @@ function pauseNode9(durationMs, durationStr) {
|
|
|
790
863
|
}
|
|
791
864
|
function resumeNode9() {
|
|
792
865
|
try {
|
|
793
|
-
if (
|
|
866
|
+
if (fs3.existsSync(PAUSED_FILE)) fs3.unlinkSync(PAUSED_FILE);
|
|
794
867
|
} catch {
|
|
795
868
|
}
|
|
796
869
|
}
|
|
870
|
+
function validateRegex(pattern) {
|
|
871
|
+
if (!pattern) return "Pattern is required";
|
|
872
|
+
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
873
|
+
try {
|
|
874
|
+
new RegExp(pattern);
|
|
875
|
+
} catch (e) {
|
|
876
|
+
return `Invalid regex syntax: ${e.message}`;
|
|
877
|
+
}
|
|
878
|
+
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
879
|
+
if (!safeRegex(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
function getCompiledRegex(pattern, flags = "") {
|
|
883
|
+
if (flags && !/^[gimsuy]+$/.test(flags)) {
|
|
884
|
+
if (process.env.NODE9_DEBUG === "1") console.error(`[Node9] Invalid regex flags: "${flags}"`);
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
const key = `${pattern}\0${flags}`;
|
|
888
|
+
if (regexCache.has(key)) {
|
|
889
|
+
const cached = regexCache.get(key);
|
|
890
|
+
regexCache.delete(key);
|
|
891
|
+
regexCache.set(key, cached);
|
|
892
|
+
return cached;
|
|
893
|
+
}
|
|
894
|
+
const err = validateRegex(pattern);
|
|
895
|
+
if (err) {
|
|
896
|
+
if (process.env.NODE9_DEBUG === "1")
|
|
897
|
+
console.error(`[Node9] Regex blocked: ${err} \u2014 pattern: "${pattern}"`);
|
|
898
|
+
return null;
|
|
899
|
+
}
|
|
900
|
+
try {
|
|
901
|
+
const re = new RegExp(pattern, flags);
|
|
902
|
+
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
903
|
+
const oldest = regexCache.keys().next().value;
|
|
904
|
+
if (oldest) regexCache.delete(oldest);
|
|
905
|
+
}
|
|
906
|
+
regexCache.set(key, re);
|
|
907
|
+
return re;
|
|
908
|
+
} catch (e) {
|
|
909
|
+
if (process.env.NODE9_DEBUG === "1") console.error(`[Node9] Regex compile failed:`, e);
|
|
910
|
+
return null;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
797
913
|
function getActiveTrustSession(toolName) {
|
|
798
914
|
try {
|
|
799
|
-
if (!
|
|
800
|
-
const trust = JSON.parse(
|
|
915
|
+
if (!fs3.existsSync(TRUST_FILE)) return false;
|
|
916
|
+
const trust = JSON.parse(fs3.readFileSync(TRUST_FILE, "utf-8"));
|
|
801
917
|
const now = Date.now();
|
|
802
918
|
const active = trust.entries.filter((e) => e.expiry > now);
|
|
803
919
|
if (active.length !== trust.entries.length) {
|
|
804
|
-
|
|
920
|
+
fs3.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
|
|
805
921
|
}
|
|
806
922
|
return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
|
|
807
923
|
} catch {
|
|
@@ -812,8 +928,8 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
812
928
|
try {
|
|
813
929
|
let trust = { entries: [] };
|
|
814
930
|
try {
|
|
815
|
-
if (
|
|
816
|
-
trust = JSON.parse(
|
|
931
|
+
if (fs3.existsSync(TRUST_FILE)) {
|
|
932
|
+
trust = JSON.parse(fs3.readFileSync(TRUST_FILE, "utf-8"));
|
|
817
933
|
}
|
|
818
934
|
} catch {
|
|
819
935
|
}
|
|
@@ -829,9 +945,9 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
829
945
|
}
|
|
830
946
|
function appendToLog(logPath, entry) {
|
|
831
947
|
try {
|
|
832
|
-
const dir =
|
|
833
|
-
if (!
|
|
834
|
-
|
|
948
|
+
const dir = path5.dirname(logPath);
|
|
949
|
+
if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
|
|
950
|
+
fs3.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
835
951
|
} catch {
|
|
836
952
|
}
|
|
837
953
|
}
|
|
@@ -873,9 +989,9 @@ function matchesPattern(text, patterns) {
|
|
|
873
989
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
874
990
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
875
991
|
}
|
|
876
|
-
function getNestedValue(obj,
|
|
992
|
+
function getNestedValue(obj, path11) {
|
|
877
993
|
if (!obj || typeof obj !== "object") return null;
|
|
878
|
-
return
|
|
994
|
+
return path11.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
879
995
|
}
|
|
880
996
|
function shouldSnapshot(toolName, args, config) {
|
|
881
997
|
if (!config.settings.enableUndo) return false;
|
|
@@ -906,19 +1022,16 @@ function evaluateSmartConditions(args, rule) {
|
|
|
906
1022
|
return val !== null && cond.value ? !val.includes(cond.value) : true;
|
|
907
1023
|
case "matches": {
|
|
908
1024
|
if (val === null || !cond.value) return false;
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
return false;
|
|
913
|
-
}
|
|
1025
|
+
const reM = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
1026
|
+
if (!reM) return false;
|
|
1027
|
+
return reM.test(val);
|
|
914
1028
|
}
|
|
915
1029
|
case "notMatches": {
|
|
916
|
-
if (
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
}
|
|
1030
|
+
if (!cond.value) return false;
|
|
1031
|
+
if (val === null) return true;
|
|
1032
|
+
const reN = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
1033
|
+
if (!reN) return false;
|
|
1034
|
+
return !reN.test(val);
|
|
922
1035
|
}
|
|
923
1036
|
case "matchesGlob":
|
|
924
1037
|
return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
|
|
@@ -1031,9 +1144,9 @@ function _resetConfigCache() {
|
|
|
1031
1144
|
}
|
|
1032
1145
|
function getGlobalSettings() {
|
|
1033
1146
|
try {
|
|
1034
|
-
const globalConfigPath =
|
|
1035
|
-
if (
|
|
1036
|
-
const parsed = JSON.parse(
|
|
1147
|
+
const globalConfigPath = path5.join(os2.homedir(), ".node9", "config.json");
|
|
1148
|
+
if (fs3.existsSync(globalConfigPath)) {
|
|
1149
|
+
const parsed = JSON.parse(fs3.readFileSync(globalConfigPath, "utf-8"));
|
|
1037
1150
|
const settings = parsed.settings || {};
|
|
1038
1151
|
return {
|
|
1039
1152
|
mode: settings.mode || "audit",
|
|
@@ -1055,9 +1168,9 @@ function getGlobalSettings() {
|
|
|
1055
1168
|
}
|
|
1056
1169
|
function getInternalToken() {
|
|
1057
1170
|
try {
|
|
1058
|
-
const pidFile =
|
|
1059
|
-
if (!
|
|
1060
|
-
const data = JSON.parse(
|
|
1171
|
+
const pidFile = path5.join(os2.homedir(), ".node9", "daemon.pid");
|
|
1172
|
+
if (!fs3.existsSync(pidFile)) return null;
|
|
1173
|
+
const data = JSON.parse(fs3.readFileSync(pidFile, "utf-8"));
|
|
1061
1174
|
process.kill(data.pid, 0);
|
|
1062
1175
|
return data.internalToken ?? null;
|
|
1063
1176
|
} catch {
|
|
@@ -1172,9 +1285,9 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
1172
1285
|
}
|
|
1173
1286
|
async function explainPolicy(toolName, args) {
|
|
1174
1287
|
const steps = [];
|
|
1175
|
-
const globalPath =
|
|
1176
|
-
const projectPath =
|
|
1177
|
-
const credsPath =
|
|
1288
|
+
const globalPath = path5.join(os2.homedir(), ".node9", "config.json");
|
|
1289
|
+
const projectPath = path5.join(process.cwd(), "node9.config.json");
|
|
1290
|
+
const credsPath = path5.join(os2.homedir(), ".node9", "credentials.json");
|
|
1178
1291
|
const waterfall = [
|
|
1179
1292
|
{
|
|
1180
1293
|
tier: 1,
|
|
@@ -1185,19 +1298,19 @@ async function explainPolicy(toolName, args) {
|
|
|
1185
1298
|
{
|
|
1186
1299
|
tier: 2,
|
|
1187
1300
|
label: "Cloud policy",
|
|
1188
|
-
status:
|
|
1189
|
-
note:
|
|
1301
|
+
status: fs3.existsSync(credsPath) ? "active" : "missing",
|
|
1302
|
+
note: fs3.existsSync(credsPath) ? "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
|
|
1190
1303
|
},
|
|
1191
1304
|
{
|
|
1192
1305
|
tier: 3,
|
|
1193
1306
|
label: "Project config",
|
|
1194
|
-
status:
|
|
1307
|
+
status: fs3.existsSync(projectPath) ? "active" : "missing",
|
|
1195
1308
|
path: projectPath
|
|
1196
1309
|
},
|
|
1197
1310
|
{
|
|
1198
1311
|
tier: 4,
|
|
1199
1312
|
label: "Global config",
|
|
1200
|
-
status:
|
|
1313
|
+
status: fs3.existsSync(globalPath) ? "active" : "missing",
|
|
1201
1314
|
path: globalPath
|
|
1202
1315
|
},
|
|
1203
1316
|
{
|
|
@@ -1210,7 +1323,9 @@ async function explainPolicy(toolName, args) {
|
|
|
1210
1323
|
const config = getConfig();
|
|
1211
1324
|
const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
|
|
1212
1325
|
if (config.policy.dlp.enabled && (!wouldBeIgnored || config.policy.dlp.scanIgnoredTools)) {
|
|
1213
|
-
const
|
|
1326
|
+
const argsObjE = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
1327
|
+
const filePathE = String(argsObjE.file_path ?? argsObjE.path ?? argsObjE.filename ?? "");
|
|
1328
|
+
const dlpMatch = (filePathE ? scanFilePath(filePathE) : null) ?? (args !== void 0 ? scanArgs(args) : null);
|
|
1214
1329
|
if (dlpMatch) {
|
|
1215
1330
|
steps.push({
|
|
1216
1331
|
name: "DLP Content Scanner",
|
|
@@ -1433,10 +1548,10 @@ function isIgnoredTool(toolName) {
|
|
|
1433
1548
|
return matchesPattern(toolName, config.policy.ignoredTools);
|
|
1434
1549
|
}
|
|
1435
1550
|
function isDaemonRunning() {
|
|
1436
|
-
const pidFile =
|
|
1437
|
-
if (
|
|
1551
|
+
const pidFile = path5.join(os2.homedir(), ".node9", "daemon.pid");
|
|
1552
|
+
if (fs3.existsSync(pidFile)) {
|
|
1438
1553
|
try {
|
|
1439
|
-
const { pid, port } = JSON.parse(
|
|
1554
|
+
const { pid, port } = JSON.parse(fs3.readFileSync(pidFile, "utf-8"));
|
|
1440
1555
|
if (port !== DAEMON_PORT) return false;
|
|
1441
1556
|
process.kill(pid, 0);
|
|
1442
1557
|
return true;
|
|
@@ -1456,9 +1571,9 @@ function isDaemonRunning() {
|
|
|
1456
1571
|
}
|
|
1457
1572
|
function getPersistentDecision(toolName) {
|
|
1458
1573
|
try {
|
|
1459
|
-
const file =
|
|
1460
|
-
if (!
|
|
1461
|
-
const decisions = JSON.parse(
|
|
1574
|
+
const file = path5.join(os2.homedir(), ".node9", "decisions.json");
|
|
1575
|
+
if (!fs3.existsSync(file)) return null;
|
|
1576
|
+
const decisions = JSON.parse(fs3.readFileSync(file, "utf-8"));
|
|
1462
1577
|
const d = decisions[toolName];
|
|
1463
1578
|
if (d === "allow" || d === "deny") return d;
|
|
1464
1579
|
} catch {
|
|
@@ -1601,7 +1716,9 @@ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = fa
|
|
|
1601
1716
|
let policyMatchedWord;
|
|
1602
1717
|
let riskMetadata;
|
|
1603
1718
|
if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
|
|
1604
|
-
const
|
|
1719
|
+
const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
1720
|
+
const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
|
|
1721
|
+
const dlpMatch = (filePath ? scanFilePath(filePath) : null) ?? scanArgs(args);
|
|
1605
1722
|
if (dlpMatch) {
|
|
1606
1723
|
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
1607
1724
|
if (dlpMatch.severity === "block") {
|
|
@@ -1973,10 +2090,10 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
1973
2090
|
}
|
|
1974
2091
|
return finalResult;
|
|
1975
2092
|
}
|
|
1976
|
-
function getConfig() {
|
|
1977
|
-
if (cachedConfig) return cachedConfig;
|
|
1978
|
-
const globalPath =
|
|
1979
|
-
const projectPath =
|
|
2093
|
+
function getConfig(cwd) {
|
|
2094
|
+
if (!cwd && cachedConfig) return cachedConfig;
|
|
2095
|
+
const globalPath = path5.join(os2.homedir(), ".node9", "config.json");
|
|
2096
|
+
const projectPath = path5.join(cwd ?? process.cwd(), "node9.config.json");
|
|
1980
2097
|
const globalConfig = tryLoadConfig(globalPath);
|
|
1981
2098
|
const projectConfig = tryLoadConfig(projectPath);
|
|
1982
2099
|
const mergedSettings = {
|
|
@@ -2063,18 +2180,19 @@ function getConfig() {
|
|
|
2063
2180
|
mergedPolicy.snapshot.tools = [...new Set(mergedPolicy.snapshot.tools)];
|
|
2064
2181
|
mergedPolicy.snapshot.onlyPaths = [...new Set(mergedPolicy.snapshot.onlyPaths)];
|
|
2065
2182
|
mergedPolicy.snapshot.ignorePaths = [...new Set(mergedPolicy.snapshot.ignorePaths)];
|
|
2066
|
-
|
|
2183
|
+
const result = {
|
|
2067
2184
|
settings: mergedSettings,
|
|
2068
2185
|
policy: mergedPolicy,
|
|
2069
2186
|
environments: mergedEnvironments
|
|
2070
2187
|
};
|
|
2071
|
-
|
|
2188
|
+
if (!cwd) cachedConfig = result;
|
|
2189
|
+
return result;
|
|
2072
2190
|
}
|
|
2073
2191
|
function tryLoadConfig(filePath) {
|
|
2074
|
-
if (!
|
|
2192
|
+
if (!fs3.existsSync(filePath)) return null;
|
|
2075
2193
|
let raw;
|
|
2076
2194
|
try {
|
|
2077
|
-
raw = JSON.parse(
|
|
2195
|
+
raw = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
|
|
2078
2196
|
} catch (err) {
|
|
2079
2197
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2080
2198
|
process.stderr.write(
|
|
@@ -2136,9 +2254,9 @@ function getCredentials() {
|
|
|
2136
2254
|
};
|
|
2137
2255
|
}
|
|
2138
2256
|
try {
|
|
2139
|
-
const credPath =
|
|
2140
|
-
if (
|
|
2141
|
-
const creds = JSON.parse(
|
|
2257
|
+
const credPath = path5.join(os2.homedir(), ".node9", "credentials.json");
|
|
2258
|
+
if (fs3.existsSync(credPath)) {
|
|
2259
|
+
const creds = JSON.parse(fs3.readFileSync(credPath, "utf-8"));
|
|
2142
2260
|
const profileName = process.env.NODE9_PROFILE || "default";
|
|
2143
2261
|
const profile = creds[profileName];
|
|
2144
2262
|
if (profile?.apiKey) {
|
|
@@ -2251,7 +2369,7 @@ async function resolveNode9SaaS(requestId, creds, approved) {
|
|
|
2251
2369
|
} catch {
|
|
2252
2370
|
}
|
|
2253
2371
|
}
|
|
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;
|
|
2372
|
+
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
2373
|
var init_core = __esm({
|
|
2256
2374
|
"src/core.ts"() {
|
|
2257
2375
|
"use strict";
|
|
@@ -2260,10 +2378,13 @@ var init_core = __esm({
|
|
|
2260
2378
|
init_config_schema();
|
|
2261
2379
|
init_shields();
|
|
2262
2380
|
init_dlp();
|
|
2263
|
-
PAUSED_FILE =
|
|
2264
|
-
TRUST_FILE =
|
|
2265
|
-
LOCAL_AUDIT_LOG =
|
|
2266
|
-
HOOK_DEBUG_LOG =
|
|
2381
|
+
PAUSED_FILE = path5.join(os2.homedir(), ".node9", "PAUSED");
|
|
2382
|
+
TRUST_FILE = path5.join(os2.homedir(), ".node9", "trust.json");
|
|
2383
|
+
LOCAL_AUDIT_LOG = path5.join(os2.homedir(), ".node9", "audit.log");
|
|
2384
|
+
HOOK_DEBUG_LOG = path5.join(os2.homedir(), ".node9", "hook-debug.log");
|
|
2385
|
+
MAX_REGEX_LENGTH = 100;
|
|
2386
|
+
REGEX_CACHE_MAX = 500;
|
|
2387
|
+
regexCache = /* @__PURE__ */ new Map();
|
|
2267
2388
|
SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
2268
2389
|
DANGEROUS_WORDS = [
|
|
2269
2390
|
"mkfs",
|
|
@@ -2478,7 +2599,7 @@ var init_core = __esm({
|
|
|
2478
2599
|
cachedConfig = null;
|
|
2479
2600
|
DAEMON_PORT = 7391;
|
|
2480
2601
|
DAEMON_HOST = "127.0.0.1";
|
|
2481
|
-
ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
2602
|
+
ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path5.join(os2.tmpdir(), "node9-activity.sock");
|
|
2482
2603
|
}
|
|
2483
2604
|
});
|
|
2484
2605
|
|
|
@@ -3937,25 +4058,25 @@ var init_ui2 = __esm({
|
|
|
3937
4058
|
// src/daemon/index.ts
|
|
3938
4059
|
import http from "http";
|
|
3939
4060
|
import net2 from "net";
|
|
3940
|
-
import
|
|
3941
|
-
import
|
|
4061
|
+
import fs5 from "fs";
|
|
4062
|
+
import path7 from "path";
|
|
3942
4063
|
import os4 from "os";
|
|
3943
4064
|
import { spawn as spawn2, spawnSync as spawnSync2 } from "child_process";
|
|
3944
4065
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3945
4066
|
import chalk4 from "chalk";
|
|
3946
4067
|
function atomicWriteSync2(filePath, data, options) {
|
|
3947
|
-
const dir =
|
|
3948
|
-
if (!
|
|
4068
|
+
const dir = path7.dirname(filePath);
|
|
4069
|
+
if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
|
|
3949
4070
|
const tmpPath = `${filePath}.${randomUUID2()}.tmp`;
|
|
3950
|
-
|
|
3951
|
-
|
|
4071
|
+
fs5.writeFileSync(tmpPath, data, options);
|
|
4072
|
+
fs5.renameSync(tmpPath, filePath);
|
|
3952
4073
|
}
|
|
3953
4074
|
function writeTrustEntry(toolName, durationMs) {
|
|
3954
4075
|
try {
|
|
3955
4076
|
let trust = { entries: [] };
|
|
3956
4077
|
try {
|
|
3957
|
-
if (
|
|
3958
|
-
trust = JSON.parse(
|
|
4078
|
+
if (fs5.existsSync(TRUST_FILE2))
|
|
4079
|
+
trust = JSON.parse(fs5.readFileSync(TRUST_FILE2, "utf-8"));
|
|
3959
4080
|
} catch {
|
|
3960
4081
|
}
|
|
3961
4082
|
trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
|
|
@@ -3982,16 +4103,16 @@ function appendAuditLog(data) {
|
|
|
3982
4103
|
decision: data.decision,
|
|
3983
4104
|
source: "daemon"
|
|
3984
4105
|
};
|
|
3985
|
-
const dir =
|
|
3986
|
-
if (!
|
|
3987
|
-
|
|
4106
|
+
const dir = path7.dirname(AUDIT_LOG_FILE);
|
|
4107
|
+
if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
|
|
4108
|
+
fs5.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
|
|
3988
4109
|
} catch {
|
|
3989
4110
|
}
|
|
3990
4111
|
}
|
|
3991
4112
|
function getAuditHistory(limit = 20) {
|
|
3992
4113
|
try {
|
|
3993
|
-
if (!
|
|
3994
|
-
const lines =
|
|
4114
|
+
if (!fs5.existsSync(AUDIT_LOG_FILE)) return [];
|
|
4115
|
+
const lines = fs5.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
|
|
3995
4116
|
if (lines.length === 1 && lines[0] === "") return [];
|
|
3996
4117
|
return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
|
|
3997
4118
|
} catch {
|
|
@@ -4000,7 +4121,7 @@ function getAuditHistory(limit = 20) {
|
|
|
4000
4121
|
}
|
|
4001
4122
|
function getOrgName() {
|
|
4002
4123
|
try {
|
|
4003
|
-
if (
|
|
4124
|
+
if (fs5.existsSync(CREDENTIALS_FILE)) {
|
|
4004
4125
|
return "Node9 Cloud";
|
|
4005
4126
|
}
|
|
4006
4127
|
} catch {
|
|
@@ -4008,13 +4129,13 @@ function getOrgName() {
|
|
|
4008
4129
|
return null;
|
|
4009
4130
|
}
|
|
4010
4131
|
function hasStoredSlackKey() {
|
|
4011
|
-
return
|
|
4132
|
+
return fs5.existsSync(CREDENTIALS_FILE);
|
|
4012
4133
|
}
|
|
4013
4134
|
function writeGlobalSetting(key, value) {
|
|
4014
4135
|
let config = {};
|
|
4015
4136
|
try {
|
|
4016
|
-
if (
|
|
4017
|
-
config = JSON.parse(
|
|
4137
|
+
if (fs5.existsSync(GLOBAL_CONFIG_FILE)) {
|
|
4138
|
+
config = JSON.parse(fs5.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
|
|
4018
4139
|
}
|
|
4019
4140
|
} catch {
|
|
4020
4141
|
}
|
|
@@ -4033,7 +4154,7 @@ function abandonPending() {
|
|
|
4033
4154
|
});
|
|
4034
4155
|
if (autoStarted) {
|
|
4035
4156
|
try {
|
|
4036
|
-
|
|
4157
|
+
fs5.unlinkSync(DAEMON_PID_FILE);
|
|
4037
4158
|
} catch {
|
|
4038
4159
|
}
|
|
4039
4160
|
setTimeout(() => {
|
|
@@ -4083,8 +4204,8 @@ function readBody(req) {
|
|
|
4083
4204
|
}
|
|
4084
4205
|
function readPersistentDecisions() {
|
|
4085
4206
|
try {
|
|
4086
|
-
if (
|
|
4087
|
-
return JSON.parse(
|
|
4207
|
+
if (fs5.existsSync(DECISIONS_FILE)) {
|
|
4208
|
+
return JSON.parse(fs5.readFileSync(DECISIONS_FILE, "utf-8"));
|
|
4088
4209
|
}
|
|
4089
4210
|
} catch {
|
|
4090
4211
|
}
|
|
@@ -4113,7 +4234,7 @@ function startDaemon() {
|
|
|
4113
4234
|
idleTimer = setTimeout(() => {
|
|
4114
4235
|
if (autoStarted) {
|
|
4115
4236
|
try {
|
|
4116
|
-
|
|
4237
|
+
fs5.unlinkSync(DAEMON_PID_FILE);
|
|
4117
4238
|
} catch {
|
|
4118
4239
|
}
|
|
4119
4240
|
}
|
|
@@ -4498,14 +4619,14 @@ data: ${JSON.stringify(item.data)}
|
|
|
4498
4619
|
server.on("error", (e) => {
|
|
4499
4620
|
if (e.code === "EADDRINUSE") {
|
|
4500
4621
|
try {
|
|
4501
|
-
if (
|
|
4502
|
-
const { pid } = JSON.parse(
|
|
4622
|
+
if (fs5.existsSync(DAEMON_PID_FILE)) {
|
|
4623
|
+
const { pid } = JSON.parse(fs5.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4503
4624
|
process.kill(pid, 0);
|
|
4504
4625
|
return process.exit(0);
|
|
4505
4626
|
}
|
|
4506
4627
|
} catch {
|
|
4507
4628
|
try {
|
|
4508
|
-
|
|
4629
|
+
fs5.unlinkSync(DAEMON_PID_FILE);
|
|
4509
4630
|
} catch {
|
|
4510
4631
|
}
|
|
4511
4632
|
server.listen(DAEMON_PORT2, DAEMON_HOST2);
|
|
@@ -4556,7 +4677,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
4556
4677
|
console.log(chalk4.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
|
|
4557
4678
|
}
|
|
4558
4679
|
try {
|
|
4559
|
-
|
|
4680
|
+
fs5.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
4560
4681
|
} catch {
|
|
4561
4682
|
}
|
|
4562
4683
|
const ACTIVITY_MAX_BYTES = 1024 * 1024;
|
|
@@ -4598,30 +4719,30 @@ data: ${JSON.stringify(item.data)}
|
|
|
4598
4719
|
unixServer.listen(ACTIVITY_SOCKET_PATH2);
|
|
4599
4720
|
process.on("exit", () => {
|
|
4600
4721
|
try {
|
|
4601
|
-
|
|
4722
|
+
fs5.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
4602
4723
|
} catch {
|
|
4603
4724
|
}
|
|
4604
4725
|
});
|
|
4605
4726
|
}
|
|
4606
4727
|
function stopDaemon() {
|
|
4607
|
-
if (!
|
|
4728
|
+
if (!fs5.existsSync(DAEMON_PID_FILE)) return console.log(chalk4.yellow("Not running."));
|
|
4608
4729
|
try {
|
|
4609
|
-
const { pid } = JSON.parse(
|
|
4730
|
+
const { pid } = JSON.parse(fs5.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4610
4731
|
process.kill(pid, "SIGTERM");
|
|
4611
4732
|
console.log(chalk4.green("\u2705 Stopped."));
|
|
4612
4733
|
} catch {
|
|
4613
4734
|
console.log(chalk4.gray("Cleaned up stale PID file."));
|
|
4614
4735
|
} finally {
|
|
4615
4736
|
try {
|
|
4616
|
-
|
|
4737
|
+
fs5.unlinkSync(DAEMON_PID_FILE);
|
|
4617
4738
|
} catch {
|
|
4618
4739
|
}
|
|
4619
4740
|
}
|
|
4620
4741
|
}
|
|
4621
4742
|
function daemonStatus() {
|
|
4622
|
-
if (
|
|
4743
|
+
if (fs5.existsSync(DAEMON_PID_FILE)) {
|
|
4623
4744
|
try {
|
|
4624
|
-
const { pid } = JSON.parse(
|
|
4745
|
+
const { pid } = JSON.parse(fs5.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4625
4746
|
process.kill(pid, 0);
|
|
4626
4747
|
console.log(chalk4.green("Node9 daemon: running"));
|
|
4627
4748
|
return;
|
|
@@ -4647,16 +4768,16 @@ var init_daemon = __esm({
|
|
|
4647
4768
|
init_ui2();
|
|
4648
4769
|
init_core();
|
|
4649
4770
|
init_shields();
|
|
4650
|
-
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
4771
|
+
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path7.join(os4.tmpdir(), "node9-activity.sock");
|
|
4651
4772
|
DAEMON_PORT2 = 7391;
|
|
4652
4773
|
DAEMON_HOST2 = "127.0.0.1";
|
|
4653
4774
|
homeDir = os4.homedir();
|
|
4654
|
-
DAEMON_PID_FILE =
|
|
4655
|
-
DECISIONS_FILE =
|
|
4656
|
-
GLOBAL_CONFIG_FILE =
|
|
4657
|
-
CREDENTIALS_FILE =
|
|
4658
|
-
AUDIT_LOG_FILE =
|
|
4659
|
-
TRUST_FILE2 =
|
|
4775
|
+
DAEMON_PID_FILE = path7.join(homeDir, ".node9", "daemon.pid");
|
|
4776
|
+
DECISIONS_FILE = path7.join(homeDir, ".node9", "decisions.json");
|
|
4777
|
+
GLOBAL_CONFIG_FILE = path7.join(homeDir, ".node9", "config.json");
|
|
4778
|
+
CREDENTIALS_FILE = path7.join(homeDir, ".node9", "credentials.json");
|
|
4779
|
+
AUDIT_LOG_FILE = path7.join(homeDir, ".node9", "audit.log");
|
|
4780
|
+
TRUST_FILE2 = path7.join(homeDir, ".node9", "trust.json");
|
|
4660
4781
|
TRUST_DURATIONS = {
|
|
4661
4782
|
"30m": 30 * 6e4,
|
|
4662
4783
|
"1h": 60 * 6e4,
|
|
@@ -4682,11 +4803,11 @@ __export(tail_exports, {
|
|
|
4682
4803
|
});
|
|
4683
4804
|
import http2 from "http";
|
|
4684
4805
|
import chalk5 from "chalk";
|
|
4685
|
-
import
|
|
4806
|
+
import fs7 from "fs";
|
|
4686
4807
|
import os6 from "os";
|
|
4687
|
-
import
|
|
4808
|
+
import path9 from "path";
|
|
4688
4809
|
import readline from "readline";
|
|
4689
|
-
import { spawn as
|
|
4810
|
+
import { spawn as spawn4 } from "child_process";
|
|
4690
4811
|
function getIcon(tool) {
|
|
4691
4812
|
const t = tool.toLowerCase();
|
|
4692
4813
|
for (const [k, v] of Object.entries(ICONS)) {
|
|
@@ -4724,9 +4845,9 @@ function renderPending(activity) {
|
|
|
4724
4845
|
}
|
|
4725
4846
|
async function ensureDaemon() {
|
|
4726
4847
|
let pidPort = null;
|
|
4727
|
-
if (
|
|
4848
|
+
if (fs7.existsSync(PID_FILE)) {
|
|
4728
4849
|
try {
|
|
4729
|
-
const { port } = JSON.parse(
|
|
4850
|
+
const { port } = JSON.parse(fs7.readFileSync(PID_FILE, "utf-8"));
|
|
4730
4851
|
pidPort = port;
|
|
4731
4852
|
} catch {
|
|
4732
4853
|
console.error(chalk5.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
@@ -4741,7 +4862,7 @@ async function ensureDaemon() {
|
|
|
4741
4862
|
} catch {
|
|
4742
4863
|
}
|
|
4743
4864
|
console.log(chalk5.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
4744
|
-
const child =
|
|
4865
|
+
const child = spawn4(process.execPath, [process.argv[1], "daemon"], {
|
|
4745
4866
|
detached: true,
|
|
4746
4867
|
stdio: "ignore",
|
|
4747
4868
|
env: { ...process.env, NODE9_AUTO_STARTED: "1" }
|
|
@@ -4886,7 +5007,7 @@ var init_tail = __esm({
|
|
|
4886
5007
|
"src/tui/tail.ts"() {
|
|
4887
5008
|
"use strict";
|
|
4888
5009
|
init_daemon();
|
|
4889
|
-
PID_FILE =
|
|
5010
|
+
PID_FILE = path9.join(os6.homedir(), ".node9", "daemon.pid");
|
|
4890
5011
|
ICONS = {
|
|
4891
5012
|
bash: "\u{1F4BB}",
|
|
4892
5013
|
shell: "\u{1F4BB}",
|
|
@@ -4912,8 +5033,8 @@ init_core();
|
|
|
4912
5033
|
import { Command } from "commander";
|
|
4913
5034
|
|
|
4914
5035
|
// src/setup.ts
|
|
4915
|
-
import
|
|
4916
|
-
import
|
|
5036
|
+
import fs4 from "fs";
|
|
5037
|
+
import path6 from "path";
|
|
4917
5038
|
import os3 from "os";
|
|
4918
5039
|
import chalk3 from "chalk";
|
|
4919
5040
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
@@ -4931,17 +5052,17 @@ function fullPathCommand(subcommand) {
|
|
|
4931
5052
|
}
|
|
4932
5053
|
function readJson(filePath) {
|
|
4933
5054
|
try {
|
|
4934
|
-
if (
|
|
4935
|
-
return JSON.parse(
|
|
5055
|
+
if (fs4.existsSync(filePath)) {
|
|
5056
|
+
return JSON.parse(fs4.readFileSync(filePath, "utf-8"));
|
|
4936
5057
|
}
|
|
4937
5058
|
} catch {
|
|
4938
5059
|
}
|
|
4939
5060
|
return null;
|
|
4940
5061
|
}
|
|
4941
5062
|
function writeJson(filePath, data) {
|
|
4942
|
-
const dir =
|
|
4943
|
-
if (!
|
|
4944
|
-
|
|
5063
|
+
const dir = path6.dirname(filePath);
|
|
5064
|
+
if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
|
|
5065
|
+
fs4.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
4945
5066
|
}
|
|
4946
5067
|
function isNode9Hook(cmd) {
|
|
4947
5068
|
if (!cmd) return false;
|
|
@@ -4949,8 +5070,8 @@ function isNode9Hook(cmd) {
|
|
|
4949
5070
|
}
|
|
4950
5071
|
function teardownClaude() {
|
|
4951
5072
|
const homeDir2 = os3.homedir();
|
|
4952
|
-
const hooksPath =
|
|
4953
|
-
const mcpPath =
|
|
5073
|
+
const hooksPath = path6.join(homeDir2, ".claude", "settings.json");
|
|
5074
|
+
const mcpPath = path6.join(homeDir2, ".claude.json");
|
|
4954
5075
|
let changed = false;
|
|
4955
5076
|
const settings = readJson(hooksPath);
|
|
4956
5077
|
if (settings?.hooks) {
|
|
@@ -4999,7 +5120,7 @@ function teardownClaude() {
|
|
|
4999
5120
|
}
|
|
5000
5121
|
function teardownGemini() {
|
|
5001
5122
|
const homeDir2 = os3.homedir();
|
|
5002
|
-
const settingsPath =
|
|
5123
|
+
const settingsPath = path6.join(homeDir2, ".gemini", "settings.json");
|
|
5003
5124
|
const settings = readJson(settingsPath);
|
|
5004
5125
|
if (!settings) {
|
|
5005
5126
|
console.log(chalk3.blue(" \u2139\uFE0F ~/.gemini/settings.json not found \u2014 nothing to remove"));
|
|
@@ -5038,7 +5159,7 @@ function teardownGemini() {
|
|
|
5038
5159
|
}
|
|
5039
5160
|
function teardownCursor() {
|
|
5040
5161
|
const homeDir2 = os3.homedir();
|
|
5041
|
-
const mcpPath =
|
|
5162
|
+
const mcpPath = path6.join(homeDir2, ".cursor", "mcp.json");
|
|
5042
5163
|
const mcpConfig = readJson(mcpPath);
|
|
5043
5164
|
if (!mcpConfig?.mcpServers) {
|
|
5044
5165
|
console.log(chalk3.blue(" \u2139\uFE0F ~/.cursor/mcp.json not found \u2014 nothing to remove"));
|
|
@@ -5065,8 +5186,8 @@ function teardownCursor() {
|
|
|
5065
5186
|
}
|
|
5066
5187
|
async function setupClaude() {
|
|
5067
5188
|
const homeDir2 = os3.homedir();
|
|
5068
|
-
const mcpPath =
|
|
5069
|
-
const hooksPath =
|
|
5189
|
+
const mcpPath = path6.join(homeDir2, ".claude.json");
|
|
5190
|
+
const hooksPath = path6.join(homeDir2, ".claude", "settings.json");
|
|
5070
5191
|
const claudeConfig = readJson(mcpPath) ?? {};
|
|
5071
5192
|
const settings = readJson(hooksPath) ?? {};
|
|
5072
5193
|
const servers = claudeConfig.mcpServers ?? {};
|
|
@@ -5141,7 +5262,7 @@ async function setupClaude() {
|
|
|
5141
5262
|
}
|
|
5142
5263
|
async function setupGemini() {
|
|
5143
5264
|
const homeDir2 = os3.homedir();
|
|
5144
|
-
const settingsPath =
|
|
5265
|
+
const settingsPath = path6.join(homeDir2, ".gemini", "settings.json");
|
|
5145
5266
|
const settings = readJson(settingsPath) ?? {};
|
|
5146
5267
|
const servers = settings.mcpServers ?? {};
|
|
5147
5268
|
let anythingChanged = false;
|
|
@@ -5224,7 +5345,7 @@ async function setupGemini() {
|
|
|
5224
5345
|
}
|
|
5225
5346
|
async function setupCursor() {
|
|
5226
5347
|
const homeDir2 = os3.homedir();
|
|
5227
|
-
const mcpPath =
|
|
5348
|
+
const mcpPath = path6.join(homeDir2, ".cursor", "mcp.json");
|
|
5228
5349
|
const mcpConfig = readJson(mcpPath) ?? {};
|
|
5229
5350
|
const servers = mcpConfig.mcpServers ?? {};
|
|
5230
5351
|
let anythingChanged = false;
|
|
@@ -5280,35 +5401,37 @@ async function setupCursor() {
|
|
|
5280
5401
|
|
|
5281
5402
|
// src/cli.ts
|
|
5282
5403
|
init_daemon();
|
|
5283
|
-
import { spawn as
|
|
5404
|
+
import { spawn as spawn5, execSync } from "child_process";
|
|
5284
5405
|
import { parseCommandString } from "execa";
|
|
5285
5406
|
import { execa } from "execa";
|
|
5286
5407
|
import chalk6 from "chalk";
|
|
5287
5408
|
import readline2 from "readline";
|
|
5288
|
-
import
|
|
5289
|
-
import
|
|
5409
|
+
import fs8 from "fs";
|
|
5410
|
+
import path10 from "path";
|
|
5290
5411
|
import os7 from "os";
|
|
5291
5412
|
|
|
5292
5413
|
// src/undo.ts
|
|
5293
|
-
import { spawnSync as spawnSync3 } from "child_process";
|
|
5294
|
-
import
|
|
5295
|
-
import
|
|
5414
|
+
import { spawnSync as spawnSync3, spawn as spawn3 } from "child_process";
|
|
5415
|
+
import crypto2 from "crypto";
|
|
5416
|
+
import fs6 from "fs";
|
|
5417
|
+
import path8 from "path";
|
|
5296
5418
|
import os5 from "os";
|
|
5297
|
-
var SNAPSHOT_STACK_PATH =
|
|
5298
|
-
var UNDO_LATEST_PATH =
|
|
5419
|
+
var SNAPSHOT_STACK_PATH = path8.join(os5.homedir(), ".node9", "snapshots.json");
|
|
5420
|
+
var UNDO_LATEST_PATH = path8.join(os5.homedir(), ".node9", "undo_latest.txt");
|
|
5299
5421
|
var MAX_SNAPSHOTS = 10;
|
|
5422
|
+
var GIT_TIMEOUT = 15e3;
|
|
5300
5423
|
function readStack() {
|
|
5301
5424
|
try {
|
|
5302
|
-
if (
|
|
5303
|
-
return JSON.parse(
|
|
5425
|
+
if (fs6.existsSync(SNAPSHOT_STACK_PATH))
|
|
5426
|
+
return JSON.parse(fs6.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
|
|
5304
5427
|
} catch {
|
|
5305
5428
|
}
|
|
5306
5429
|
return [];
|
|
5307
5430
|
}
|
|
5308
5431
|
function writeStack(stack) {
|
|
5309
|
-
const dir =
|
|
5310
|
-
if (!
|
|
5311
|
-
|
|
5432
|
+
const dir = path8.dirname(SNAPSHOT_STACK_PATH);
|
|
5433
|
+
if (!fs6.existsSync(dir)) fs6.mkdirSync(dir, { recursive: true });
|
|
5434
|
+
fs6.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
5312
5435
|
}
|
|
5313
5436
|
function buildArgsSummary(tool, args) {
|
|
5314
5437
|
if (!args || typeof args !== "object") return "";
|
|
@@ -5321,54 +5444,177 @@ function buildArgsSummary(tool, args) {
|
|
|
5321
5444
|
if (typeof sql === "string") return sql.slice(0, 80);
|
|
5322
5445
|
return tool;
|
|
5323
5446
|
}
|
|
5324
|
-
|
|
5447
|
+
function normalizeCwdForHash(cwd) {
|
|
5448
|
+
let normalized;
|
|
5449
|
+
try {
|
|
5450
|
+
normalized = fs6.realpathSync(cwd);
|
|
5451
|
+
} catch {
|
|
5452
|
+
normalized = cwd;
|
|
5453
|
+
}
|
|
5454
|
+
normalized = normalized.replace(/\\/g, "/");
|
|
5455
|
+
if (process.platform === "win32") normalized = normalized.toLowerCase();
|
|
5456
|
+
return normalized;
|
|
5457
|
+
}
|
|
5458
|
+
function getShadowRepoDir(cwd) {
|
|
5459
|
+
const hash = crypto2.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
|
|
5460
|
+
return path8.join(os5.homedir(), ".node9", "snapshots", hash);
|
|
5461
|
+
}
|
|
5462
|
+
function cleanOrphanedIndexFiles(shadowDir) {
|
|
5463
|
+
try {
|
|
5464
|
+
const cutoff = Date.now() - 6e4;
|
|
5465
|
+
for (const f of fs6.readdirSync(shadowDir)) {
|
|
5466
|
+
if (f.startsWith("index_")) {
|
|
5467
|
+
const fp = path8.join(shadowDir, f);
|
|
5468
|
+
try {
|
|
5469
|
+
if (fs6.statSync(fp).mtimeMs < cutoff) fs6.unlinkSync(fp);
|
|
5470
|
+
} catch {
|
|
5471
|
+
}
|
|
5472
|
+
}
|
|
5473
|
+
}
|
|
5474
|
+
} catch {
|
|
5475
|
+
}
|
|
5476
|
+
}
|
|
5477
|
+
function writeShadowExcludes(shadowDir, ignorePaths) {
|
|
5478
|
+
const hardcoded = [".git", ".node9"];
|
|
5479
|
+
const lines = [...hardcoded, ...ignorePaths].join("\n");
|
|
5480
|
+
try {
|
|
5481
|
+
fs6.writeFileSync(path8.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
|
|
5482
|
+
} catch {
|
|
5483
|
+
}
|
|
5484
|
+
}
|
|
5485
|
+
function ensureShadowRepo(shadowDir, cwd) {
|
|
5486
|
+
cleanOrphanedIndexFiles(shadowDir);
|
|
5487
|
+
const normalizedCwd = normalizeCwdForHash(cwd);
|
|
5488
|
+
const shadowEnvBase = { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd };
|
|
5489
|
+
const check = spawnSync3("git", ["rev-parse", "--git-dir"], {
|
|
5490
|
+
env: shadowEnvBase,
|
|
5491
|
+
timeout: 3e3
|
|
5492
|
+
});
|
|
5493
|
+
if (check.status === 0) {
|
|
5494
|
+
const ptPath = path8.join(shadowDir, "project-path.txt");
|
|
5495
|
+
try {
|
|
5496
|
+
const stored = fs6.readFileSync(ptPath, "utf8").trim();
|
|
5497
|
+
if (stored === normalizedCwd) return true;
|
|
5498
|
+
if (process.env.NODE9_DEBUG === "1")
|
|
5499
|
+
console.error(
|
|
5500
|
+
`[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
|
|
5501
|
+
);
|
|
5502
|
+
fs6.rmSync(shadowDir, { recursive: true, force: true });
|
|
5503
|
+
} catch {
|
|
5504
|
+
try {
|
|
5505
|
+
fs6.writeFileSync(ptPath, normalizedCwd, "utf8");
|
|
5506
|
+
} catch {
|
|
5507
|
+
}
|
|
5508
|
+
return true;
|
|
5509
|
+
}
|
|
5510
|
+
}
|
|
5511
|
+
try {
|
|
5512
|
+
fs6.mkdirSync(shadowDir, { recursive: true });
|
|
5513
|
+
} catch {
|
|
5514
|
+
}
|
|
5515
|
+
const init = spawnSync3("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
|
|
5516
|
+
if (init.status !== 0) {
|
|
5517
|
+
if (process.env.NODE9_DEBUG === "1")
|
|
5518
|
+
console.error("[Node9] git init --bare failed:", init.stderr?.toString());
|
|
5519
|
+
return false;
|
|
5520
|
+
}
|
|
5521
|
+
const configFile = path8.join(shadowDir, "config");
|
|
5522
|
+
spawnSync3("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
|
|
5523
|
+
timeout: 3e3
|
|
5524
|
+
});
|
|
5525
|
+
spawnSync3("git", ["config", "--file", configFile, "core.fsmonitor", "true"], {
|
|
5526
|
+
timeout: 3e3
|
|
5527
|
+
});
|
|
5528
|
+
try {
|
|
5529
|
+
fs6.writeFileSync(path8.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
|
|
5530
|
+
} catch {
|
|
5531
|
+
}
|
|
5532
|
+
return true;
|
|
5533
|
+
}
|
|
5534
|
+
function buildGitEnv(cwd) {
|
|
5535
|
+
const shadowDir = getShadowRepoDir(cwd);
|
|
5536
|
+
const check = spawnSync3("git", ["rev-parse", "--git-dir"], {
|
|
5537
|
+
env: { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd },
|
|
5538
|
+
timeout: 2e3
|
|
5539
|
+
});
|
|
5540
|
+
if (check.status === 0) {
|
|
5541
|
+
return { ...process.env, GIT_DIR: shadowDir, GIT_WORK_TREE: cwd };
|
|
5542
|
+
}
|
|
5543
|
+
return { ...process.env };
|
|
5544
|
+
}
|
|
5545
|
+
async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = []) {
|
|
5546
|
+
let indexFile = null;
|
|
5325
5547
|
try {
|
|
5326
5548
|
const cwd = process.cwd();
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
const
|
|
5332
|
-
|
|
5333
|
-
|
|
5549
|
+
const shadowDir = getShadowRepoDir(cwd);
|
|
5550
|
+
if (!ensureShadowRepo(shadowDir, cwd)) return null;
|
|
5551
|
+
writeShadowExcludes(shadowDir, ignorePaths);
|
|
5552
|
+
indexFile = path8.join(shadowDir, `index_${process.pid}_${Date.now()}`);
|
|
5553
|
+
const shadowEnv = {
|
|
5554
|
+
...process.env,
|
|
5555
|
+
GIT_DIR: shadowDir,
|
|
5556
|
+
GIT_WORK_TREE: cwd,
|
|
5557
|
+
GIT_INDEX_FILE: indexFile
|
|
5558
|
+
};
|
|
5559
|
+
spawnSync3("git", ["add", "-A"], { env: shadowEnv, timeout: GIT_TIMEOUT });
|
|
5560
|
+
const treeRes = spawnSync3("git", ["write-tree"], { env: shadowEnv, timeout: GIT_TIMEOUT });
|
|
5561
|
+
const treeHash = treeRes.stdout?.toString().trim();
|
|
5334
5562
|
if (!treeHash || treeRes.status !== 0) return null;
|
|
5335
|
-
const commitRes = spawnSync3(
|
|
5336
|
-
"
|
|
5337
|
-
treeHash,
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
const commitHash = commitRes.stdout.toString().trim();
|
|
5563
|
+
const commitRes = spawnSync3(
|
|
5564
|
+
"git",
|
|
5565
|
+
["commit-tree", treeHash, "-m", `Node9 AI Snapshot: ${(/* @__PURE__ */ new Date()).toISOString()}`],
|
|
5566
|
+
{ env: shadowEnv, timeout: GIT_TIMEOUT }
|
|
5567
|
+
);
|
|
5568
|
+
const commitHash = commitRes.stdout?.toString().trim();
|
|
5342
5569
|
if (!commitHash || commitRes.status !== 0) return null;
|
|
5343
5570
|
const stack = readStack();
|
|
5344
|
-
|
|
5571
|
+
stack.push({
|
|
5345
5572
|
hash: commitHash,
|
|
5346
5573
|
tool,
|
|
5347
5574
|
argsSummary: buildArgsSummary(tool, args),
|
|
5348
5575
|
cwd,
|
|
5349
5576
|
timestamp: Date.now()
|
|
5350
|
-
};
|
|
5351
|
-
stack.
|
|
5577
|
+
});
|
|
5578
|
+
const shouldGc = stack.length % 5 === 0;
|
|
5352
5579
|
if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
|
|
5353
5580
|
writeStack(stack);
|
|
5354
|
-
|
|
5581
|
+
fs6.writeFileSync(UNDO_LATEST_PATH, commitHash);
|
|
5582
|
+
if (shouldGc) {
|
|
5583
|
+
spawn3("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
|
|
5584
|
+
}
|
|
5355
5585
|
return commitHash;
|
|
5356
5586
|
} catch (err) {
|
|
5357
5587
|
if (process.env.NODE9_DEBUG === "1") console.error("[Node9 Undo Engine Error]:", err);
|
|
5588
|
+
return null;
|
|
5589
|
+
} finally {
|
|
5590
|
+
if (indexFile) {
|
|
5591
|
+
try {
|
|
5592
|
+
fs6.unlinkSync(indexFile);
|
|
5593
|
+
} catch {
|
|
5594
|
+
}
|
|
5595
|
+
}
|
|
5358
5596
|
}
|
|
5359
|
-
return null;
|
|
5360
5597
|
}
|
|
5361
5598
|
function getSnapshotHistory() {
|
|
5362
5599
|
return readStack();
|
|
5363
5600
|
}
|
|
5364
5601
|
function computeUndoDiff(hash, cwd) {
|
|
5365
5602
|
try {
|
|
5366
|
-
const
|
|
5367
|
-
const
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5603
|
+
const env = buildGitEnv(cwd);
|
|
5604
|
+
const statRes = spawnSync3("git", ["diff", hash, "--stat", "--", "."], {
|
|
5605
|
+
cwd,
|
|
5606
|
+
env,
|
|
5607
|
+
timeout: GIT_TIMEOUT
|
|
5608
|
+
});
|
|
5609
|
+
const stat = statRes.stdout?.toString().trim();
|
|
5610
|
+
if (!stat || statRes.status !== 0) return null;
|
|
5611
|
+
const diffRes = spawnSync3("git", ["diff", hash, "--", "."], {
|
|
5612
|
+
cwd,
|
|
5613
|
+
env,
|
|
5614
|
+
timeout: GIT_TIMEOUT
|
|
5615
|
+
});
|
|
5616
|
+
const raw = diffRes.stdout?.toString();
|
|
5617
|
+
if (!raw || diffRes.status !== 0) return null;
|
|
5372
5618
|
const lines = raw.split("\n").filter(
|
|
5373
5619
|
(l) => !l.startsWith("diff --git") && !l.startsWith("index ") && !l.startsWith("Binary")
|
|
5374
5620
|
);
|
|
@@ -5380,18 +5626,41 @@ function computeUndoDiff(hash, cwd) {
|
|
|
5380
5626
|
function applyUndo(hash, cwd) {
|
|
5381
5627
|
try {
|
|
5382
5628
|
const dir = cwd ?? process.cwd();
|
|
5629
|
+
const env = buildGitEnv(dir);
|
|
5383
5630
|
const restore = spawnSync3("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
|
|
5384
|
-
cwd: dir
|
|
5631
|
+
cwd: dir,
|
|
5632
|
+
env,
|
|
5633
|
+
timeout: GIT_TIMEOUT
|
|
5385
5634
|
});
|
|
5386
5635
|
if (restore.status !== 0) return false;
|
|
5387
|
-
const lsTree = spawnSync3("git", ["ls-tree", "-r", "--name-only", hash], {
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5636
|
+
const lsTree = spawnSync3("git", ["ls-tree", "-r", "--name-only", hash], {
|
|
5637
|
+
cwd: dir,
|
|
5638
|
+
env,
|
|
5639
|
+
timeout: GIT_TIMEOUT
|
|
5640
|
+
});
|
|
5641
|
+
if (lsTree.status !== 0) {
|
|
5642
|
+
process.stderr.write(`[Node9] applyUndo: git ls-tree failed for hash ${hash}
|
|
5643
|
+
`);
|
|
5644
|
+
return false;
|
|
5645
|
+
}
|
|
5646
|
+
const snapshotFiles = new Set(
|
|
5647
|
+
lsTree.stdout?.toString().trim().split("\n").filter(Boolean) ?? []
|
|
5648
|
+
);
|
|
5649
|
+
if (snapshotFiles.size === 0) {
|
|
5650
|
+
process.stderr.write(`[Node9] applyUndo: ls-tree returned no files for hash ${hash}
|
|
5651
|
+
`);
|
|
5652
|
+
return false;
|
|
5653
|
+
}
|
|
5654
|
+
const tracked = spawnSync3("git", ["ls-files"], { cwd: dir, env, timeout: GIT_TIMEOUT }).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
5655
|
+
const untracked = spawnSync3("git", ["ls-files", "--others", "--exclude-standard"], {
|
|
5656
|
+
cwd: dir,
|
|
5657
|
+
env,
|
|
5658
|
+
timeout: GIT_TIMEOUT
|
|
5659
|
+
}).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
5391
5660
|
for (const file of [...tracked, ...untracked]) {
|
|
5392
|
-
const fullPath =
|
|
5393
|
-
if (!snapshotFiles.has(file) &&
|
|
5394
|
-
|
|
5661
|
+
const fullPath = path8.join(dir, file);
|
|
5662
|
+
if (!snapshotFiles.has(file) && fs6.existsSync(fullPath)) {
|
|
5663
|
+
fs6.unlinkSync(fullPath);
|
|
5395
5664
|
}
|
|
5396
5665
|
}
|
|
5397
5666
|
return true;
|
|
@@ -5404,7 +5673,7 @@ function applyUndo(hash, cwd) {
|
|
|
5404
5673
|
init_shields();
|
|
5405
5674
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
5406
5675
|
var { version } = JSON.parse(
|
|
5407
|
-
|
|
5676
|
+
fs8.readFileSync(path10.join(__dirname, "../package.json"), "utf-8")
|
|
5408
5677
|
);
|
|
5409
5678
|
function parseDuration(str) {
|
|
5410
5679
|
const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
|
|
@@ -5500,7 +5769,7 @@ function openBrowserLocal() {
|
|
|
5500
5769
|
}
|
|
5501
5770
|
async function autoStartDaemonAndWait() {
|
|
5502
5771
|
try {
|
|
5503
|
-
const child =
|
|
5772
|
+
const child = spawn5("node9", ["daemon"], {
|
|
5504
5773
|
detached: true,
|
|
5505
5774
|
stdio: "ignore",
|
|
5506
5775
|
env: { ...process.env, NODE9_AUTO_STARTED: "1" }
|
|
@@ -5536,8 +5805,8 @@ async function runProxy(targetCommand) {
|
|
|
5536
5805
|
if (stdout) executable = stdout.trim();
|
|
5537
5806
|
} catch {
|
|
5538
5807
|
}
|
|
5539
|
-
console.
|
|
5540
|
-
const child =
|
|
5808
|
+
console.error(chalk6.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
|
|
5809
|
+
const child = spawn5(executable, args, {
|
|
5541
5810
|
stdio: ["pipe", "pipe", "inherit"],
|
|
5542
5811
|
// We control STDIN and STDOUT
|
|
5543
5812
|
shell: false,
|
|
@@ -5606,14 +5875,14 @@ async function runProxy(targetCommand) {
|
|
|
5606
5875
|
}
|
|
5607
5876
|
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) => {
|
|
5608
5877
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
5609
|
-
const credPath =
|
|
5610
|
-
if (!
|
|
5611
|
-
|
|
5878
|
+
const credPath = path10.join(os7.homedir(), ".node9", "credentials.json");
|
|
5879
|
+
if (!fs8.existsSync(path10.dirname(credPath)))
|
|
5880
|
+
fs8.mkdirSync(path10.dirname(credPath), { recursive: true });
|
|
5612
5881
|
const profileName = options.profile || "default";
|
|
5613
5882
|
let existingCreds = {};
|
|
5614
5883
|
try {
|
|
5615
|
-
if (
|
|
5616
|
-
const raw = JSON.parse(
|
|
5884
|
+
if (fs8.existsSync(credPath)) {
|
|
5885
|
+
const raw = JSON.parse(fs8.readFileSync(credPath, "utf-8"));
|
|
5617
5886
|
if (raw.apiKey) {
|
|
5618
5887
|
existingCreds = {
|
|
5619
5888
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
|
|
@@ -5625,13 +5894,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
5625
5894
|
} catch {
|
|
5626
5895
|
}
|
|
5627
5896
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
5628
|
-
|
|
5897
|
+
fs8.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
5629
5898
|
if (profileName === "default") {
|
|
5630
|
-
const configPath =
|
|
5899
|
+
const configPath = path10.join(os7.homedir(), ".node9", "config.json");
|
|
5631
5900
|
let config = {};
|
|
5632
5901
|
try {
|
|
5633
|
-
if (
|
|
5634
|
-
config = JSON.parse(
|
|
5902
|
+
if (fs8.existsSync(configPath))
|
|
5903
|
+
config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
|
|
5635
5904
|
} catch {
|
|
5636
5905
|
}
|
|
5637
5906
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -5646,9 +5915,9 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
5646
5915
|
approvers.cloud = false;
|
|
5647
5916
|
}
|
|
5648
5917
|
s.approvers = approvers;
|
|
5649
|
-
if (!
|
|
5650
|
-
|
|
5651
|
-
|
|
5918
|
+
if (!fs8.existsSync(path10.dirname(configPath)))
|
|
5919
|
+
fs8.mkdirSync(path10.dirname(configPath), { recursive: true });
|
|
5920
|
+
fs8.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
5652
5921
|
}
|
|
5653
5922
|
if (options.profile && profileName !== "default") {
|
|
5654
5923
|
console.log(chalk6.green(`\u2705 Profile "${profileName}" saved`));
|
|
@@ -5734,15 +6003,15 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
5734
6003
|
}
|
|
5735
6004
|
}
|
|
5736
6005
|
if (options.purge) {
|
|
5737
|
-
const node9Dir =
|
|
5738
|
-
if (
|
|
6006
|
+
const node9Dir = path10.join(os7.homedir(), ".node9");
|
|
6007
|
+
if (fs8.existsSync(node9Dir)) {
|
|
5739
6008
|
const confirmed = await confirm3({
|
|
5740
6009
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
5741
6010
|
default: false
|
|
5742
6011
|
});
|
|
5743
6012
|
if (confirmed) {
|
|
5744
|
-
|
|
5745
|
-
if (
|
|
6013
|
+
fs8.rmSync(node9Dir, { recursive: true });
|
|
6014
|
+
if (fs8.existsSync(node9Dir)) {
|
|
5746
6015
|
console.error(
|
|
5747
6016
|
chalk6.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
5748
6017
|
);
|
|
@@ -5814,10 +6083,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
5814
6083
|
);
|
|
5815
6084
|
}
|
|
5816
6085
|
section("Configuration");
|
|
5817
|
-
const globalConfigPath =
|
|
5818
|
-
if (
|
|
6086
|
+
const globalConfigPath = path10.join(homeDir2, ".node9", "config.json");
|
|
6087
|
+
if (fs8.existsSync(globalConfigPath)) {
|
|
5819
6088
|
try {
|
|
5820
|
-
JSON.parse(
|
|
6089
|
+
JSON.parse(fs8.readFileSync(globalConfigPath, "utf-8"));
|
|
5821
6090
|
pass("~/.node9/config.json found and valid");
|
|
5822
6091
|
} catch {
|
|
5823
6092
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -5825,17 +6094,17 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
5825
6094
|
} else {
|
|
5826
6095
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
5827
6096
|
}
|
|
5828
|
-
const projectConfigPath =
|
|
5829
|
-
if (
|
|
6097
|
+
const projectConfigPath = path10.join(process.cwd(), "node9.config.json");
|
|
6098
|
+
if (fs8.existsSync(projectConfigPath)) {
|
|
5830
6099
|
try {
|
|
5831
|
-
JSON.parse(
|
|
6100
|
+
JSON.parse(fs8.readFileSync(projectConfigPath, "utf-8"));
|
|
5832
6101
|
pass("node9.config.json found and valid (project)");
|
|
5833
6102
|
} catch {
|
|
5834
6103
|
fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
|
|
5835
6104
|
}
|
|
5836
6105
|
}
|
|
5837
|
-
const credsPath =
|
|
5838
|
-
if (
|
|
6106
|
+
const credsPath = path10.join(homeDir2, ".node9", "credentials.json");
|
|
6107
|
+
if (fs8.existsSync(credsPath)) {
|
|
5839
6108
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
5840
6109
|
} else {
|
|
5841
6110
|
warn(
|
|
@@ -5844,10 +6113,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
5844
6113
|
);
|
|
5845
6114
|
}
|
|
5846
6115
|
section("Agent Hooks");
|
|
5847
|
-
const claudeSettingsPath =
|
|
5848
|
-
if (
|
|
6116
|
+
const claudeSettingsPath = path10.join(homeDir2, ".claude", "settings.json");
|
|
6117
|
+
if (fs8.existsSync(claudeSettingsPath)) {
|
|
5849
6118
|
try {
|
|
5850
|
-
const cs = JSON.parse(
|
|
6119
|
+
const cs = JSON.parse(fs8.readFileSync(claudeSettingsPath, "utf-8"));
|
|
5851
6120
|
const hasHook = cs.hooks?.PreToolUse?.some(
|
|
5852
6121
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
5853
6122
|
);
|
|
@@ -5860,10 +6129,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
5860
6129
|
} else {
|
|
5861
6130
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
5862
6131
|
}
|
|
5863
|
-
const geminiSettingsPath =
|
|
5864
|
-
if (
|
|
6132
|
+
const geminiSettingsPath = path10.join(homeDir2, ".gemini", "settings.json");
|
|
6133
|
+
if (fs8.existsSync(geminiSettingsPath)) {
|
|
5865
6134
|
try {
|
|
5866
|
-
const gs = JSON.parse(
|
|
6135
|
+
const gs = JSON.parse(fs8.readFileSync(geminiSettingsPath, "utf-8"));
|
|
5867
6136
|
const hasHook = gs.hooks?.BeforeTool?.some(
|
|
5868
6137
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
5869
6138
|
);
|
|
@@ -5876,10 +6145,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
5876
6145
|
} else {
|
|
5877
6146
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
5878
6147
|
}
|
|
5879
|
-
const cursorHooksPath =
|
|
5880
|
-
if (
|
|
6148
|
+
const cursorHooksPath = path10.join(homeDir2, ".cursor", "hooks.json");
|
|
6149
|
+
if (fs8.existsSync(cursorHooksPath)) {
|
|
5881
6150
|
try {
|
|
5882
|
-
const cur = JSON.parse(
|
|
6151
|
+
const cur = JSON.parse(fs8.readFileSync(cursorHooksPath, "utf-8"));
|
|
5883
6152
|
const hasHook = cur.hooks?.preToolUse?.some(
|
|
5884
6153
|
(h) => h.command?.includes("node9") || h.command?.includes("cli.js")
|
|
5885
6154
|
);
|
|
@@ -5981,8 +6250,8 @@ program.command("explain").description(
|
|
|
5981
6250
|
console.log("");
|
|
5982
6251
|
});
|
|
5983
6252
|
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) => {
|
|
5984
|
-
const configPath =
|
|
5985
|
-
if (
|
|
6253
|
+
const configPath = path10.join(os7.homedir(), ".node9", "config.json");
|
|
6254
|
+
if (fs8.existsSync(configPath) && !options.force) {
|
|
5986
6255
|
console.log(chalk6.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
5987
6256
|
console.log(chalk6.gray(` Run with --force to overwrite.`));
|
|
5988
6257
|
return;
|
|
@@ -5996,9 +6265,9 @@ program.command("init").description("Create ~/.node9/config.json with default po
|
|
|
5996
6265
|
mode: safeMode
|
|
5997
6266
|
}
|
|
5998
6267
|
};
|
|
5999
|
-
const dir =
|
|
6000
|
-
if (!
|
|
6001
|
-
|
|
6268
|
+
const dir = path10.dirname(configPath);
|
|
6269
|
+
if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
|
|
6270
|
+
fs8.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
6002
6271
|
console.log(chalk6.green(`\u2705 Global config created: ${configPath}`));
|
|
6003
6272
|
console.log(chalk6.cyan(` Mode set to: ${safeMode}`));
|
|
6004
6273
|
console.log(
|
|
@@ -6016,14 +6285,14 @@ function formatRelativeTime(timestamp) {
|
|
|
6016
6285
|
return new Date(timestamp).toLocaleDateString();
|
|
6017
6286
|
}
|
|
6018
6287
|
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) => {
|
|
6019
|
-
const logPath =
|
|
6020
|
-
if (!
|
|
6288
|
+
const logPath = path10.join(os7.homedir(), ".node9", "audit.log");
|
|
6289
|
+
if (!fs8.existsSync(logPath)) {
|
|
6021
6290
|
console.log(
|
|
6022
6291
|
chalk6.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
6023
6292
|
);
|
|
6024
6293
|
return;
|
|
6025
6294
|
}
|
|
6026
|
-
const raw =
|
|
6295
|
+
const raw = fs8.readFileSync(logPath, "utf-8");
|
|
6027
6296
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
6028
6297
|
let entries = lines.flatMap((line) => {
|
|
6029
6298
|
try {
|
|
@@ -6106,13 +6375,13 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
6106
6375
|
console.log("");
|
|
6107
6376
|
const modeLabel = settings.mode === "audit" ? chalk6.blue("audit") : settings.mode === "strict" ? chalk6.red("strict") : chalk6.white("standard");
|
|
6108
6377
|
console.log(` Mode: ${modeLabel}`);
|
|
6109
|
-
const projectConfig =
|
|
6110
|
-
const globalConfig =
|
|
6378
|
+
const projectConfig = path10.join(process.cwd(), "node9.config.json");
|
|
6379
|
+
const globalConfig = path10.join(os7.homedir(), ".node9", "config.json");
|
|
6111
6380
|
console.log(
|
|
6112
|
-
` Local: ${
|
|
6381
|
+
` Local: ${fs8.existsSync(projectConfig) ? chalk6.green("Active (node9.config.json)") : chalk6.gray("Not present")}`
|
|
6113
6382
|
);
|
|
6114
6383
|
console.log(
|
|
6115
|
-
` Global: ${
|
|
6384
|
+
` Global: ${fs8.existsSync(globalConfig) ? chalk6.green("Active (~/.node9/config.json)") : chalk6.gray("Not present")}`
|
|
6116
6385
|
);
|
|
6117
6386
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
6118
6387
|
console.log(
|
|
@@ -6156,7 +6425,7 @@ program.command("daemon").description("Run the local approval server").argument(
|
|
|
6156
6425
|
console.log(chalk6.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
|
|
6157
6426
|
process.exit(0);
|
|
6158
6427
|
}
|
|
6159
|
-
const child =
|
|
6428
|
+
const child = spawn5("node9", ["daemon"], { detached: true, stdio: "ignore" });
|
|
6160
6429
|
child.unref();
|
|
6161
6430
|
for (let i = 0; i < 12; i++) {
|
|
6162
6431
|
await new Promise((r) => setTimeout(r, 250));
|
|
@@ -6168,7 +6437,7 @@ program.command("daemon").description("Run the local approval server").argument(
|
|
|
6168
6437
|
process.exit(0);
|
|
6169
6438
|
}
|
|
6170
6439
|
if (options.background) {
|
|
6171
|
-
const child =
|
|
6440
|
+
const child = spawn5("node9", ["daemon"], { detached: true, stdio: "ignore" });
|
|
6172
6441
|
child.unref();
|
|
6173
6442
|
console.log(chalk6.green(`
|
|
6174
6443
|
\u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
|
|
@@ -6196,9 +6465,9 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
|
|
|
6196
6465
|
} catch (err) {
|
|
6197
6466
|
const tempConfig = getConfig();
|
|
6198
6467
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
6199
|
-
const logPath =
|
|
6468
|
+
const logPath = path10.join(os7.homedir(), ".node9", "hook-debug.log");
|
|
6200
6469
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
6201
|
-
|
|
6470
|
+
fs8.appendFileSync(
|
|
6202
6471
|
logPath,
|
|
6203
6472
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
6204
6473
|
RAW: ${raw}
|
|
@@ -6207,19 +6476,12 @@ RAW: ${raw}
|
|
|
6207
6476
|
}
|
|
6208
6477
|
process.exit(0);
|
|
6209
6478
|
}
|
|
6210
|
-
|
|
6211
|
-
try {
|
|
6212
|
-
process.chdir(payload.cwd);
|
|
6213
|
-
_resetConfigCache();
|
|
6214
|
-
} catch {
|
|
6215
|
-
}
|
|
6216
|
-
}
|
|
6217
|
-
const config = getConfig();
|
|
6479
|
+
const config = getConfig(payload.cwd || void 0);
|
|
6218
6480
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
6219
|
-
const logPath =
|
|
6220
|
-
if (!
|
|
6221
|
-
|
|
6222
|
-
|
|
6481
|
+
const logPath = path10.join(os7.homedir(), ".node9", "hook-debug.log");
|
|
6482
|
+
if (!fs8.existsSync(path10.dirname(logPath)))
|
|
6483
|
+
fs8.mkdirSync(path10.dirname(logPath), { recursive: true });
|
|
6484
|
+
fs8.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
6223
6485
|
`);
|
|
6224
6486
|
}
|
|
6225
6487
|
const toolName = sanitize(payload.tool_name ?? payload.name ?? "");
|
|
@@ -6263,7 +6525,7 @@ RAW: ${raw}
|
|
|
6263
6525
|
}
|
|
6264
6526
|
const meta = { agent, mcpServer };
|
|
6265
6527
|
if (shouldSnapshot(toolName, toolInput, config)) {
|
|
6266
|
-
await createShadowSnapshot(toolName, toolInput);
|
|
6528
|
+
await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
|
|
6267
6529
|
}
|
|
6268
6530
|
const result = await authorizeHeadless(toolName, toolInput, false, meta);
|
|
6269
6531
|
if (result.approved) {
|
|
@@ -6296,9 +6558,9 @@ RAW: ${raw}
|
|
|
6296
6558
|
});
|
|
6297
6559
|
} catch (err) {
|
|
6298
6560
|
if (process.env.NODE9_DEBUG === "1") {
|
|
6299
|
-
const logPath =
|
|
6561
|
+
const logPath = path10.join(os7.homedir(), ".node9", "hook-debug.log");
|
|
6300
6562
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
6301
|
-
|
|
6563
|
+
fs8.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
6302
6564
|
`);
|
|
6303
6565
|
}
|
|
6304
6566
|
process.exit(0);
|
|
@@ -6343,15 +6605,25 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
|
|
|
6343
6605
|
decision: "allowed",
|
|
6344
6606
|
source: "post-hook"
|
|
6345
6607
|
};
|
|
6346
|
-
const logPath =
|
|
6347
|
-
if (!
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
const
|
|
6608
|
+
const logPath = path10.join(os7.homedir(), ".node9", "audit.log");
|
|
6609
|
+
if (!fs8.existsSync(path10.dirname(logPath)))
|
|
6610
|
+
fs8.mkdirSync(path10.dirname(logPath), { recursive: true });
|
|
6611
|
+
fs8.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
6612
|
+
const safeCwd = typeof payload.cwd === "string" && path10.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
6613
|
+
const config = getConfig(safeCwd);
|
|
6351
6614
|
if (shouldSnapshot(tool, {}, config)) {
|
|
6352
|
-
await createShadowSnapshot();
|
|
6615
|
+
await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
|
|
6616
|
+
}
|
|
6617
|
+
} catch (err) {
|
|
6618
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6619
|
+
process.stderr.write(`[Node9] audit log error: ${msg}
|
|
6620
|
+
`);
|
|
6621
|
+
const debugPath = path10.join(os7.homedir(), ".node9", "hook-debug.log");
|
|
6622
|
+
try {
|
|
6623
|
+
fs8.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
|
|
6624
|
+
`);
|
|
6625
|
+
} catch {
|
|
6353
6626
|
}
|
|
6354
|
-
} catch {
|
|
6355
6627
|
}
|
|
6356
6628
|
process.exit(0);
|
|
6357
6629
|
};
|
|
@@ -6622,9 +6894,9 @@ process.on("unhandledRejection", (reason) => {
|
|
|
6622
6894
|
const isCheckHook = process.argv[2] === "check";
|
|
6623
6895
|
if (isCheckHook) {
|
|
6624
6896
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
6625
|
-
const logPath =
|
|
6897
|
+
const logPath = path10.join(os7.homedir(), ".node9", "hook-debug.log");
|
|
6626
6898
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
6627
|
-
|
|
6899
|
+
fs8.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
6628
6900
|
`);
|
|
6629
6901
|
}
|
|
6630
6902
|
process.exit(0);
|