@node9/proxy 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -38,13 +38,14 @@ module.exports = __toCommonJS(src_exports);
38
38
  var import_chalk2 = __toESM(require("chalk"));
39
39
  var import_prompts = require("@inquirer/prompts");
40
40
  var import_fs = __toESM(require("fs"));
41
- var import_path = __toESM(require("path"));
41
+ var import_path2 = __toESM(require("path"));
42
42
  var import_os = __toESM(require("os"));
43
43
  var import_picomatch = __toESM(require("picomatch"));
44
44
  var import_sh_syntax = require("sh-syntax");
45
45
 
46
46
  // src/ui/native.ts
47
47
  var import_child_process = require("child_process");
48
+ var import_path = __toESM(require("path"));
48
49
  var import_chalk = __toESM(require("chalk"));
49
50
  var isTestEnv = () => {
50
51
  return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
@@ -54,8 +55,29 @@ function smartTruncate(str, maxLen = 500) {
54
55
  const edge = Math.floor(maxLen / 2) - 3;
55
56
  return `${str.slice(0, edge)} ... ${str.slice(-edge)}`;
56
57
  }
57
- function formatArgs(args) {
58
- if (args === null || args === void 0) return "(none)";
58
+ function extractContext(text, matchedWord) {
59
+ const lines = text.split("\n");
60
+ if (lines.length <= 7 || !matchedWord) return smartTruncate(text, 500);
61
+ const escaped = matchedWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
62
+ const pattern = new RegExp(`\\b${escaped}\\b`, "i");
63
+ const allHits = lines.map((line, i) => ({ i, line })).filter(({ line }) => pattern.test(line));
64
+ if (allHits.length === 0) return smartTruncate(text, 500);
65
+ const nonComment = allHits.find(({ line }) => {
66
+ const trimmed = line.trim();
67
+ return !trimmed.startsWith("//") && !trimmed.startsWith("#");
68
+ });
69
+ const hitIndex = (nonComment ?? allHits[0]).i;
70
+ const start = Math.max(0, hitIndex - 3);
71
+ const end = Math.min(lines.length, hitIndex + 4);
72
+ const snippet = lines.slice(start, end).map((line, i) => `${start + i === hitIndex ? "\u{1F6D1} " : " "}${line}`).join("\n");
73
+ const head = start > 0 ? `... [${start} lines hidden] ...
74
+ ` : "";
75
+ const tail = end < lines.length ? `
76
+ ... [${lines.length - end} lines hidden] ...` : "";
77
+ return `${head}${snippet}${tail}`;
78
+ }
79
+ function formatArgs(args, matchedField, matchedWord) {
80
+ if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
59
81
  let parsed = args;
60
82
  if (typeof args === "string") {
61
83
  const trimmed = args.trim();
@@ -66,11 +88,39 @@ function formatArgs(args) {
66
88
  parsed = args;
67
89
  }
68
90
  } else {
69
- return smartTruncate(args, 600);
91
+ return { message: smartTruncate(args, 600), intent: "EXEC" };
70
92
  }
71
93
  }
72
94
  if (typeof parsed === "object" && !Array.isArray(parsed)) {
73
95
  const obj = parsed;
96
+ if (obj.old_string !== void 0 && obj.new_string !== void 0) {
97
+ const file = obj.file_path ? import_path.default.basename(String(obj.file_path)) : "file";
98
+ const oldPreview = smartTruncate(String(obj.old_string), 120);
99
+ const newPreview = extractContext(String(obj.new_string), matchedWord);
100
+ return {
101
+ intent: "EDIT",
102
+ message: `\u{1F4DD} EDITING: ${file}
103
+ \u{1F4C2} PATH: ${obj.file_path}
104
+
105
+ --- REPLACING ---
106
+ ${oldPreview}
107
+
108
+ +++ NEW CODE +++
109
+ ${newPreview}`
110
+ };
111
+ }
112
+ if (matchedField && obj[matchedField] !== void 0) {
113
+ const otherKeys = Object.keys(obj).filter((k) => k !== matchedField);
114
+ const context = otherKeys.length > 0 ? `\u2699\uFE0F Context: ${otherKeys.map((k) => `${k}=${smartTruncate(typeof obj[k] === "object" ? JSON.stringify(obj[k]) : String(obj[k]), 30)}`).join(", ")}
115
+
116
+ ` : "";
117
+ const content = extractContext(String(obj[matchedField]), matchedWord);
118
+ return {
119
+ intent: "EXEC",
120
+ message: `${context}\u{1F6D1} [${matchedField.toUpperCase()}]:
121
+ ${content}`
122
+ };
123
+ }
74
124
  const codeKeys = [
75
125
  "command",
76
126
  "cmd",
@@ -91,14 +141,18 @@ function formatArgs(args) {
91
141
  if (foundKey) {
92
142
  const val = obj[foundKey];
93
143
  const str = typeof val === "string" ? val : JSON.stringify(val);
94
- return `[${foundKey.toUpperCase()}]:
95
- ${smartTruncate(str, 500)}`;
144
+ return {
145
+ intent: "EXEC",
146
+ message: `[${foundKey.toUpperCase()}]:
147
+ ${smartTruncate(str, 500)}`
148
+ };
96
149
  }
97
- return Object.entries(obj).slice(0, 5).map(
150
+ const msg = Object.entries(obj).slice(0, 5).map(
98
151
  ([k, v]) => ` ${k}: ${smartTruncate(typeof v === "string" ? v : JSON.stringify(v), 300)}`
99
152
  ).join("\n");
153
+ return { intent: "EXEC", message: msg };
100
154
  }
101
- return smartTruncate(JSON.stringify(parsed), 200);
155
+ return { intent: "EXEC", message: smartTruncate(JSON.stringify(parsed), 200) };
102
156
  }
103
157
  function sendDesktopNotification(title, body) {
104
158
  if (isTestEnv()) return;
@@ -151,10 +205,11 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
151
205
  }
152
206
  return lines.join("\n");
153
207
  }
154
- async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
208
+ async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord) {
155
209
  if (isTestEnv()) return "deny";
156
- const formattedArgs = formatArgs(args);
157
- const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Approval`;
210
+ const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
211
+ const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
212
+ const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
158
213
  const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
159
214
  process.stderr.write(import_chalk.default.yellow(`
160
215
  \u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
@@ -232,10 +287,10 @@ end run`;
232
287
  }
233
288
 
234
289
  // src/core.ts
235
- var PAUSED_FILE = import_path.default.join(import_os.default.homedir(), ".node9", "PAUSED");
236
- var TRUST_FILE = import_path.default.join(import_os.default.homedir(), ".node9", "trust.json");
237
- var LOCAL_AUDIT_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "audit.log");
238
- var HOOK_DEBUG_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
290
+ var PAUSED_FILE = import_path2.default.join(import_os.default.homedir(), ".node9", "PAUSED");
291
+ var TRUST_FILE = import_path2.default.join(import_os.default.homedir(), ".node9", "trust.json");
292
+ var LOCAL_AUDIT_LOG = import_path2.default.join(import_os.default.homedir(), ".node9", "audit.log");
293
+ var HOOK_DEBUG_LOG = import_path2.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
239
294
  function checkPause() {
240
295
  try {
241
296
  if (!import_fs.default.existsSync(PAUSED_FILE)) return { paused: false };
@@ -253,7 +308,7 @@ function checkPause() {
253
308
  }
254
309
  }
255
310
  function atomicWriteSync(filePath, data, options) {
256
- const dir = import_path.default.dirname(filePath);
311
+ const dir = import_path2.default.dirname(filePath);
257
312
  if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
258
313
  const tmpPath = `${filePath}.${import_os.default.hostname()}.${process.pid}.tmp`;
259
314
  import_fs.default.writeFileSync(tmpPath, data, options);
@@ -294,7 +349,7 @@ function writeTrustSession(toolName, durationMs) {
294
349
  }
295
350
  function appendToLog(logPath, entry) {
296
351
  try {
297
- const dir = import_path.default.dirname(logPath);
352
+ const dir = import_path2.default.dirname(logPath);
298
353
  if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
299
354
  import_fs.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
300
355
  } catch {
@@ -338,9 +393,9 @@ function matchesPattern(text, patterns) {
338
393
  const withoutDotSlash = text.replace(/^\.\//, "");
339
394
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
340
395
  }
341
- function getNestedValue(obj, path2) {
396
+ function getNestedValue(obj, path3) {
342
397
  if (!obj || typeof obj !== "object") return null;
343
- return path2.split(".").reduce((prev, curr) => prev?.[curr], obj);
398
+ return path3.split(".").reduce((prev, curr) => prev?.[curr], obj);
344
399
  }
345
400
  function evaluateSmartConditions(args, rule) {
346
401
  if (!rule.conditions || rule.conditions.length === 0) return true;
@@ -529,6 +584,18 @@ var DEFAULT_CONFIG = {
529
584
  "terminal.execute": "command",
530
585
  "postgres:query": "sql"
531
586
  },
587
+ snapshot: {
588
+ tools: [
589
+ "str_replace_based_edit_tool",
590
+ "write_file",
591
+ "edit_file",
592
+ "create_file",
593
+ "edit",
594
+ "replace"
595
+ ],
596
+ onlyPaths: [],
597
+ ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
598
+ },
532
599
  rules: [
533
600
  {
534
601
  action: "rm",
@@ -564,7 +631,7 @@ var DEFAULT_CONFIG = {
564
631
  var cachedConfig = null;
565
632
  function getInternalToken() {
566
633
  try {
567
- const pidFile = import_path.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
634
+ const pidFile = import_path2.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
568
635
  if (!import_fs.default.existsSync(pidFile)) return null;
569
636
  const data = JSON.parse(import_fs.default.readFileSync(pidFile, "utf-8"));
570
637
  process.kill(data.pid, 0);
@@ -668,9 +735,29 @@ async function evaluatePolicy(toolName, args, agent) {
668
735
  })
669
736
  );
670
737
  if (isDangerous) {
738
+ let matchedField;
739
+ if (matchedDangerousWord && args && typeof args === "object" && !Array.isArray(args)) {
740
+ const obj = args;
741
+ for (const [key, value] of Object.entries(obj)) {
742
+ if (typeof value === "string") {
743
+ try {
744
+ if (new RegExp(
745
+ `\\b${matchedDangerousWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
746
+ "i"
747
+ ).test(value)) {
748
+ matchedField = key;
749
+ break;
750
+ }
751
+ } catch {
752
+ }
753
+ }
754
+ }
755
+ }
671
756
  return {
672
757
  decision: "review",
673
- blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`
758
+ blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`,
759
+ matchedWord: matchedDangerousWord,
760
+ matchedField
674
761
  };
675
762
  }
676
763
  if (config.settings.mode === "strict") {
@@ -688,7 +775,7 @@ var DAEMON_PORT = 7391;
688
775
  var DAEMON_HOST = "127.0.0.1";
689
776
  function isDaemonRunning() {
690
777
  try {
691
- const pidFile = import_path.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
778
+ const pidFile = import_path2.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
692
779
  if (!import_fs.default.existsSync(pidFile)) return false;
693
780
  const { pid, port } = JSON.parse(import_fs.default.readFileSync(pidFile, "utf-8"));
694
781
  if (port !== DAEMON_PORT) return false;
@@ -700,7 +787,7 @@ function isDaemonRunning() {
700
787
  }
701
788
  function getPersistentDecision(toolName) {
702
789
  try {
703
- const file = import_path.default.join(import_os.default.homedir(), ".node9", "decisions.json");
790
+ const file = import_path2.default.join(import_os.default.homedir(), ".node9", "decisions.json");
704
791
  if (!import_fs.default.existsSync(file)) return null;
705
792
  const decisions = JSON.parse(import_fs.default.readFileSync(file, "utf-8"));
706
793
  const d = decisions[toolName];
@@ -771,7 +858,7 @@ async function resolveViaDaemon(id, decision, internalToken) {
771
858
  signal: AbortSignal.timeout(3e3)
772
859
  });
773
860
  }
774
- async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta) {
861
+ async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
775
862
  if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
776
863
  const pauseState = checkPause();
777
864
  if (pauseState.paused) return { approved: true, checkedBy: "paused" };
@@ -791,6 +878,8 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
791
878
  }
792
879
  const isManual = meta?.agent === "Terminal";
793
880
  let explainableLabel = "Local Config";
881
+ let policyMatchedField;
882
+ let policyMatchedWord;
794
883
  if (config.settings.mode === "audit") {
795
884
  if (!isIgnoredTool(toolName)) {
796
885
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
@@ -826,6 +915,8 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
826
915
  };
827
916
  }
828
917
  explainableLabel = policyResult.blockedByLabel || "Local Config";
918
+ policyMatchedField = policyResult.matchedField;
919
+ policyMatchedWord = policyResult.matchedWord;
829
920
  const persistent = getPersistentDecision(toolName);
830
921
  if (persistent === "allow") {
831
922
  if (creds?.apiKey) auditLocalAllow(toolName, args, "persistent", creds, meta);
@@ -918,7 +1009,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
918
1009
  racePromises.push(
919
1010
  (async () => {
920
1011
  try {
921
- if (isDaemonRunning() && internalToken) {
1012
+ if (isDaemonRunning() && internalToken && !options?.calledFromDaemon) {
922
1013
  viewerId = await notifyDaemonViewer(toolName, args, meta).catch(() => null);
923
1014
  }
924
1015
  const cloudResult = await pollNode9SaaS(cloudRequestId, creds, signal);
@@ -946,7 +1037,9 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
946
1037
  meta?.agent,
947
1038
  explainableLabel,
948
1039
  isRemoteLocked,
949
- signal
1040
+ signal,
1041
+ policyMatchedField,
1042
+ policyMatchedWord
950
1043
  );
951
1044
  if (decision === "always_allow") {
952
1045
  writeTrustSession(toolName, 36e5);
@@ -963,7 +1056,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
963
1056
  })()
964
1057
  );
965
1058
  }
966
- if (approvers.browser && isDaemonRunning()) {
1059
+ if (approvers.browser && isDaemonRunning() && !options?.calledFromDaemon) {
967
1060
  racePromises.push(
968
1061
  (async () => {
969
1062
  try {
@@ -1099,8 +1192,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
1099
1192
  }
1100
1193
  function getConfig() {
1101
1194
  if (cachedConfig) return cachedConfig;
1102
- const globalPath = import_path.default.join(import_os.default.homedir(), ".node9", "config.json");
1103
- const projectPath = import_path.default.join(process.cwd(), "node9.config.json");
1195
+ const globalPath = import_path2.default.join(import_os.default.homedir(), ".node9", "config.json");
1196
+ const projectPath = import_path2.default.join(process.cwd(), "node9.config.json");
1104
1197
  const globalConfig = tryLoadConfig(globalPath);
1105
1198
  const projectConfig = tryLoadConfig(projectPath);
1106
1199
  const mergedSettings = {
@@ -1113,7 +1206,12 @@ function getConfig() {
1113
1206
  ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
1114
1207
  toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
1115
1208
  rules: [...DEFAULT_CONFIG.policy.rules],
1116
- smartRules: [...DEFAULT_CONFIG.policy.smartRules]
1209
+ smartRules: [...DEFAULT_CONFIG.policy.smartRules],
1210
+ snapshot: {
1211
+ tools: [...DEFAULT_CONFIG.policy.snapshot.tools],
1212
+ onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
1213
+ ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
1214
+ }
1117
1215
  };
1118
1216
  const applyLayer = (source) => {
1119
1217
  if (!source) return;
@@ -1125,6 +1223,7 @@ function getConfig() {
1125
1223
  if (s.enableHookLogDebug !== void 0)
1126
1224
  mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
1127
1225
  if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
1226
+ if (s.approvalTimeoutMs !== void 0) mergedSettings.approvalTimeoutMs = s.approvalTimeoutMs;
1128
1227
  if (s.environment !== void 0) mergedSettings.environment = s.environment;
1129
1228
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
1130
1229
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
@@ -1133,6 +1232,12 @@ function getConfig() {
1133
1232
  mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
1134
1233
  if (p.rules) mergedPolicy.rules.push(...p.rules);
1135
1234
  if (p.smartRules) mergedPolicy.smartRules.push(...p.smartRules);
1235
+ if (p.snapshot) {
1236
+ const s2 = p.snapshot;
1237
+ if (s2.tools) mergedPolicy.snapshot.tools.push(...s2.tools);
1238
+ if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
1239
+ if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
1240
+ }
1136
1241
  };
1137
1242
  applyLayer(globalConfig);
1138
1243
  applyLayer(projectConfig);
@@ -1140,6 +1245,9 @@ function getConfig() {
1140
1245
  mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
1141
1246
  mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
1142
1247
  mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
1248
+ mergedPolicy.snapshot.tools = [...new Set(mergedPolicy.snapshot.tools)];
1249
+ mergedPolicy.snapshot.onlyPaths = [...new Set(mergedPolicy.snapshot.onlyPaths)];
1250
+ mergedPolicy.snapshot.ignorePaths = [...new Set(mergedPolicy.snapshot.ignorePaths)];
1143
1251
  cachedConfig = {
1144
1252
  settings: mergedSettings,
1145
1253
  policy: mergedPolicy,
@@ -1168,7 +1276,7 @@ function getCredentials() {
1168
1276
  };
1169
1277
  }
1170
1278
  try {
1171
- const credPath = import_path.default.join(import_os.default.homedir(), ".node9", "credentials.json");
1279
+ const credPath = import_path2.default.join(import_os.default.homedir(), ".node9", "credentials.json");
1172
1280
  if (import_fs.default.existsSync(credPath)) {
1173
1281
  const creds = JSON.parse(import_fs.default.readFileSync(credPath, "utf-8"));
1174
1282
  const profileName = process.env.NODE9_PROFILE || "default";