@node9/proxy 1.36.0 → 1.37.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/dist/cli.js +1662 -530
- package/dist/cli.mjs +1662 -530
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -206,8 +206,8 @@ function sanitizeConfig(raw) {
|
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
208
|
const lines = result.error.issues.map((issue) => {
|
|
209
|
-
const
|
|
210
|
-
return ` \u2022 ${
|
|
209
|
+
const path58 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
210
|
+
return ` \u2022 ${path58}: ${issue.message}`;
|
|
211
211
|
});
|
|
212
212
|
return {
|
|
213
213
|
sanitized,
|
|
@@ -1256,9 +1256,9 @@ function matchesPattern(text, patterns) {
|
|
|
1256
1256
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1257
1257
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1258
1258
|
}
|
|
1259
|
-
function getNestedValue(obj,
|
|
1259
|
+
function getNestedValue(obj, path58) {
|
|
1260
1260
|
if (!obj || typeof obj !== "object") return null;
|
|
1261
|
-
const segments =
|
|
1261
|
+
const segments = path58.split(".");
|
|
1262
1262
|
for (const seg of segments) {
|
|
1263
1263
|
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
1264
1264
|
}
|
|
@@ -1771,8 +1771,8 @@ function narrativeRuleLabel(name) {
|
|
|
1771
1771
|
"eval-dynamic": "dynamic eval",
|
|
1772
1772
|
"config-set": "Redis CONFIG SET"
|
|
1773
1773
|
};
|
|
1774
|
-
for (const [key,
|
|
1775
|
-
if (stripped.includes(key)) return
|
|
1774
|
+
for (const [key, label2] of Object.entries(map)) {
|
|
1775
|
+
if (stripped.includes(key)) return label2;
|
|
1776
1776
|
}
|
|
1777
1777
|
return stripped;
|
|
1778
1778
|
}
|
|
@@ -1784,6 +1784,17 @@ function stripRulePrefixes(name) {
|
|
|
1784
1784
|
n = n.replace(/^(block|review|allow)-/, "");
|
|
1785
1785
|
return n;
|
|
1786
1786
|
}
|
|
1787
|
+
function computeSecurityScore(opts) {
|
|
1788
|
+
const { critical, high, medium, total } = opts;
|
|
1789
|
+
if (total === 0) return { score: 100, tier: "good" };
|
|
1790
|
+
const criticalRate = critical / total;
|
|
1791
|
+
const highRate = high / total;
|
|
1792
|
+
const mediumRate = medium / total;
|
|
1793
|
+
const deduction = Math.min(criticalRate * 3e3, 60) + Math.min(highRate * 500, 30) + Math.min(mediumRate * 100, 15);
|
|
1794
|
+
const score = Math.max(0, Math.min(100, Math.round(100 - deduction)));
|
|
1795
|
+
const tier = score >= 80 ? "good" : score >= 50 ? "at-risk" : "critical";
|
|
1796
|
+
return { score, tier };
|
|
1797
|
+
}
|
|
1787
1798
|
function truncateBlastPath(full) {
|
|
1788
1799
|
if (!full) return "";
|
|
1789
1800
|
const cleaned = full.replace(/[/\\]+$/, "");
|
|
@@ -4941,12 +4952,12 @@ async function explainPolicy(toolName, args) {
|
|
|
4941
4952
|
(rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
|
|
4942
4953
|
);
|
|
4943
4954
|
if (matchedRule) {
|
|
4944
|
-
const
|
|
4955
|
+
const label2 = `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`;
|
|
4945
4956
|
if (matchedRule.verdict === "allow") {
|
|
4946
4957
|
steps.push({
|
|
4947
4958
|
name: "Smart rules",
|
|
4948
4959
|
outcome: "allow",
|
|
4949
|
-
detail: `${
|
|
4960
|
+
detail: `${label2} \u2192 allow`,
|
|
4950
4961
|
isFinal: true
|
|
4951
4962
|
});
|
|
4952
4963
|
return { tool: toolName, args, waterfall, steps, decision: "allow" };
|
|
@@ -4954,7 +4965,7 @@ async function explainPolicy(toolName, args) {
|
|
|
4954
4965
|
steps.push({
|
|
4955
4966
|
name: "Smart rules",
|
|
4956
4967
|
outcome: matchedRule.verdict,
|
|
4957
|
-
detail: `${
|
|
4968
|
+
detail: `${label2} \u2192 ${matchedRule.verdict}${matchedRule.reason ? `: ${matchedRule.reason}` : ""}`,
|
|
4958
4969
|
isFinal: true
|
|
4959
4970
|
});
|
|
4960
4971
|
return {
|
|
@@ -4963,7 +4974,7 @@ async function explainPolicy(toolName, args) {
|
|
|
4963
4974
|
waterfall,
|
|
4964
4975
|
steps,
|
|
4965
4976
|
decision: matchedRule.verdict,
|
|
4966
|
-
blockedByLabel:
|
|
4977
|
+
blockedByLabel: label2
|
|
4967
4978
|
};
|
|
4968
4979
|
}
|
|
4969
4980
|
steps.push({
|
|
@@ -5015,7 +5026,7 @@ async function explainPolicy(toolName, args) {
|
|
|
5015
5026
|
});
|
|
5016
5027
|
const evalVerdict = detectDangerousShellExec(shellCommand);
|
|
5017
5028
|
if (evalVerdict) {
|
|
5018
|
-
const
|
|
5029
|
+
const label2 = evalVerdict === "block" ? "Node9: Eval Remote Execution" : "Node9: Eval Dynamic Content";
|
|
5019
5030
|
const detail = evalVerdict === "block" ? "eval of remote download (curl/wget) \u2014 near-certain supply-chain attack" : "eval of dynamic content (variable or subshell expansion) \u2014 requires approval";
|
|
5020
5031
|
steps.push({ name: "AST eval detection", outcome: evalVerdict, detail, isFinal: true });
|
|
5021
5032
|
return {
|
|
@@ -5024,7 +5035,7 @@ async function explainPolicy(toolName, args) {
|
|
|
5024
5035
|
waterfall,
|
|
5025
5036
|
steps,
|
|
5026
5037
|
decision: evalVerdict,
|
|
5027
|
-
blockedByLabel:
|
|
5038
|
+
blockedByLabel: label2
|
|
5028
5039
|
};
|
|
5029
5040
|
}
|
|
5030
5041
|
steps.push({
|
|
@@ -6961,10 +6972,10 @@ function checkPin(serverKey, currentHash, cwd) {
|
|
|
6961
6972
|
if (!homeEntry) return "new";
|
|
6962
6973
|
return homeEntry.toolsHash === currentHash ? "match" : "mismatch";
|
|
6963
6974
|
}
|
|
6964
|
-
function updatePin(serverKey,
|
|
6975
|
+
function updatePin(serverKey, label2, toolsHash, toolNames) {
|
|
6965
6976
|
const pins = readMcpPins();
|
|
6966
6977
|
pins.servers[serverKey] = {
|
|
6967
|
-
label,
|
|
6978
|
+
label: label2,
|
|
6968
6979
|
toolsHash,
|
|
6969
6980
|
toolNames,
|
|
6970
6981
|
toolCount: toolNames.length,
|
|
@@ -8293,9 +8304,9 @@ function writeToml(filePath, data) {
|
|
|
8293
8304
|
async function setupCodex() {
|
|
8294
8305
|
seedMcpPinsIfMissing();
|
|
8295
8306
|
const homeDir2 = import_os12.default.homedir();
|
|
8296
|
-
const
|
|
8307
|
+
const configPath2 = import_path15.default.join(homeDir2, ".codex", "config.toml");
|
|
8297
8308
|
const hooksPath = import_path15.default.join(homeDir2, ".codex", "hooks.json");
|
|
8298
|
-
const config = readToml(
|
|
8309
|
+
const config = readToml(configPath2) ?? {};
|
|
8299
8310
|
const servers = config.mcp_servers ?? {};
|
|
8300
8311
|
let anythingChanged = false;
|
|
8301
8312
|
const hooksFile = readJson(hooksPath) ?? {};
|
|
@@ -8370,7 +8381,7 @@ async function setupCodex() {
|
|
|
8370
8381
|
if (!hasNode9McpServer(servers)) {
|
|
8371
8382
|
servers["node9"] = NODE9_MCP_SERVER_ENTRY;
|
|
8372
8383
|
config.mcp_servers = servers;
|
|
8373
|
-
writeToml(
|
|
8384
|
+
writeToml(configPath2, config);
|
|
8374
8385
|
console.log(import_chalk.default.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
|
|
8375
8386
|
anythingChanged = true;
|
|
8376
8387
|
}
|
|
@@ -8382,7 +8393,7 @@ async function setupCodex() {
|
|
|
8382
8393
|
}
|
|
8383
8394
|
if (serversToWrap.length > 0) {
|
|
8384
8395
|
console.log(import_chalk.default.bold("The following existing entries will be modified:\n"));
|
|
8385
|
-
console.log(import_chalk.default.white(` ${
|
|
8396
|
+
console.log(import_chalk.default.white(` ${configPath2}`));
|
|
8386
8397
|
for (const { name, upstream } of serversToWrap) {
|
|
8387
8398
|
console.log(import_chalk.default.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
|
|
8388
8399
|
}
|
|
@@ -8397,7 +8408,7 @@ async function setupCodex() {
|
|
|
8397
8408
|
};
|
|
8398
8409
|
}
|
|
8399
8410
|
config.mcp_servers = servers;
|
|
8400
|
-
writeToml(
|
|
8411
|
+
writeToml(configPath2, config);
|
|
8401
8412
|
console.log(import_chalk.default.green(`
|
|
8402
8413
|
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
8403
8414
|
anythingChanged = true;
|
|
@@ -8445,7 +8456,7 @@ async function setupCodex() {
|
|
|
8445
8456
|
}
|
|
8446
8457
|
function teardownCodex() {
|
|
8447
8458
|
const homeDir2 = import_os12.default.homedir();
|
|
8448
|
-
const
|
|
8459
|
+
const configPath2 = import_path15.default.join(homeDir2, ".codex", "config.toml");
|
|
8449
8460
|
const hooksPath = import_path15.default.join(homeDir2, ".codex", "hooks.json");
|
|
8450
8461
|
const hooksFile = readJson(hooksPath);
|
|
8451
8462
|
if (hooksFile?.hooks) {
|
|
@@ -8463,7 +8474,7 @@ function teardownCodex() {
|
|
|
8463
8474
|
console.log(import_chalk.default.green(" \u2705 Removed Node9 hooks from ~/.codex/hooks.json"));
|
|
8464
8475
|
}
|
|
8465
8476
|
}
|
|
8466
|
-
const config = readToml(
|
|
8477
|
+
const config = readToml(configPath2);
|
|
8467
8478
|
if (!config?.mcp_servers) {
|
|
8468
8479
|
console.log(import_chalk.default.blue(" \u2139\uFE0F ~/.codex/config.toml not found \u2014 nothing to remove"));
|
|
8469
8480
|
return;
|
|
@@ -8486,7 +8497,7 @@ function teardownCodex() {
|
|
|
8486
8497
|
}
|
|
8487
8498
|
}
|
|
8488
8499
|
if (changed) {
|
|
8489
|
-
writeToml(
|
|
8500
|
+
writeToml(configPath2, config);
|
|
8490
8501
|
console.log(import_chalk.default.green(" \u2705 Unwrapped MCP servers in ~/.codex/config.toml"));
|
|
8491
8502
|
} else {
|
|
8492
8503
|
console.log(import_chalk.default.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in ~/.codex/config.toml"));
|
|
@@ -8741,18 +8752,18 @@ function teardownVSCode() {
|
|
|
8741
8752
|
}
|
|
8742
8753
|
async function setupClaudeDesktop() {
|
|
8743
8754
|
seedMcpPinsIfMissing();
|
|
8744
|
-
const
|
|
8745
|
-
if (!
|
|
8755
|
+
const configPath2 = claudeDesktopConfigPath();
|
|
8756
|
+
if (!configPath2) {
|
|
8746
8757
|
console.log(import_chalk.default.yellow(" \u26A0\uFE0F Claude Desktop is not supported on this platform."));
|
|
8747
8758
|
return;
|
|
8748
8759
|
}
|
|
8749
|
-
const config = readJson(
|
|
8760
|
+
const config = readJson(configPath2) ?? {};
|
|
8750
8761
|
const servers = config.mcpServers ?? {};
|
|
8751
8762
|
let anythingChanged = false;
|
|
8752
8763
|
if (!hasNode9McpServer(servers)) {
|
|
8753
8764
|
servers["node9"] = NODE9_MCP_SERVER_ENTRY;
|
|
8754
8765
|
config.mcpServers = servers;
|
|
8755
|
-
writeJson(
|
|
8766
|
+
writeJson(configPath2, config);
|
|
8756
8767
|
console.log(import_chalk.default.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
|
|
8757
8768
|
anythingChanged = true;
|
|
8758
8769
|
}
|
|
@@ -8763,7 +8774,7 @@ async function setupClaudeDesktop() {
|
|
|
8763
8774
|
}
|
|
8764
8775
|
if (serversToWrap.length > 0) {
|
|
8765
8776
|
console.log(import_chalk.default.bold("The following existing entries will be modified:\n"));
|
|
8766
|
-
console.log(import_chalk.default.white(` ${
|
|
8777
|
+
console.log(import_chalk.default.white(` ${configPath2}`));
|
|
8767
8778
|
for (const { name, upstream } of serversToWrap) {
|
|
8768
8779
|
console.log(import_chalk.default.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
|
|
8769
8780
|
}
|
|
@@ -8778,7 +8789,7 @@ async function setupClaudeDesktop() {
|
|
|
8778
8789
|
};
|
|
8779
8790
|
}
|
|
8780
8791
|
config.mcpServers = servers;
|
|
8781
|
-
writeJson(
|
|
8792
|
+
writeJson(configPath2, config);
|
|
8782
8793
|
console.log(import_chalk.default.green(`
|
|
8783
8794
|
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
8784
8795
|
anythingChanged = true;
|
|
@@ -8805,12 +8816,12 @@ async function setupClaudeDesktop() {
|
|
|
8805
8816
|
}
|
|
8806
8817
|
}
|
|
8807
8818
|
function teardownClaudeDesktop() {
|
|
8808
|
-
const
|
|
8809
|
-
if (!
|
|
8819
|
+
const configPath2 = claudeDesktopConfigPath();
|
|
8820
|
+
if (!configPath2) {
|
|
8810
8821
|
console.log(import_chalk.default.yellow(" \u26A0\uFE0F Claude Desktop is not supported on this platform."));
|
|
8811
8822
|
return;
|
|
8812
8823
|
}
|
|
8813
|
-
const config = readJson(
|
|
8824
|
+
const config = readJson(configPath2);
|
|
8814
8825
|
if (!config?.mcpServers) {
|
|
8815
8826
|
console.log(import_chalk.default.blue(" \u2139\uFE0F Claude Desktop config not found \u2014 nothing to remove"));
|
|
8816
8827
|
return;
|
|
@@ -8818,7 +8829,7 @@ function teardownClaudeDesktop() {
|
|
|
8818
8829
|
let changed = false;
|
|
8819
8830
|
if (removeNode9McpServer(config.mcpServers)) {
|
|
8820
8831
|
changed = true;
|
|
8821
|
-
console.log(import_chalk.default.green(` \u2705 Removed node9 MCP server entry from ${
|
|
8832
|
+
console.log(import_chalk.default.green(` \u2705 Removed node9 MCP server entry from ${configPath2}`));
|
|
8822
8833
|
}
|
|
8823
8834
|
for (const [name, server] of Object.entries(config.mcpServers)) {
|
|
8824
8835
|
const args = server.args;
|
|
@@ -8833,7 +8844,7 @@ function teardownClaudeDesktop() {
|
|
|
8833
8844
|
}
|
|
8834
8845
|
}
|
|
8835
8846
|
if (changed) {
|
|
8836
|
-
writeJson(
|
|
8847
|
+
writeJson(configPath2, config);
|
|
8837
8848
|
console.log(import_chalk.default.green(" \u2705 Unwrapped MCP servers in Claude Desktop config"));
|
|
8838
8849
|
} else {
|
|
8839
8850
|
console.log(import_chalk.default.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in Claude Desktop config"));
|
|
@@ -8861,7 +8872,7 @@ async function setupOpencode() {
|
|
|
8861
8872
|
const homeDir2 = import_os12.default.homedir();
|
|
8862
8873
|
const configDir = import_path15.default.join(homeDir2, ".config", "opencode");
|
|
8863
8874
|
const pluginsDir = import_path15.default.join(configDir, "plugins");
|
|
8864
|
-
const
|
|
8875
|
+
const configPath2 = import_path15.default.join(configDir, "opencode.json");
|
|
8865
8876
|
const pluginPath = import_path15.default.join(pluginsDir, OPENCODE_PLUGIN_NAME);
|
|
8866
8877
|
try {
|
|
8867
8878
|
import_fs13.default.mkdirSync(pluginsDir, { recursive: true });
|
|
@@ -8895,7 +8906,7 @@ async function setupOpencode() {
|
|
|
8895
8906
|
);
|
|
8896
8907
|
}
|
|
8897
8908
|
}
|
|
8898
|
-
const config = readJson(
|
|
8909
|
+
const config = readJson(configPath2) ?? {};
|
|
8899
8910
|
const mcp = config.mcp ?? {};
|
|
8900
8911
|
let configChanged = false;
|
|
8901
8912
|
const desiredCommand = [...node9ArgvForShim(), "mcp-server"];
|
|
@@ -8916,7 +8927,7 @@ async function setupOpencode() {
|
|
|
8916
8927
|
console.log(import_chalk.default.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
|
|
8917
8928
|
}
|
|
8918
8929
|
}
|
|
8919
|
-
if (configChanged) writeJson(
|
|
8930
|
+
if (configChanged) writeJson(configPath2, config);
|
|
8920
8931
|
if (pluginChanged || configChanged) {
|
|
8921
8932
|
console.log(import_chalk.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Opencode!"));
|
|
8922
8933
|
console.log(import_chalk.default.gray(" Restart Opencode for changes to take effect."));
|
|
@@ -8929,7 +8940,7 @@ function teardownOpencode() {
|
|
|
8929
8940
|
const homeDir2 = import_os12.default.homedir();
|
|
8930
8941
|
const configDir = import_path15.default.join(homeDir2, ".config", "opencode");
|
|
8931
8942
|
const pluginsDir = import_path15.default.join(configDir, "plugins");
|
|
8932
|
-
const
|
|
8943
|
+
const configPath2 = import_path15.default.join(configDir, "opencode.json");
|
|
8933
8944
|
const pluginPath = import_path15.default.join(pluginsDir, OPENCODE_PLUGIN_NAME);
|
|
8934
8945
|
try {
|
|
8935
8946
|
if (import_fs13.default.existsSync(pluginPath)) {
|
|
@@ -8939,7 +8950,7 @@ function teardownOpencode() {
|
|
|
8939
8950
|
} catch (err2) {
|
|
8940
8951
|
console.log(import_chalk.default.yellow(` \u26A0\uFE0F Could not remove ${pluginPath}: ${String(err2)}`));
|
|
8941
8952
|
}
|
|
8942
|
-
const config = readJson(
|
|
8953
|
+
const config = readJson(configPath2);
|
|
8943
8954
|
if (!config) {
|
|
8944
8955
|
console.log(import_chalk.default.blue(" \u2139\uFE0F ~/.config/opencode/opencode.json not found \u2014 nothing to remove"));
|
|
8945
8956
|
return;
|
|
@@ -8955,7 +8966,7 @@ function teardownOpencode() {
|
|
|
8955
8966
|
}
|
|
8956
8967
|
if (changed) {
|
|
8957
8968
|
config.mcp = mcp;
|
|
8958
|
-
writeJson(
|
|
8969
|
+
writeJson(configPath2, config);
|
|
8959
8970
|
} else {
|
|
8960
8971
|
console.log(import_chalk.default.blue(" \u2139\uFE0F No node9 entries found in ~/.config/opencode/opencode.json"));
|
|
8961
8972
|
}
|
|
@@ -9026,15 +9037,15 @@ function hermesAllowlistPath(homeDir2 = import_os12.default.homedir()) {
|
|
|
9026
9037
|
}
|
|
9027
9038
|
function setupHermes() {
|
|
9028
9039
|
const homeDir2 = import_os12.default.homedir();
|
|
9029
|
-
const
|
|
9040
|
+
const configPath2 = hermesConfigPath(homeDir2);
|
|
9030
9041
|
const allowlistPath = hermesAllowlistPath(homeDir2);
|
|
9031
|
-
if (!import_fs13.default.existsSync(
|
|
9032
|
-
console.log(import_chalk.default.yellow(` \u26A0\uFE0F Hermes config not found at ${
|
|
9033
|
-
console.log(import_chalk.default.gray(" Run `hermes setup` first, then re-run node9
|
|
9042
|
+
if (!import_fs13.default.existsSync(configPath2)) {
|
|
9043
|
+
console.log(import_chalk.default.yellow(` \u26A0\uFE0F Hermes config not found at ${configPath2}`));
|
|
9044
|
+
console.log(import_chalk.default.gray(" Run `hermes setup` first, then re-run node9 agents add hermes."));
|
|
9034
9045
|
return;
|
|
9035
9046
|
}
|
|
9036
9047
|
let anythingChanged = false;
|
|
9037
|
-
const raw = import_fs13.default.readFileSync(
|
|
9048
|
+
const raw = import_fs13.default.readFileSync(configPath2, "utf-8");
|
|
9038
9049
|
const doc = yaml.parseDocument(raw);
|
|
9039
9050
|
if (doc.errors.length > 0) {
|
|
9040
9051
|
console.log(import_chalk.default.yellow(` \u26A0\uFE0F Hermes config.yaml has YAML parse errors:`));
|
|
@@ -9042,7 +9053,9 @@ function setupHermes() {
|
|
|
9042
9053
|
console.log(import_chalk.default.gray(` \u2022 ${err2.message}`));
|
|
9043
9054
|
}
|
|
9044
9055
|
console.log(
|
|
9045
|
-
import_chalk.default.gray(
|
|
9056
|
+
import_chalk.default.gray(
|
|
9057
|
+
" Fix the file (or run `hermes config edit`), then re-run node9 agents add hermes."
|
|
9058
|
+
)
|
|
9046
9059
|
);
|
|
9047
9060
|
return;
|
|
9048
9061
|
}
|
|
@@ -9072,7 +9085,7 @@ function setupHermes() {
|
|
|
9072
9085
|
anythingChanged = true;
|
|
9073
9086
|
}
|
|
9074
9087
|
if (anythingChanged) {
|
|
9075
|
-
import_fs13.default.writeFileSync(
|
|
9088
|
+
import_fs13.default.writeFileSync(configPath2, doc.toString());
|
|
9076
9089
|
}
|
|
9077
9090
|
let allowlist = {};
|
|
9078
9091
|
if (import_fs13.default.existsSync(allowlistPath)) {
|
|
@@ -9115,24 +9128,24 @@ function setupHermes() {
|
|
|
9115
9128
|
}
|
|
9116
9129
|
function teardownHermes() {
|
|
9117
9130
|
const homeDir2 = import_os12.default.homedir();
|
|
9118
|
-
const
|
|
9131
|
+
const configPath2 = hermesConfigPath(homeDir2);
|
|
9119
9132
|
const allowlistPath = hermesAllowlistPath(homeDir2);
|
|
9120
|
-
if (!import_fs13.default.existsSync(
|
|
9121
|
-
console.log(import_chalk.default.blue(` \u2139\uFE0F ${
|
|
9133
|
+
if (!import_fs13.default.existsSync(configPath2)) {
|
|
9134
|
+
console.log(import_chalk.default.blue(` \u2139\uFE0F ${configPath2} not found \u2014 nothing to remove`));
|
|
9122
9135
|
return;
|
|
9123
9136
|
}
|
|
9124
|
-
const raw = import_fs13.default.readFileSync(
|
|
9137
|
+
const raw = import_fs13.default.readFileSync(configPath2, "utf-8");
|
|
9125
9138
|
const doc = yaml.parseDocument(raw);
|
|
9126
9139
|
if (doc.errors.length > 0) {
|
|
9127
9140
|
console.log(
|
|
9128
|
-
import_chalk.default.yellow(` \u26A0\uFE0F Skipping ${
|
|
9141
|
+
import_chalk.default.yellow(` \u26A0\uFE0F Skipping ${configPath2} \u2014 file has YAML parse errors, fix it manually.`)
|
|
9129
9142
|
);
|
|
9130
9143
|
} else {
|
|
9131
|
-
teardownHermesConfigDoc(doc,
|
|
9144
|
+
teardownHermesConfigDoc(doc, configPath2);
|
|
9132
9145
|
}
|
|
9133
9146
|
teardownHermesAllowlist(allowlistPath);
|
|
9134
9147
|
}
|
|
9135
|
-
function teardownHermesConfigDoc(doc,
|
|
9148
|
+
function teardownHermesConfigDoc(doc, configPath2) {
|
|
9136
9149
|
let anythingChanged = false;
|
|
9137
9150
|
const current = doc.toJS() ?? {};
|
|
9138
9151
|
for (const { event } of HERMES_HOOK_PLAN) {
|
|
@@ -9154,10 +9167,10 @@ function teardownHermesConfigDoc(doc, configPath) {
|
|
|
9154
9167
|
anythingChanged = true;
|
|
9155
9168
|
}
|
|
9156
9169
|
if (anythingChanged) {
|
|
9157
|
-
import_fs13.default.writeFileSync(
|
|
9158
|
-
console.log(import_chalk.default.green(` \u2705 Removed Node9 hooks from ${
|
|
9170
|
+
import_fs13.default.writeFileSync(configPath2, doc.toString());
|
|
9171
|
+
console.log(import_chalk.default.green(` \u2705 Removed Node9 hooks from ${configPath2}`));
|
|
9159
9172
|
} else {
|
|
9160
|
-
console.log(import_chalk.default.blue(` \u2139\uFE0F No Node9 hooks found in ${
|
|
9173
|
+
console.log(import_chalk.default.blue(` \u2139\uFE0F No Node9 hooks found in ${configPath2}`));
|
|
9161
9174
|
}
|
|
9162
9175
|
}
|
|
9163
9176
|
function teardownHermesAllowlist(allowlistPath) {
|
|
@@ -9999,12 +10012,12 @@ function buildScanSummary(agents) {
|
|
|
9999
10012
|
}
|
|
10000
10013
|
function buildSections(findings) {
|
|
10001
10014
|
const sectionMap = /* @__PURE__ */ new Map();
|
|
10002
|
-
function ensureSection(id,
|
|
10015
|
+
function ensureSection(id, label2, subtitle, sourceType, shieldKey) {
|
|
10003
10016
|
let s = sectionMap.get(id);
|
|
10004
10017
|
if (!s) {
|
|
10005
10018
|
s = {
|
|
10006
10019
|
id,
|
|
10007
|
-
label,
|
|
10020
|
+
label: label2,
|
|
10008
10021
|
subtitle,
|
|
10009
10022
|
sourceType,
|
|
10010
10023
|
shieldKey,
|
|
@@ -13097,9 +13110,9 @@ function printRuleGroup(rule, topN, drillDown, previewWidth) {
|
|
|
13097
13110
|
}
|
|
13098
13111
|
}
|
|
13099
13112
|
function compactRuleLabel(name) {
|
|
13100
|
-
let
|
|
13101
|
-
|
|
13102
|
-
return
|
|
13113
|
+
let label2 = name.replace(/^shield:[^:]+:/, "");
|
|
13114
|
+
label2 = label2.replace(/^(block|review|allow)-/, "");
|
|
13115
|
+
return label2.replace(/-+/g, "-");
|
|
13103
13116
|
}
|
|
13104
13117
|
function renderCompactScorecard(input) {
|
|
13105
13118
|
const { scan, summary, blast, blastExposures, blockedCount, reviewCount } = input;
|
|
@@ -13200,9 +13213,9 @@ function renderNarrativeScorecard(input) {
|
|
|
13200
13213
|
for (const section of summary.sections) {
|
|
13201
13214
|
for (const rule of section.rules) {
|
|
13202
13215
|
const sev = classifyRuleSeverity2(rule.name, rule.verdict);
|
|
13203
|
-
const
|
|
13216
|
+
const label2 = narrativeRuleLabel2(rule.name);
|
|
13204
13217
|
const count = rule.findings.length;
|
|
13205
|
-
const display = count > 1 ? `${
|
|
13218
|
+
const display = count > 1 ? `${label2} \xD7${count}` : label2;
|
|
13206
13219
|
const entry = { label: display, count };
|
|
13207
13220
|
if (sev === "critical") critical.push(entry);
|
|
13208
13221
|
else if (sev === "high") high.push(entry);
|
|
@@ -14031,7 +14044,7 @@ function registerScanCommand(program2) {
|
|
|
14031
14044
|
console.log(import_chalk5.default.bold(" Enable real-time protection:"));
|
|
14032
14045
|
console.log("");
|
|
14033
14046
|
console.log(
|
|
14034
|
-
" " + import_chalk5.default.cyan("npm install -g
|
|
14047
|
+
" " + import_chalk5.default.cyan("npm install -g node9-ai") + import_chalk5.default.dim(" && ") + import_chalk5.default.cyan("node9 init --recommended")
|
|
14035
14048
|
);
|
|
14036
14049
|
console.log("");
|
|
14037
14050
|
console.log(
|
|
@@ -14337,8 +14350,8 @@ var init_session_counters = __esm({
|
|
|
14337
14350
|
if (!isFinite(amount) || amount < 0) return;
|
|
14338
14351
|
this._estimatedCost += amount;
|
|
14339
14352
|
}
|
|
14340
|
-
recordRuleHit(
|
|
14341
|
-
this._lastRuleHit =
|
|
14353
|
+
recordRuleHit(label2) {
|
|
14354
|
+
this._lastRuleHit = label2;
|
|
14342
14355
|
}
|
|
14343
14356
|
recordBlockedTool(toolName) {
|
|
14344
14357
|
this._lastBlockedTool = toolName;
|
|
@@ -14612,10 +14625,10 @@ function broadcast(event, data) {
|
|
|
14612
14625
|
activityRing.push({ event, data });
|
|
14613
14626
|
if (activityRing.length > ACTIVITY_RING_SIZE) activityRing.shift();
|
|
14614
14627
|
} else if (event === "activity-result") {
|
|
14615
|
-
const { id, status, label, costEstimate } = data;
|
|
14628
|
+
const { id, status, label: label2, costEstimate } = data;
|
|
14616
14629
|
for (let i = activityRing.length - 1; i >= 0; i--) {
|
|
14617
14630
|
if (activityRing[i].data.id === id) {
|
|
14618
|
-
Object.assign(activityRing[i].data, { status, label, costEstimate });
|
|
14631
|
+
Object.assign(activityRing[i].data, { status, label: label2, costEstimate });
|
|
14619
14632
|
break;
|
|
14620
14633
|
}
|
|
14621
14634
|
}
|
|
@@ -16951,20 +16964,20 @@ function getModelContextLimit(model) {
|
|
|
16951
16964
|
return 2e5;
|
|
16952
16965
|
}
|
|
16953
16966
|
function readSessionUsage() {
|
|
16954
|
-
const projectsDir =
|
|
16955
|
-
if (!
|
|
16967
|
+
const projectsDir = import_path55.default.join(import_os51.default.homedir(), ".claude", "projects");
|
|
16968
|
+
if (!import_fs56.default.existsSync(projectsDir)) return null;
|
|
16956
16969
|
let latestFile = null;
|
|
16957
16970
|
let latestMtime = 0;
|
|
16958
16971
|
try {
|
|
16959
|
-
for (const dir of
|
|
16960
|
-
const dirPath =
|
|
16972
|
+
for (const dir of import_fs56.default.readdirSync(projectsDir)) {
|
|
16973
|
+
const dirPath = import_path55.default.join(projectsDir, dir);
|
|
16961
16974
|
try {
|
|
16962
|
-
if (!
|
|
16963
|
-
for (const file of
|
|
16975
|
+
if (!import_fs56.default.statSync(dirPath).isDirectory()) continue;
|
|
16976
|
+
for (const file of import_fs56.default.readdirSync(dirPath)) {
|
|
16964
16977
|
if (!file.endsWith(".jsonl") || file.startsWith("agent-")) continue;
|
|
16965
|
-
const filePath =
|
|
16978
|
+
const filePath = import_path55.default.join(dirPath, file);
|
|
16966
16979
|
try {
|
|
16967
|
-
const mtime =
|
|
16980
|
+
const mtime = import_fs56.default.statSync(filePath).mtimeMs;
|
|
16968
16981
|
if (mtime > latestMtime) {
|
|
16969
16982
|
latestMtime = mtime;
|
|
16970
16983
|
latestFile = filePath;
|
|
@@ -16979,7 +16992,7 @@ function readSessionUsage() {
|
|
|
16979
16992
|
}
|
|
16980
16993
|
if (!latestFile) return null;
|
|
16981
16994
|
try {
|
|
16982
|
-
const lines =
|
|
16995
|
+
const lines = import_fs56.default.readFileSync(latestFile, "utf-8").split("\n");
|
|
16983
16996
|
let lastModel = "";
|
|
16984
16997
|
let lastInput = 0;
|
|
16985
16998
|
let lastOutput = 0;
|
|
@@ -17004,10 +17017,10 @@ function readSessionUsage() {
|
|
|
17004
17017
|
}
|
|
17005
17018
|
}
|
|
17006
17019
|
function formatContextStat(stat) {
|
|
17007
|
-
const pctColor = stat.fillPct >= 80 ?
|
|
17020
|
+
const pctColor = stat.fillPct >= 80 ? import_chalk32.default.red : stat.fillPct >= 50 ? import_chalk32.default.yellow : import_chalk32.default.cyan;
|
|
17008
17021
|
const k = (n) => `${Math.round(n / 1e3)}k`;
|
|
17009
17022
|
const modelShort = stat.model.replace(/@.*$/, "").replace(/-\d{8}$/, "").replace(/^claude-/, "");
|
|
17010
|
-
return
|
|
17023
|
+
return import_chalk32.default.dim("ctx: ") + pctColor(`${stat.fillPct}%`) + import_chalk32.default.dim(
|
|
17011
17024
|
` (${k(stat.inputTokens)}/${k(getModelContextLimit(stat.model))} out ${k(stat.outputTokens)} \xB7 ${modelShort})`
|
|
17012
17025
|
);
|
|
17013
17026
|
}
|
|
@@ -17030,32 +17043,32 @@ function agentLabel(agent, mcpServer, sessionId) {
|
|
|
17030
17043
|
const tag = sessionTag(sessionId);
|
|
17031
17044
|
const tagSuffix = tag ? `\xB7${tag}` : "";
|
|
17032
17045
|
if (!agent || agent === "Terminal") {
|
|
17033
|
-
return mcpServer ?
|
|
17046
|
+
return mcpServer ? import_chalk32.default.dim(`[\u2192 ${mcpServer}] `) : "";
|
|
17034
17047
|
}
|
|
17035
17048
|
const short = agent === "Claude Code" ? "Claude" : agent === "Gemini CLI" ? "Gemini" : agent === "Unknown Agent" ? "" : agent.split(" ")[0];
|
|
17036
|
-
if (!short) return mcpServer ?
|
|
17037
|
-
return mcpServer ?
|
|
17049
|
+
if (!short) return mcpServer ? import_chalk32.default.dim(`[\u2192 ${mcpServer}] `) : "";
|
|
17050
|
+
return mcpServer ? import_chalk32.default.dim(`[${short}${tagSuffix} \u2192 ${mcpServer}] `) : import_chalk32.default.dim(`[${short}${tagSuffix}] `);
|
|
17038
17051
|
}
|
|
17039
17052
|
function formatBase(activity) {
|
|
17040
17053
|
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
17041
17054
|
const icon = getIcon(activity.tool);
|
|
17042
17055
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
17043
|
-
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(
|
|
17056
|
+
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(import_os51.default.homedir(), "~");
|
|
17044
17057
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
17045
|
-
return `${
|
|
17058
|
+
return `${import_chalk32.default.gray(time)} ${icon} ${agentLabel(activity.agent, activity.mcpServer, activity.sessionId)}${import_chalk32.default.white.bold(toolName)} ${import_chalk32.default.dim(argsPreview)}`;
|
|
17046
17059
|
}
|
|
17047
17060
|
function renderResult(activity, result) {
|
|
17048
17061
|
const base = formatBase(activity);
|
|
17049
17062
|
let status;
|
|
17050
17063
|
if (result.status === "allow") {
|
|
17051
|
-
status =
|
|
17064
|
+
status = import_chalk32.default.green("\u2713 ALLOW");
|
|
17052
17065
|
} else if (result.status === "dlp") {
|
|
17053
|
-
status =
|
|
17066
|
+
status = import_chalk32.default.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
17054
17067
|
} else {
|
|
17055
|
-
status =
|
|
17068
|
+
status = import_chalk32.default.red("\u2717 BLOCK");
|
|
17056
17069
|
}
|
|
17057
17070
|
const cost = result.costEstimate ?? activity.costEstimate;
|
|
17058
|
-
const costSuffix = cost == null ? "" :
|
|
17071
|
+
const costSuffix = cost == null ? "" : import_chalk32.default.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
|
|
17059
17072
|
if (process.stdout.isTTY) {
|
|
17060
17073
|
if (pendingShownForId === activity.id && pendingWrappedLines > 1) {
|
|
17061
17074
|
import_readline6.default.moveCursor(process.stdout, 0, -(pendingWrappedLines - 1));
|
|
@@ -17072,19 +17085,19 @@ function renderResult(activity, result) {
|
|
|
17072
17085
|
}
|
|
17073
17086
|
function renderPending(activity) {
|
|
17074
17087
|
if (!process.stdout.isTTY) return;
|
|
17075
|
-
const line = `${formatBase(activity)} ${
|
|
17088
|
+
const line = `${formatBase(activity)} ${import_chalk32.default.yellow("\u25CF \u2026")}`;
|
|
17076
17089
|
pendingShownForId = activity.id;
|
|
17077
17090
|
pendingWrappedLines = wrappedLineCount(line);
|
|
17078
17091
|
process.stdout.write(`${line}\r`);
|
|
17079
17092
|
}
|
|
17080
17093
|
async function ensureDaemon() {
|
|
17081
17094
|
let pidPort = null;
|
|
17082
|
-
if (
|
|
17095
|
+
if (import_fs56.default.existsSync(PID_FILE)) {
|
|
17083
17096
|
try {
|
|
17084
|
-
const { port } = JSON.parse(
|
|
17097
|
+
const { port } = JSON.parse(import_fs56.default.readFileSync(PID_FILE, "utf-8"));
|
|
17085
17098
|
pidPort = port;
|
|
17086
17099
|
} catch {
|
|
17087
|
-
console.error(
|
|
17100
|
+
console.error(import_chalk32.default.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
17088
17101
|
}
|
|
17089
17102
|
}
|
|
17090
17103
|
const checkPort = pidPort ?? DAEMON_PORT;
|
|
@@ -17095,7 +17108,7 @@ async function ensureDaemon() {
|
|
|
17095
17108
|
if (res.ok) return checkPort;
|
|
17096
17109
|
} catch {
|
|
17097
17110
|
}
|
|
17098
|
-
console.log(
|
|
17111
|
+
console.log(import_chalk32.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
17099
17112
|
const child = (0, import_child_process12.spawn)(process.execPath, [process.argv[1], "daemon"], {
|
|
17100
17113
|
detached: true,
|
|
17101
17114
|
stdio: "ignore",
|
|
@@ -17112,7 +17125,7 @@ async function ensureDaemon() {
|
|
|
17112
17125
|
} catch {
|
|
17113
17126
|
}
|
|
17114
17127
|
}
|
|
17115
|
-
console.error(
|
|
17128
|
+
console.error(import_chalk32.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
17116
17129
|
process.exit(1);
|
|
17117
17130
|
}
|
|
17118
17131
|
function postDecisionHttp(id, decision, authToken, port, opts) {
|
|
@@ -17122,7 +17135,7 @@ function postDecisionHttp(id, decision, authToken, port, opts) {
|
|
|
17122
17135
|
if (opts?.trustDuration) bodyObj.trustDuration = opts.trustDuration;
|
|
17123
17136
|
if (opts?.reason) bodyObj.reason = opts.reason;
|
|
17124
17137
|
const body = JSON.stringify(bodyObj);
|
|
17125
|
-
const req =
|
|
17138
|
+
const req = import_http3.default.request(
|
|
17126
17139
|
{
|
|
17127
17140
|
hostname: "127.0.0.1",
|
|
17128
17141
|
port,
|
|
@@ -17181,7 +17194,7 @@ function buildCardLines(req, localCount = 0) {
|
|
|
17181
17194
|
const severityIcon = isBlock ? `${RED}\u{1F6D1}` : `${YELLOW}\u26A0 `;
|
|
17182
17195
|
const rawDesc = req.riskMetadata?.ruleDescription ?? "";
|
|
17183
17196
|
const description = rawDesc ? cleanReason(rawDesc) : "";
|
|
17184
|
-
const agentSuffix = req.agent && req.agent !== "Terminal" ? ` ${RESET2}${
|
|
17197
|
+
const agentSuffix = req.agent && req.agent !== "Terminal" ? ` ${RESET2}${import_chalk32.default.dim(`(${req.agent})`)}` : "";
|
|
17185
17198
|
const lines = [
|
|
17186
17199
|
``,
|
|
17187
17200
|
`${BOLD2}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET2}`,
|
|
@@ -17237,9 +17250,9 @@ function buildRecoveryCardLines(req) {
|
|
|
17237
17250
|
];
|
|
17238
17251
|
}
|
|
17239
17252
|
function readApproversFromDisk() {
|
|
17240
|
-
const
|
|
17253
|
+
const configPath2 = import_path55.default.join(import_os51.default.homedir(), ".node9", "config.json");
|
|
17241
17254
|
try {
|
|
17242
|
-
const raw = JSON.parse(
|
|
17255
|
+
const raw = JSON.parse(import_fs56.default.readFileSync(configPath2, "utf-8"));
|
|
17243
17256
|
const settings = raw.settings ?? {};
|
|
17244
17257
|
return settings.approvers ?? {};
|
|
17245
17258
|
} catch {
|
|
@@ -17248,22 +17261,22 @@ function readApproversFromDisk() {
|
|
|
17248
17261
|
}
|
|
17249
17262
|
function approverStatusLine() {
|
|
17250
17263
|
const a = readApproversFromDisk();
|
|
17251
|
-
const fmt = (
|
|
17264
|
+
const fmt = (label2, key) => {
|
|
17252
17265
|
const on = a[key] !== false;
|
|
17253
|
-
return `[${key[0]}]${
|
|
17266
|
+
return `[${key[0]}]${label2.slice(1)} ${on ? import_chalk32.default.green("\u2713") : import_chalk32.default.dim("\u2717")}`;
|
|
17254
17267
|
};
|
|
17255
17268
|
return `${fmt("native", "native")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
|
|
17256
17269
|
}
|
|
17257
17270
|
function toggleApprover(channel) {
|
|
17258
|
-
const
|
|
17271
|
+
const configPath2 = import_path55.default.join(import_os51.default.homedir(), ".node9", "config.json");
|
|
17259
17272
|
try {
|
|
17260
|
-
const raw = JSON.parse(
|
|
17273
|
+
const raw = JSON.parse(import_fs56.default.readFileSync(configPath2, "utf-8"));
|
|
17261
17274
|
const settings = raw.settings ?? {};
|
|
17262
17275
|
const approvers = settings.approvers ?? {};
|
|
17263
17276
|
approvers[channel] = approvers[channel] === false;
|
|
17264
17277
|
settings.approvers = approvers;
|
|
17265
17278
|
raw.settings = settings;
|
|
17266
|
-
|
|
17279
|
+
import_fs56.default.writeFileSync(configPath2, JSON.stringify(raw, null, 2) + "\n");
|
|
17267
17280
|
} catch (err2) {
|
|
17268
17281
|
process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
|
|
17269
17282
|
`);
|
|
@@ -17273,7 +17286,7 @@ async function startTail(options = {}) {
|
|
|
17273
17286
|
const port = await ensureDaemon();
|
|
17274
17287
|
if (options.clear) {
|
|
17275
17288
|
const result = await new Promise((resolve) => {
|
|
17276
|
-
const req2 =
|
|
17289
|
+
const req2 = import_http3.default.request(
|
|
17277
17290
|
{ method: "POST", hostname: "127.0.0.1", port, path: "/events/clear" },
|
|
17278
17291
|
(res) => {
|
|
17279
17292
|
const status = res.statusCode ?? 0;
|
|
@@ -17295,7 +17308,7 @@ async function startTail(options = {}) {
|
|
|
17295
17308
|
req2.end();
|
|
17296
17309
|
});
|
|
17297
17310
|
if (result.ok) {
|
|
17298
|
-
console.log(
|
|
17311
|
+
console.log(import_chalk32.default.green("\u2713 Flight Recorder buffer cleared."));
|
|
17299
17312
|
} else if (result.code === "ECONNREFUSED") {
|
|
17300
17313
|
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
17301
17314
|
} else if (result.code === "ETIMEDOUT") {
|
|
@@ -17341,7 +17354,7 @@ async function startTail(options = {}) {
|
|
|
17341
17354
|
const channel = name === "n" ? "native" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
|
|
17342
17355
|
if (channel) {
|
|
17343
17356
|
toggleApprover(channel);
|
|
17344
|
-
console.log(
|
|
17357
|
+
console.log(import_chalk32.default.dim(` Approvers: ${approverStatusLine()}`));
|
|
17345
17358
|
}
|
|
17346
17359
|
};
|
|
17347
17360
|
process.stdin.on("keypress", idleKeypressHandler);
|
|
@@ -17407,7 +17420,7 @@ async function startTail(options = {}) {
|
|
|
17407
17420
|
localAllowCounts.get(req2.toolName) ?? 0
|
|
17408
17421
|
)
|
|
17409
17422
|
);
|
|
17410
|
-
const decisionStamp = action === "always-allow" ?
|
|
17423
|
+
const decisionStamp = action === "always-allow" ? import_chalk32.default.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? import_chalk32.default.cyan("\u23F1 TRUST 30m") : action === "allow" ? import_chalk32.default.green("\u2713 ALLOWED") : action === "redirect" ? import_chalk32.default.yellow("\u21A9 REDIRECT AI") : import_chalk32.default.red("\u2717 DENIED");
|
|
17411
17424
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
|
|
17412
17425
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
17413
17426
|
process.stdout.write(SHOW_CURSOR);
|
|
@@ -17435,8 +17448,8 @@ async function startTail(options = {}) {
|
|
|
17435
17448
|
}
|
|
17436
17449
|
postDecisionHttp(req2.id, httpDecision, authToken, port, httpOpts).catch((err2) => {
|
|
17437
17450
|
try {
|
|
17438
|
-
|
|
17439
|
-
|
|
17451
|
+
import_fs56.default.appendFileSync(
|
|
17452
|
+
import_path55.default.join(import_os51.default.homedir(), ".node9", "hook-debug.log"),
|
|
17440
17453
|
`[tail] POST /decision failed: ${String(err2)}
|
|
17441
17454
|
`
|
|
17442
17455
|
);
|
|
@@ -17458,7 +17471,7 @@ async function startTail(options = {}) {
|
|
|
17458
17471
|
);
|
|
17459
17472
|
const stampedLines = buildCardLines(req2, priorCount);
|
|
17460
17473
|
if (externalDecision) {
|
|
17461
|
-
const source = externalDecision === "allow" ?
|
|
17474
|
+
const source = externalDecision === "allow" ? import_chalk32.default.green("\u2713 ALLOWED") : import_chalk32.default.red("\u2717 DENIED");
|
|
17462
17475
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
|
|
17463
17476
|
}
|
|
17464
17477
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
@@ -17500,31 +17513,31 @@ async function startTail(options = {}) {
|
|
|
17500
17513
|
};
|
|
17501
17514
|
process.stdin.on("keypress", onKeypress);
|
|
17502
17515
|
}
|
|
17503
|
-
const auditLog =
|
|
17516
|
+
const auditLog = import_path55.default.join(import_os51.default.homedir(), ".node9", "audit.log");
|
|
17504
17517
|
try {
|
|
17505
|
-
const unackedDlp =
|
|
17518
|
+
const unackedDlp = import_fs56.default.readFileSync(auditLog, "utf-8").split("\n").filter((l) => l.includes('"response-dlp"')).length;
|
|
17506
17519
|
if (unackedDlp > 0) {
|
|
17507
17520
|
console.log("");
|
|
17508
17521
|
console.log(
|
|
17509
|
-
|
|
17522
|
+
import_chalk32.default.bgRed.white.bold(
|
|
17510
17523
|
` \u26A0\uFE0F DLP ALERT: ${unackedDlp} secret${unackedDlp !== 1 ? "s" : ""} found in Claude response text \u2014 run: node9 dlp `
|
|
17511
17524
|
)
|
|
17512
17525
|
);
|
|
17513
17526
|
}
|
|
17514
17527
|
} catch {
|
|
17515
17528
|
}
|
|
17516
|
-
console.log(
|
|
17529
|
+
console.log(import_chalk32.default.cyan.bold(`
|
|
17517
17530
|
\u{1F6F0}\uFE0F Node9 tail`));
|
|
17518
17531
|
if (canApprove) {
|
|
17519
|
-
console.log(
|
|
17520
|
-
console.log(
|
|
17532
|
+
console.log(import_chalk32.default.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
|
|
17533
|
+
console.log(import_chalk32.default.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
|
|
17521
17534
|
}
|
|
17522
17535
|
const ctxStat = readSessionUsage();
|
|
17523
17536
|
if (ctxStat) console.log(" " + formatContextStat(ctxStat));
|
|
17524
17537
|
if (options.history) {
|
|
17525
|
-
console.log(
|
|
17538
|
+
console.log(import_chalk32.default.dim("Showing history + live events.\n"));
|
|
17526
17539
|
} else {
|
|
17527
|
-
console.log(
|
|
17540
|
+
console.log(import_chalk32.default.dim("Showing live events only. Use --history to include past.\n"));
|
|
17528
17541
|
}
|
|
17529
17542
|
process.on("SIGINT", () => {
|
|
17530
17543
|
exitIdleMode();
|
|
@@ -17534,7 +17547,7 @@ async function startTail(options = {}) {
|
|
|
17534
17547
|
import_readline6.default.clearLine(process.stdout, 0);
|
|
17535
17548
|
import_readline6.default.cursorTo(process.stdout, 0);
|
|
17536
17549
|
}
|
|
17537
|
-
console.log(
|
|
17550
|
+
console.log(import_chalk32.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
17538
17551
|
process.exit(0);
|
|
17539
17552
|
});
|
|
17540
17553
|
const STALL_THRESHOLD_MS = 6e4;
|
|
@@ -17542,11 +17555,11 @@ async function startTail(options = {}) {
|
|
|
17542
17555
|
if (stallWarned) return;
|
|
17543
17556
|
if (Date.now() - lastActivityFromDaemon < STALL_THRESHOLD_MS) return;
|
|
17544
17557
|
try {
|
|
17545
|
-
const auditMtime =
|
|
17558
|
+
const auditMtime = import_fs56.default.statSync(auditLog).mtimeMs;
|
|
17546
17559
|
if (Date.now() - auditMtime >= STALL_THRESHOLD_MS) return;
|
|
17547
17560
|
console.log("");
|
|
17548
17561
|
console.log(
|
|
17549
|
-
|
|
17562
|
+
import_chalk32.default.yellow(
|
|
17550
17563
|
"\u26A0\uFE0F Tail appears stalled \u2014 hooks are firing but no events are arriving. Try: node9 daemon restart"
|
|
17551
17564
|
)
|
|
17552
17565
|
);
|
|
@@ -17556,14 +17569,14 @@ async function startTail(options = {}) {
|
|
|
17556
17569
|
}, STALL_THRESHOLD_MS / 2);
|
|
17557
17570
|
stallWatchdog.unref();
|
|
17558
17571
|
const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
|
|
17559
|
-
const req =
|
|
17572
|
+
const req = import_http3.default.get(
|
|
17560
17573
|
sseUrl,
|
|
17561
17574
|
{
|
|
17562
17575
|
headers: authToken ? { "X-Node9-Internal": authToken } : {}
|
|
17563
17576
|
},
|
|
17564
17577
|
(res) => {
|
|
17565
17578
|
if (res.statusCode !== 200) {
|
|
17566
|
-
console.error(
|
|
17579
|
+
console.error(import_chalk32.default.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
17567
17580
|
process.exit(1);
|
|
17568
17581
|
}
|
|
17569
17582
|
if (canApprove) enterIdleMode();
|
|
@@ -17594,7 +17607,7 @@ async function startTail(options = {}) {
|
|
|
17594
17607
|
import_readline6.default.clearLine(process.stdout, 0);
|
|
17595
17608
|
import_readline6.default.cursorTo(process.stdout, 0);
|
|
17596
17609
|
}
|
|
17597
|
-
console.log(
|
|
17610
|
+
console.log(import_chalk32.default.red("\n\u274C Daemon disconnected."));
|
|
17598
17611
|
process.exit(1);
|
|
17599
17612
|
});
|
|
17600
17613
|
}
|
|
@@ -17607,7 +17620,7 @@ async function startTail(options = {}) {
|
|
|
17607
17620
|
const parsed = JSON.parse(rawData);
|
|
17608
17621
|
const msg = parsed.message ?? "Flight recorder is down \u2014 run: node9 daemon restart";
|
|
17609
17622
|
console.log("");
|
|
17610
|
-
console.log(
|
|
17623
|
+
console.log(import_chalk32.default.bgRed.white.bold(` \u26A0\uFE0F ${msg} `));
|
|
17611
17624
|
} catch {
|
|
17612
17625
|
}
|
|
17613
17626
|
return;
|
|
@@ -17692,9 +17705,9 @@ async function startTail(options = {}) {
|
|
|
17692
17705
|
const rawSummary = data.argsSummary ?? data.tool;
|
|
17693
17706
|
const summary = shortenPathSummary(rawSummary);
|
|
17694
17707
|
const fileCount = data.fileCount ?? 0;
|
|
17695
|
-
const files = fileCount > 0 ?
|
|
17708
|
+
const files = fileCount > 0 ? import_chalk32.default.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
|
|
17696
17709
|
process.stdout.write(
|
|
17697
|
-
`${
|
|
17710
|
+
`${import_chalk32.default.dim(time)} ${import_chalk32.default.cyan("\u{1F4F8} snapshot")} ${import_chalk32.default.dim(hash)} ${summary}${files}
|
|
17698
17711
|
`
|
|
17699
17712
|
);
|
|
17700
17713
|
return;
|
|
@@ -17711,36 +17724,36 @@ async function startTail(options = {}) {
|
|
|
17711
17724
|
if (event === "execution-result") {
|
|
17712
17725
|
const exec = data;
|
|
17713
17726
|
const time = new Date(Date.now()).toLocaleTimeString([], { hour12: false });
|
|
17714
|
-
const arrow = exec.isError ?
|
|
17715
|
-
const
|
|
17727
|
+
const arrow = exec.isError ? import_chalk32.default.red(" \u21B3 \u2717") : import_chalk32.default.green(" \u21B3 \u2713");
|
|
17728
|
+
const label2 = agentLabel(exec.agent, exec.mcpServer);
|
|
17716
17729
|
const tool = (exec.tool ?? "").slice(0, 16);
|
|
17717
|
-
const duration = typeof exec.durationMs === "number" ?
|
|
17730
|
+
const duration = typeof exec.durationMs === "number" ? import_chalk32.default.dim(` (${exec.durationMs}ms)`) : "";
|
|
17718
17731
|
console.log(
|
|
17719
|
-
`${
|
|
17732
|
+
`${import_chalk32.default.gray(time)} ${arrow} ${label2}${import_chalk32.default.dim(tool)}${import_chalk32.default.dim(" completed")}${duration}`
|
|
17720
17733
|
);
|
|
17721
17734
|
}
|
|
17722
17735
|
}
|
|
17723
17736
|
req.on("error", (err2) => {
|
|
17724
17737
|
const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
|
|
17725
|
-
console.error(
|
|
17738
|
+
console.error(import_chalk32.default.red(`
|
|
17726
17739
|
\u274C ${msg}`));
|
|
17727
17740
|
process.exit(1);
|
|
17728
17741
|
});
|
|
17729
17742
|
}
|
|
17730
|
-
var
|
|
17743
|
+
var import_http3, import_chalk32, import_fs56, import_os51, import_path55, import_readline6, import_child_process12, PID_FILE, ICONS, MODEL_CONTEXT_LIMITS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, pendingShownForId, pendingWrappedLines, DIVIDER;
|
|
17731
17744
|
var init_tail = __esm({
|
|
17732
17745
|
"src/tui/tail.ts"() {
|
|
17733
17746
|
"use strict";
|
|
17734
|
-
|
|
17735
|
-
|
|
17736
|
-
|
|
17737
|
-
|
|
17738
|
-
|
|
17747
|
+
import_http3 = __toESM(require("http"));
|
|
17748
|
+
import_chalk32 = __toESM(require("chalk"));
|
|
17749
|
+
import_fs56 = __toESM(require("fs"));
|
|
17750
|
+
import_os51 = __toESM(require("os"));
|
|
17751
|
+
import_path55 = __toESM(require("path"));
|
|
17739
17752
|
import_readline6 = __toESM(require("readline"));
|
|
17740
17753
|
import_child_process12 = require("child_process");
|
|
17741
17754
|
init_daemon2();
|
|
17742
17755
|
init_daemon();
|
|
17743
|
-
PID_FILE =
|
|
17756
|
+
PID_FILE = import_path55.default.join(import_os51.default.homedir(), ".node9", "daemon.pid");
|
|
17744
17757
|
ICONS = {
|
|
17745
17758
|
bash: "\u{1F4BB}",
|
|
17746
17759
|
shell: "\u{1F4BB}",
|
|
@@ -17805,7 +17818,7 @@ function queryDaemon() {
|
|
|
17805
17818
|
return new Promise((resolve) => {
|
|
17806
17819
|
const timeout = setTimeout(() => resolve(null), 50);
|
|
17807
17820
|
try {
|
|
17808
|
-
const req =
|
|
17821
|
+
const req = import_http4.default.get(
|
|
17809
17822
|
`http://${DAEMON_HOST}:${DAEMON_PORT}/status`,
|
|
17810
17823
|
{ timeout: 50 },
|
|
17811
17824
|
(res) => {
|
|
@@ -17862,9 +17875,9 @@ function formatTimeLeft(resetsAt) {
|
|
|
17862
17875
|
return ` (${m}m left)`;
|
|
17863
17876
|
}
|
|
17864
17877
|
function safeReadJson(filePath) {
|
|
17865
|
-
if (!
|
|
17878
|
+
if (!import_fs57.default.existsSync(filePath)) return null;
|
|
17866
17879
|
try {
|
|
17867
|
-
return JSON.parse(
|
|
17880
|
+
return JSON.parse(import_fs57.default.readFileSync(filePath, "utf-8"));
|
|
17868
17881
|
} catch {
|
|
17869
17882
|
return null;
|
|
17870
17883
|
}
|
|
@@ -17885,12 +17898,12 @@ function countHooksInFile(filePath) {
|
|
|
17885
17898
|
return Object.keys(cfg.hooks).length;
|
|
17886
17899
|
}
|
|
17887
17900
|
function countRulesInDir(rulesDir) {
|
|
17888
|
-
if (!
|
|
17901
|
+
if (!import_fs57.default.existsSync(rulesDir)) return 0;
|
|
17889
17902
|
let count = 0;
|
|
17890
17903
|
try {
|
|
17891
|
-
for (const entry of
|
|
17904
|
+
for (const entry of import_fs57.default.readdirSync(rulesDir, { withFileTypes: true })) {
|
|
17892
17905
|
if (entry.isDirectory()) {
|
|
17893
|
-
count += countRulesInDir(
|
|
17906
|
+
count += countRulesInDir(import_path56.default.join(rulesDir, entry.name));
|
|
17894
17907
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
17895
17908
|
count++;
|
|
17896
17909
|
}
|
|
@@ -17901,46 +17914,46 @@ function countRulesInDir(rulesDir) {
|
|
|
17901
17914
|
}
|
|
17902
17915
|
function isSamePath(a, b) {
|
|
17903
17916
|
try {
|
|
17904
|
-
return
|
|
17917
|
+
return import_path56.default.resolve(a) === import_path56.default.resolve(b);
|
|
17905
17918
|
} catch {
|
|
17906
17919
|
return false;
|
|
17907
17920
|
}
|
|
17908
17921
|
}
|
|
17909
17922
|
function countConfigs(cwd) {
|
|
17910
|
-
const homeDir2 =
|
|
17911
|
-
const claudeDir =
|
|
17923
|
+
const homeDir2 = import_os52.default.homedir();
|
|
17924
|
+
const claudeDir = import_path56.default.join(homeDir2, ".claude");
|
|
17912
17925
|
let claudeMdCount = 0;
|
|
17913
17926
|
let rulesCount = 0;
|
|
17914
17927
|
let hooksCount = 0;
|
|
17915
17928
|
const userMcpServers = /* @__PURE__ */ new Set();
|
|
17916
17929
|
const projectMcpServers = /* @__PURE__ */ new Set();
|
|
17917
|
-
if (
|
|
17918
|
-
rulesCount += countRulesInDir(
|
|
17919
|
-
const userSettings =
|
|
17930
|
+
if (import_fs57.default.existsSync(import_path56.default.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
17931
|
+
rulesCount += countRulesInDir(import_path56.default.join(claudeDir, "rules"));
|
|
17932
|
+
const userSettings = import_path56.default.join(claudeDir, "settings.json");
|
|
17920
17933
|
for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
|
|
17921
17934
|
hooksCount += countHooksInFile(userSettings);
|
|
17922
|
-
const userClaudeJson =
|
|
17935
|
+
const userClaudeJson = import_path56.default.join(homeDir2, ".claude.json");
|
|
17923
17936
|
for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
|
|
17924
17937
|
for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
|
|
17925
17938
|
userMcpServers.delete(name);
|
|
17926
17939
|
}
|
|
17927
17940
|
if (cwd) {
|
|
17928
|
-
if (
|
|
17929
|
-
if (
|
|
17930
|
-
const projectClaudeDir =
|
|
17941
|
+
if (import_fs57.default.existsSync(import_path56.default.join(cwd, "CLAUDE.md"))) claudeMdCount++;
|
|
17942
|
+
if (import_fs57.default.existsSync(import_path56.default.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
|
|
17943
|
+
const projectClaudeDir = import_path56.default.join(cwd, ".claude");
|
|
17931
17944
|
const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
|
|
17932
17945
|
if (!overlapsUserScope) {
|
|
17933
|
-
if (
|
|
17934
|
-
rulesCount += countRulesInDir(
|
|
17935
|
-
const projSettings =
|
|
17946
|
+
if (import_fs57.default.existsSync(import_path56.default.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
17947
|
+
rulesCount += countRulesInDir(import_path56.default.join(projectClaudeDir, "rules"));
|
|
17948
|
+
const projSettings = import_path56.default.join(projectClaudeDir, "settings.json");
|
|
17936
17949
|
for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
|
|
17937
17950
|
hooksCount += countHooksInFile(projSettings);
|
|
17938
17951
|
}
|
|
17939
|
-
if (
|
|
17940
|
-
const localSettings =
|
|
17952
|
+
if (import_fs57.default.existsSync(import_path56.default.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
|
|
17953
|
+
const localSettings = import_path56.default.join(projectClaudeDir, "settings.local.json");
|
|
17941
17954
|
for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
|
|
17942
17955
|
hooksCount += countHooksInFile(localSettings);
|
|
17943
|
-
const mcpJsonServers = getMcpServerNames(
|
|
17956
|
+
const mcpJsonServers = getMcpServerNames(import_path56.default.join(cwd, ".mcp.json"));
|
|
17944
17957
|
const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
|
|
17945
17958
|
for (const name of disabledMcpJson) mcpJsonServers.delete(name);
|
|
17946
17959
|
for (const name of mcpJsonServers) projectMcpServers.add(name);
|
|
@@ -17973,12 +17986,12 @@ function readActiveShieldsHud() {
|
|
|
17973
17986
|
return shieldsCache.value;
|
|
17974
17987
|
}
|
|
17975
17988
|
try {
|
|
17976
|
-
const shieldsPath =
|
|
17977
|
-
if (!
|
|
17989
|
+
const shieldsPath = import_path56.default.join(import_os52.default.homedir(), ".node9", "shields.json");
|
|
17990
|
+
if (!import_fs57.default.existsSync(shieldsPath)) {
|
|
17978
17991
|
shieldsCache = { value: [], ts: now };
|
|
17979
17992
|
return [];
|
|
17980
17993
|
}
|
|
17981
|
-
const parsed = JSON.parse(
|
|
17994
|
+
const parsed = JSON.parse(import_fs57.default.readFileSync(shieldsPath, "utf-8"));
|
|
17982
17995
|
if (!Array.isArray(parsed.active)) {
|
|
17983
17996
|
shieldsCache = { value: [], ts: now };
|
|
17984
17997
|
return [];
|
|
@@ -18080,17 +18093,17 @@ function renderContextLine(stdin) {
|
|
|
18080
18093
|
async function main() {
|
|
18081
18094
|
try {
|
|
18082
18095
|
const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
|
|
18083
|
-
if (
|
|
18096
|
+
if (import_fs57.default.existsSync(import_path56.default.join(import_os52.default.homedir(), ".node9", "hud-debug"))) {
|
|
18084
18097
|
try {
|
|
18085
|
-
const logPath =
|
|
18098
|
+
const logPath = import_path56.default.join(import_os52.default.homedir(), ".node9", "hud-debug.log");
|
|
18086
18099
|
const MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
18087
18100
|
let size = 0;
|
|
18088
18101
|
try {
|
|
18089
|
-
size =
|
|
18102
|
+
size = import_fs57.default.statSync(logPath).size;
|
|
18090
18103
|
} catch {
|
|
18091
18104
|
}
|
|
18092
18105
|
if (size < MAX_LOG_SIZE) {
|
|
18093
|
-
|
|
18106
|
+
import_fs57.default.appendFileSync(
|
|
18094
18107
|
logPath,
|
|
18095
18108
|
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
|
|
18096
18109
|
);
|
|
@@ -18110,12 +18123,12 @@ async function main() {
|
|
|
18110
18123
|
const showEnvCounts = (() => {
|
|
18111
18124
|
try {
|
|
18112
18125
|
const cwd = stdin.cwd ?? process.cwd();
|
|
18113
|
-
for (const
|
|
18114
|
-
|
|
18115
|
-
|
|
18126
|
+
for (const configPath2 of [
|
|
18127
|
+
import_path56.default.join(cwd, "node9.config.json"),
|
|
18128
|
+
import_path56.default.join(import_os52.default.homedir(), ".node9", "config.json")
|
|
18116
18129
|
]) {
|
|
18117
|
-
if (!
|
|
18118
|
-
const cfg = JSON.parse(
|
|
18130
|
+
if (!import_fs57.default.existsSync(configPath2)) continue;
|
|
18131
|
+
const cfg = JSON.parse(import_fs57.default.readFileSync(configPath2, "utf-8"));
|
|
18119
18132
|
const hud = cfg.settings?.hud;
|
|
18120
18133
|
if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
|
|
18121
18134
|
}
|
|
@@ -18133,14 +18146,14 @@ async function main() {
|
|
|
18133
18146
|
renderOffline();
|
|
18134
18147
|
}
|
|
18135
18148
|
}
|
|
18136
|
-
var
|
|
18149
|
+
var import_fs57, import_path56, import_os52, import_http4, RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH, shieldsCache, SHIELDS_CACHE_TTL_MS;
|
|
18137
18150
|
var init_hud = __esm({
|
|
18138
18151
|
"src/cli/hud.ts"() {
|
|
18139
18152
|
"use strict";
|
|
18140
|
-
|
|
18141
|
-
|
|
18142
|
-
|
|
18143
|
-
|
|
18153
|
+
import_fs57 = __toESM(require("fs"));
|
|
18154
|
+
import_path56 = __toESM(require("path"));
|
|
18155
|
+
import_os52 = __toESM(require("os"));
|
|
18156
|
+
import_http4 = __toESM(require("http"));
|
|
18144
18157
|
init_daemon();
|
|
18145
18158
|
RESET3 = "\x1B[0m";
|
|
18146
18159
|
BOLD3 = "\x1B[1m";
|
|
@@ -18165,10 +18178,10 @@ var import_commander = require("commander");
|
|
|
18165
18178
|
init_core();
|
|
18166
18179
|
init_setup();
|
|
18167
18180
|
init_daemon2();
|
|
18168
|
-
var
|
|
18169
|
-
var
|
|
18170
|
-
var
|
|
18171
|
-
var
|
|
18181
|
+
var import_chalk33 = __toESM(require("chalk"));
|
|
18182
|
+
var import_fs58 = __toESM(require("fs"));
|
|
18183
|
+
var import_path57 = __toESM(require("path"));
|
|
18184
|
+
var import_os53 = __toESM(require("os"));
|
|
18172
18185
|
var import_prompts2 = require("@inquirer/prompts");
|
|
18173
18186
|
|
|
18174
18187
|
// src/utils/duration.ts
|
|
@@ -18208,8 +18221,8 @@ INSTRUCTIONS:
|
|
|
18208
18221
|
- Acknowledge the block to the user and ask if there is an alternative approach.
|
|
18209
18222
|
- If you believe this action is critical, explain your reasoning and ask them to run "node9 pause 15m" to proceed.`;
|
|
18210
18223
|
}
|
|
18211
|
-
const
|
|
18212
|
-
if (
|
|
18224
|
+
const label2 = blockedByLabel.toLowerCase();
|
|
18225
|
+
if (label2.includes("dlp") || label2.includes("secret detected") || label2.includes("credential review")) {
|
|
18213
18226
|
return `NODE9 SECURITY ALERT: A sensitive credential (API key, token, or private key) was found in your tool call arguments.
|
|
18214
18227
|
CRITICAL INSTRUCTION: Do NOT retry this action.
|
|
18215
18228
|
REQUIRED ACTIONS:
|
|
@@ -18218,37 +18231,37 @@ REQUIRED ACTIONS:
|
|
|
18218
18231
|
3. Treat the leaked credential as compromised and rotate it immediately.
|
|
18219
18232
|
Do NOT attempt to bypass this check or pass the credential through another tool.`;
|
|
18220
18233
|
}
|
|
18221
|
-
if (
|
|
18234
|
+
if (label2.includes("sql safety") && label2.includes("delete without where")) {
|
|
18222
18235
|
return `NODE9: Blocked \u2014 DELETE without WHERE clause would wipe the entire table.
|
|
18223
18236
|
INSTRUCTION: Add a WHERE clause to scope the deletion (e.g. WHERE id = <value>).
|
|
18224
18237
|
Do NOT retry without a WHERE clause.`;
|
|
18225
18238
|
}
|
|
18226
|
-
if (
|
|
18239
|
+
if (label2.includes("sql safety") && label2.includes("update without where")) {
|
|
18227
18240
|
return `NODE9: Blocked \u2014 UPDATE without WHERE clause would update every row.
|
|
18228
18241
|
INSTRUCTION: Add a WHERE clause to scope the update (e.g. WHERE id = <value>).
|
|
18229
18242
|
Do NOT retry without a WHERE clause.`;
|
|
18230
18243
|
}
|
|
18231
|
-
if (
|
|
18244
|
+
if (label2.includes("dangerous word")) {
|
|
18232
18245
|
const match = blockedByLabel.match(/dangerous word: "([^"]+)"/i);
|
|
18233
18246
|
const word = match?.[1] ?? "a dangerous keyword";
|
|
18234
18247
|
return `NODE9: Blocked \u2014 command contains forbidden keyword "${word}".
|
|
18235
18248
|
INSTRUCTION: Do NOT use "${word}". Use a non-destructive alternative.
|
|
18236
18249
|
Do NOT attempt to bypass this with shell tricks or aliases \u2014 it will be blocked again.`;
|
|
18237
18250
|
}
|
|
18238
|
-
if (
|
|
18251
|
+
if (label2.includes("path blocked") || label2.includes("sandbox")) {
|
|
18239
18252
|
return `NODE9: Blocked \u2014 operation targets a path outside the allowed sandbox.
|
|
18240
18253
|
INSTRUCTION: Move your output to an allowed directory such as /tmp/ or the project directory.
|
|
18241
18254
|
Do NOT retry on the same path.`;
|
|
18242
18255
|
}
|
|
18243
|
-
if (
|
|
18256
|
+
if (label2.includes("inline execution")) {
|
|
18244
18257
|
return `NODE9: Blocked \u2014 inline code execution (e.g. bash -c "...") is not allowed.
|
|
18245
18258
|
INSTRUCTION: Use individual tool calls instead of embedding code in a shell string.`;
|
|
18246
18259
|
}
|
|
18247
|
-
if (
|
|
18260
|
+
if (label2.includes("strict mode")) {
|
|
18248
18261
|
return `NODE9: Blocked \u2014 strict mode is active. All tool calls require explicit human approval.
|
|
18249
18262
|
INSTRUCTION: Inform the user this action is pending approval. Wait for them to approve via the dashboard or run "node9 pause".`;
|
|
18250
18263
|
}
|
|
18251
|
-
if (
|
|
18264
|
+
if (label2.includes("rule") && label2.includes("default block")) {
|
|
18252
18265
|
const match = blockedByLabel.match(/rule "([^"]+)"/i);
|
|
18253
18266
|
const rule = match?.[1] ?? "a policy rule";
|
|
18254
18267
|
return `NODE9: Blocked \u2014 action "${rule}" is forbidden by security policy.
|
|
@@ -20039,7 +20052,7 @@ var AGENT_SPECS = [
|
|
|
20039
20052
|
{
|
|
20040
20053
|
id: "claude",
|
|
20041
20054
|
label: "Claude Code",
|
|
20042
|
-
setupCommand: "node9
|
|
20055
|
+
setupCommand: "node9 agents add claude",
|
|
20043
20056
|
hookFile: (h) => import_path39.default.join(h, ".claude", "settings.json"),
|
|
20044
20057
|
hookFormat: "matcher",
|
|
20045
20058
|
hookEvents: [ck("PreToolUse"), lg("PostToolUse")],
|
|
@@ -20049,7 +20062,7 @@ var AGENT_SPECS = [
|
|
|
20049
20062
|
{
|
|
20050
20063
|
id: "gemini",
|
|
20051
20064
|
label: "Gemini CLI",
|
|
20052
|
-
setupCommand: "node9
|
|
20065
|
+
setupCommand: "node9 agents add gemini",
|
|
20053
20066
|
hookFile: (h) => import_path39.default.join(h, ".gemini", "settings.json"),
|
|
20054
20067
|
hookFormat: "matcher",
|
|
20055
20068
|
hookEvents: [ck("BeforeTool"), lg("AfterTool")],
|
|
@@ -20059,7 +20072,7 @@ var AGENT_SPECS = [
|
|
|
20059
20072
|
{
|
|
20060
20073
|
id: "codex",
|
|
20061
20074
|
label: "Codex",
|
|
20062
|
-
setupCommand: "node9
|
|
20075
|
+
setupCommand: "node9 agents add codex",
|
|
20063
20076
|
hookFile: (h) => import_path39.default.join(h, ".codex", "hooks.json"),
|
|
20064
20077
|
hookFormat: "matcher",
|
|
20065
20078
|
hookEvents: [ck("PreToolUse"), ck("UserPromptSubmit")],
|
|
@@ -20070,7 +20083,7 @@ var AGENT_SPECS = [
|
|
|
20070
20083
|
{
|
|
20071
20084
|
id: "antigravity",
|
|
20072
20085
|
label: "Antigravity",
|
|
20073
|
-
setupCommand: "node9
|
|
20086
|
+
setupCommand: "node9 agents add antigravity",
|
|
20074
20087
|
hookFile: (h) => import_path39.default.join(h, ".gemini", "config", "hooks.json"),
|
|
20075
20088
|
hookFormat: "matcher",
|
|
20076
20089
|
hookEvents: [ck("PreToolUse"), lg("PostToolUse")],
|
|
@@ -20080,7 +20093,7 @@ var AGENT_SPECS = [
|
|
|
20080
20093
|
{
|
|
20081
20094
|
id: "copilot",
|
|
20082
20095
|
label: "GitHub Copilot",
|
|
20083
|
-
setupCommand: "node9
|
|
20096
|
+
setupCommand: "node9 agents add copilot",
|
|
20084
20097
|
hookFile: (h) => import_path39.default.join(h, ".copilot", "hooks", "node9.json"),
|
|
20085
20098
|
hookFormat: "flat",
|
|
20086
20099
|
hookEvents: [ck("PreToolUse"), lg("PostToolUse"), ck("UserPromptSubmit")],
|
|
@@ -20090,7 +20103,7 @@ var AGENT_SPECS = [
|
|
|
20090
20103
|
{
|
|
20091
20104
|
id: "cursor",
|
|
20092
20105
|
label: "Cursor",
|
|
20093
|
-
setupCommand: "node9
|
|
20106
|
+
setupCommand: "node9 agents add cursor",
|
|
20094
20107
|
// MCP-only — no hook file (see note above).
|
|
20095
20108
|
hookFormat: "flat",
|
|
20096
20109
|
hookEvents: [],
|
|
@@ -20100,7 +20113,7 @@ var AGENT_SPECS = [
|
|
|
20100
20113
|
{
|
|
20101
20114
|
id: "hermes",
|
|
20102
20115
|
label: "Hermes Agent",
|
|
20103
|
-
setupCommand: "node9
|
|
20116
|
+
setupCommand: "node9 agents add hermes",
|
|
20104
20117
|
hookFile: (h) => hermesConfigPath(h),
|
|
20105
20118
|
hookFormat: "yaml",
|
|
20106
20119
|
hookEvents: [ck("pre_tool_call"), lg("post_tool_call")],
|
|
@@ -20113,7 +20126,7 @@ var AGENT_SPECS = [
|
|
|
20113
20126
|
// (no hooks, no MCP). hookFormat is unused for these (shimFile drives it).
|
|
20114
20127
|
id: "opencode",
|
|
20115
20128
|
label: "OpenCode",
|
|
20116
|
-
setupCommand: "node9
|
|
20129
|
+
setupCommand: "node9 agents add opencode",
|
|
20117
20130
|
hookFormat: "flat",
|
|
20118
20131
|
hookEvents: [],
|
|
20119
20132
|
shimFile: (h) => import_path39.default.join(h, ".config", "opencode", "plugins", "node9.js"),
|
|
@@ -20122,7 +20135,7 @@ var AGENT_SPECS = [
|
|
|
20122
20135
|
{
|
|
20123
20136
|
id: "pi",
|
|
20124
20137
|
label: "Pi",
|
|
20125
|
-
setupCommand: "node9
|
|
20138
|
+
setupCommand: "node9 agents add pi",
|
|
20126
20139
|
hookFormat: "flat",
|
|
20127
20140
|
hookEvents: [],
|
|
20128
20141
|
shimFile: (h) => import_path39.default.join(h, ".pi", "agent", "extensions", "node9.js"),
|
|
@@ -20206,10 +20219,7 @@ function registerDoctorCommand(program2, version2) {
|
|
|
20206
20219
|
const which = (0, import_child_process8.execSync)("which node9", { encoding: "utf-8", timeout: 3e3 }).trim();
|
|
20207
20220
|
pass(`node9 found at ${which}`);
|
|
20208
20221
|
} catch {
|
|
20209
|
-
warn(
|
|
20210
|
-
"node9 not found in $PATH \u2014 hooks may not find it",
|
|
20211
|
-
"Run: npm install -g @node9/proxy"
|
|
20212
|
-
);
|
|
20222
|
+
warn("node9 not found in $PATH \u2014 hooks may not find it", "Run: npm install -g node9-ai");
|
|
20213
20223
|
}
|
|
20214
20224
|
const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
|
20215
20225
|
if (nodeMajor >= 18) {
|
|
@@ -20279,7 +20289,7 @@ function registerDoctorCommand(program2, version2) {
|
|
|
20279
20289
|
if (notConfigured.length > 0) {
|
|
20280
20290
|
console.log(
|
|
20281
20291
|
import_chalk11.default.gray(
|
|
20282
|
-
` \xB7 Not configured: ${notConfigured.join(", ")} \u2014 run \`node9
|
|
20292
|
+
` \xB7 Not configured: ${notConfigured.join(", ")} \u2014 run \`node9 agents add <agent>\` if you use one`
|
|
20283
20293
|
)
|
|
20284
20294
|
);
|
|
20285
20295
|
}
|
|
@@ -21260,10 +21270,10 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
|
|
|
21260
21270
|
);
|
|
21261
21271
|
console.log("");
|
|
21262
21272
|
const COL1 = 18;
|
|
21263
|
-
const summaryRow = (icon,
|
|
21273
|
+
const summaryRow = (icon, label2, count, note, colorFn = (s) => s) => {
|
|
21264
21274
|
const countStr = colorFn(num2(count));
|
|
21265
21275
|
const noteStr = note ? import_chalk13.default.dim(" " + note) : "";
|
|
21266
|
-
console.log(" " + icon + " " + import_chalk13.default.white(
|
|
21276
|
+
console.log(" " + icon + " " + import_chalk13.default.white(label2.padEnd(COL1)) + countStr + noteStr);
|
|
21267
21277
|
};
|
|
21268
21278
|
summaryRow(
|
|
21269
21279
|
userApproved > 0 ? import_chalk13.default.green("\u2705") : import_chalk13.default.dim("\u2705"),
|
|
@@ -21330,21 +21340,21 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
|
|
|
21330
21340
|
let leftStyled = " ".repeat(COL);
|
|
21331
21341
|
if (i < topTools.length) {
|
|
21332
21342
|
const [tool, { calls }] = topTools[i];
|
|
21333
|
-
const
|
|
21343
|
+
const label2 = tool.length > LABEL - 1 ? tool.slice(0, LABEL - 2) + "\u2026" : tool;
|
|
21334
21344
|
const countStr = num2(calls).padStart(TOOL_COUNT_W);
|
|
21335
21345
|
const b = colorBar(calls, maxTool, BAR);
|
|
21336
21346
|
const rawLen = LABEL + BAR + 1 + TOOL_COUNT_W;
|
|
21337
21347
|
const pad = Math.max(0, COL - rawLen);
|
|
21338
|
-
leftStyled = import_chalk13.default.white(
|
|
21348
|
+
leftStyled = import_chalk13.default.white(label2.padEnd(LABEL)) + b + " " + import_chalk13.default.white(countStr) + " ".repeat(pad);
|
|
21339
21349
|
}
|
|
21340
21350
|
let rightStyled = "";
|
|
21341
21351
|
if (i < topBlocks.length) {
|
|
21342
21352
|
const [reason, count] = topBlocks[i];
|
|
21343
21353
|
const readable = humanBlockReason(reason);
|
|
21344
|
-
const
|
|
21354
|
+
const label2 = readable.length > LABEL - 1 ? readable.slice(0, LABEL - 2) + "\u2026" : readable;
|
|
21345
21355
|
const countStr = num2(count).padStart(BLOCK_COUNT_W);
|
|
21346
21356
|
const b = colorBar(count, maxBlock, BAR);
|
|
21347
|
-
rightStyled = import_chalk13.default.white(
|
|
21357
|
+
rightStyled = import_chalk13.default.white(label2.padEnd(LABEL)) + b + " " + import_chalk13.default.red(countStr);
|
|
21348
21358
|
}
|
|
21349
21359
|
console.log(" " + leftStyled + " " + rightStyled);
|
|
21350
21360
|
}
|
|
@@ -21357,9 +21367,9 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
|
|
|
21357
21367
|
console.log(" " + import_chalk13.default.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
21358
21368
|
const maxAgent = Math.max(...agentMap.values(), 1);
|
|
21359
21369
|
for (const [agent, count] of [...agentMap.entries()].sort((a, b) => b[1] - a[1])) {
|
|
21360
|
-
const
|
|
21370
|
+
const label2 = agent.slice(0, LABEL - 1);
|
|
21361
21371
|
const b = colorBar(count, maxAgent, BAR);
|
|
21362
|
-
console.log(" " + import_chalk13.default.white(
|
|
21372
|
+
console.log(" " + import_chalk13.default.white(label2.padEnd(LABEL)) + b + " " + import_chalk13.default.white(num2(count)));
|
|
21363
21373
|
}
|
|
21364
21374
|
}
|
|
21365
21375
|
if (mcpMap.size > 0) {
|
|
@@ -21368,9 +21378,9 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
|
|
|
21368
21378
|
console.log(" " + import_chalk13.default.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
21369
21379
|
const maxMcp = Math.max(...mcpMap.values(), 1);
|
|
21370
21380
|
for (const [server, count] of [...mcpMap.entries()].sort((a, b) => b[1] - a[1])) {
|
|
21371
|
-
const
|
|
21381
|
+
const label2 = server.slice(0, LABEL - 1).padEnd(LABEL);
|
|
21372
21382
|
const b = colorBar(count, maxMcp, BAR);
|
|
21373
|
-
console.log(" " + import_chalk13.default.white(
|
|
21383
|
+
console.log(" " + import_chalk13.default.white(label2) + b + " " + import_chalk13.default.white(num2(count)));
|
|
21374
21384
|
}
|
|
21375
21385
|
}
|
|
21376
21386
|
if (hourMap.size > 0) {
|
|
@@ -21391,13 +21401,13 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
|
|
|
21391
21401
|
console.log(" " + import_chalk13.default.dim("\u2500".repeat(W - 2)));
|
|
21392
21402
|
const DAY_BAR = Math.max(8, Math.min(30, W - 36));
|
|
21393
21403
|
for (const [dateKey, { calls, blocked: db }] of dailyList) {
|
|
21394
|
-
const
|
|
21404
|
+
const label2 = fmtDate(dateKey).padEnd(10);
|
|
21395
21405
|
const b = colorBar(calls, maxDaily, DAY_BAR);
|
|
21396
21406
|
const dayCost = costByDay.get(dateKey);
|
|
21397
21407
|
const costNote = dayCost ? import_chalk13.default.magenta(` ${fmtCost2(dayCost)}`) : "";
|
|
21398
21408
|
const blockNote = db > 0 ? import_chalk13.default.red(` ${db} blocked`) : "";
|
|
21399
21409
|
console.log(
|
|
21400
|
-
" " + import_chalk13.default.dim(
|
|
21410
|
+
" " + import_chalk13.default.dim(label2) + " " + b + " " + import_chalk13.default.white(num2(calls)) + blockNote + costNote
|
|
21401
21411
|
);
|
|
21402
21412
|
}
|
|
21403
21413
|
}
|
|
@@ -21415,10 +21425,10 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
|
|
|
21415
21425
|
["Output", costOutputTokens, import_chalk13.default.white(num2(costOutputTokens))],
|
|
21416
21426
|
["Cache write", costCacheWrite, import_chalk13.default.yellow(num2(costCacheWrite))]
|
|
21417
21427
|
];
|
|
21418
|
-
for (const [
|
|
21428
|
+
for (const [label2, count, colored] of nonCacheRows) {
|
|
21419
21429
|
if (count === 0) continue;
|
|
21420
21430
|
const b = colorBar(count, maxNonCache, TOK_BAR);
|
|
21421
|
-
console.log(" " + import_chalk13.default.white(
|
|
21431
|
+
console.log(" " + import_chalk13.default.white(label2.padEnd(TOK_LABEL)) + b + " " + colored);
|
|
21422
21432
|
}
|
|
21423
21433
|
if (costCacheRead > 0) {
|
|
21424
21434
|
const cacheBar = colorBar(costCacheRead, costCacheRead, TOK_BAR);
|
|
@@ -21449,10 +21459,10 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
|
|
|
21449
21459
|
const MODEL_LABEL = 22;
|
|
21450
21460
|
const MODEL_BAR = Math.max(6, Math.min(20, W - MODEL_LABEL - 12));
|
|
21451
21461
|
for (const [model, cost] of modelList) {
|
|
21452
|
-
const
|
|
21462
|
+
const label2 = model.length > MODEL_LABEL - 1 ? model.slice(0, MODEL_LABEL - 2) + "\u2026" : model;
|
|
21453
21463
|
const b = colorBar(cost, maxModelCost, MODEL_BAR);
|
|
21454
21464
|
console.log(
|
|
21455
|
-
" " + import_chalk13.default.white(
|
|
21465
|
+
" " + import_chalk13.default.white(label2.padEnd(MODEL_LABEL)) + b + " " + import_chalk13.default.yellow(fmtCost2(cost))
|
|
21456
21466
|
);
|
|
21457
21467
|
}
|
|
21458
21468
|
}
|
|
@@ -21577,8 +21587,8 @@ var import_path43 = __toESM(require("path"));
|
|
|
21577
21587
|
var import_os38 = __toESM(require("os"));
|
|
21578
21588
|
init_core();
|
|
21579
21589
|
init_daemon();
|
|
21580
|
-
function printAgentSection(
|
|
21581
|
-
console.log(import_chalk15.default.bold(` ${
|
|
21590
|
+
function printAgentSection(label2, hookPairs, wrapped) {
|
|
21591
|
+
console.log(import_chalk15.default.bold(` ${label2}`));
|
|
21582
21592
|
for (const { name, present } of hookPairs) {
|
|
21583
21593
|
if (present) {
|
|
21584
21594
|
console.log(import_chalk15.default.green(` \u2713 ${name}`));
|
|
@@ -21770,32 +21780,32 @@ function registerInitCommand(program2) {
|
|
|
21770
21780
|
}
|
|
21771
21781
|
console.log("");
|
|
21772
21782
|
}
|
|
21773
|
-
const
|
|
21774
|
-
const isFirstInstall = !import_fs43.default.existsSync(
|
|
21775
|
-
if (import_fs43.default.existsSync(
|
|
21783
|
+
const configPath2 = import_path44.default.join(import_os39.default.homedir(), ".node9", "config.json");
|
|
21784
|
+
const isFirstInstall = !import_fs43.default.existsSync(configPath2);
|
|
21785
|
+
if (import_fs43.default.existsSync(configPath2) && !options.force) {
|
|
21776
21786
|
try {
|
|
21777
|
-
const existing = JSON.parse(import_fs43.default.readFileSync(
|
|
21787
|
+
const existing = JSON.parse(import_fs43.default.readFileSync(configPath2, "utf-8"));
|
|
21778
21788
|
const settings = existing.settings ?? {};
|
|
21779
21789
|
if (settings.mode !== chosenMode) {
|
|
21780
21790
|
settings.mode = chosenMode;
|
|
21781
21791
|
existing.settings = settings;
|
|
21782
|
-
import_fs43.default.writeFileSync(
|
|
21792
|
+
import_fs43.default.writeFileSync(configPath2, JSON.stringify(existing, null, 2) + "\n");
|
|
21783
21793
|
console.log(import_chalk16.default.green(`\u2705 Mode updated: ${chosenMode}`));
|
|
21784
21794
|
} else {
|
|
21785
|
-
console.log(import_chalk16.default.blue(`\u2139\uFE0F Config already exists: ${
|
|
21795
|
+
console.log(import_chalk16.default.blue(`\u2139\uFE0F Config already exists: ${configPath2}`));
|
|
21786
21796
|
}
|
|
21787
21797
|
} catch {
|
|
21788
|
-
console.log(import_chalk16.default.blue(`\u2139\uFE0F Config already exists: ${
|
|
21798
|
+
console.log(import_chalk16.default.blue(`\u2139\uFE0F Config already exists: ${configPath2}`));
|
|
21789
21799
|
}
|
|
21790
21800
|
} else {
|
|
21791
21801
|
const configToSave = {
|
|
21792
21802
|
...DEFAULT_CONFIG,
|
|
21793
21803
|
settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
|
|
21794
21804
|
};
|
|
21795
|
-
const dir = import_path44.default.dirname(
|
|
21805
|
+
const dir = import_path44.default.dirname(configPath2);
|
|
21796
21806
|
if (!import_fs43.default.existsSync(dir)) import_fs43.default.mkdirSync(dir, { recursive: true });
|
|
21797
|
-
import_fs43.default.writeFileSync(
|
|
21798
|
-
console.log(import_chalk16.default.green(`\u2705 Config created: ${
|
|
21807
|
+
import_fs43.default.writeFileSync(configPath2, JSON.stringify(configToSave, null, 2) + "\n");
|
|
21808
|
+
console.log(import_chalk16.default.green(`\u2705 Config created: ${configPath2}`));
|
|
21799
21809
|
console.log(import_chalk16.default.gray(` Mode: ${chosenMode}`));
|
|
21800
21810
|
}
|
|
21801
21811
|
if (options.skipSetup) return;
|
|
@@ -22106,13 +22116,13 @@ function registerUndoCommand(program2) {
|
|
|
22106
22116
|
const e = display[i];
|
|
22107
22117
|
const isGap = prevTs !== null && prevTs - e.timestamp > 6e4;
|
|
22108
22118
|
if (isGap) console.log(import_chalk18.default.gray(" \u2500\u2500 earlier \u2500\u2500"));
|
|
22109
|
-
const
|
|
22119
|
+
const label2 = (e.argsSummary || e.files?.[0] || "\u2014").slice(0, 30).padEnd(30);
|
|
22110
22120
|
const tool = e.tool.slice(0, 8).padEnd(8);
|
|
22111
22121
|
const when = formatAge2(e.timestamp).padEnd(10);
|
|
22112
22122
|
const dir = e.cwd.length > 30 ? "\u2026" + e.cwd.slice(-29) : e.cwd;
|
|
22113
22123
|
console.log(
|
|
22114
22124
|
import_chalk18.default.white(
|
|
22115
|
-
` ${String(i + 1).padEnd(3)} ${
|
|
22125
|
+
` ${String(i + 1).padEnd(3)} ${label2} ${import_chalk18.default.cyan(tool)} ${import_chalk18.default.gray(when)} ${import_chalk18.default.gray(dir)}`
|
|
22116
22126
|
)
|
|
22117
22127
|
);
|
|
22118
22128
|
prevTs = e.timestamp;
|
|
@@ -23527,11 +23537,11 @@ function registerMcpPinCommand(program2) {
|
|
|
23527
23537
|
`);
|
|
23528
23538
|
process.exit(1);
|
|
23529
23539
|
}
|
|
23530
|
-
const
|
|
23540
|
+
const label2 = pins.servers[serverKey].label;
|
|
23531
23541
|
removePin(serverKey);
|
|
23532
23542
|
console.log(import_chalk21.default.green(`
|
|
23533
23543
|
\u{1F513} Pin removed for ${import_chalk21.default.cyan(serverKey)}`));
|
|
23534
|
-
console.log(import_chalk21.default.gray(` Server: ${
|
|
23544
|
+
console.log(import_chalk21.default.gray(` Server: ${label2}`));
|
|
23535
23545
|
console.log(import_chalk21.default.gray(" Next connection will re-pin with current tool definitions.\n"));
|
|
23536
23546
|
});
|
|
23537
23547
|
pinSubCmd.command("reset").description("Clear all MCP pins (next connection to each server will re-pin)").action(() => {
|
|
@@ -23743,11 +23753,1131 @@ function registerAgentsCommand(program2) {
|
|
|
23743
23753
|
// src/cli.ts
|
|
23744
23754
|
init_scan();
|
|
23745
23755
|
|
|
23746
|
-
// src/cli/commands/
|
|
23747
|
-
var
|
|
23756
|
+
// src/cli/commands/posture.ts
|
|
23757
|
+
var import_chalk25 = __toESM(require("chalk"));
|
|
23758
|
+
|
|
23759
|
+
// src/posture/index.ts
|
|
23760
|
+
var import_os44 = __toESM(require("os"));
|
|
23761
|
+
|
|
23762
|
+
// src/posture/secrets.ts
|
|
23748
23763
|
var import_fs46 = __toESM(require("fs"));
|
|
23749
23764
|
var import_path47 = __toESM(require("path"));
|
|
23750
23765
|
var import_os41 = __toESM(require("os"));
|
|
23766
|
+
init_dist();
|
|
23767
|
+
var MAX_FILE_BYTES = 256 * 1024;
|
|
23768
|
+
function displayPath(p, home) {
|
|
23769
|
+
if (p === home) return "~";
|
|
23770
|
+
const prefix = home.endsWith(import_path47.default.sep) ? home : home + import_path47.default.sep;
|
|
23771
|
+
if (p.startsWith(prefix)) return "~" + import_path47.default.sep + p.slice(prefix.length);
|
|
23772
|
+
return p;
|
|
23773
|
+
}
|
|
23774
|
+
function safeRead(file) {
|
|
23775
|
+
try {
|
|
23776
|
+
const stat = import_fs46.default.statSync(file);
|
|
23777
|
+
if (!stat.isFile() || stat.size === 0 || stat.size > MAX_FILE_BYTES) return null;
|
|
23778
|
+
return import_fs46.default.readFileSync(file, "utf8");
|
|
23779
|
+
} catch {
|
|
23780
|
+
return null;
|
|
23781
|
+
}
|
|
23782
|
+
}
|
|
23783
|
+
function candidateFiles(home, cwd) {
|
|
23784
|
+
const files = /* @__PURE__ */ new Set();
|
|
23785
|
+
try {
|
|
23786
|
+
for (const name of import_fs46.default.readdirSync(cwd)) {
|
|
23787
|
+
if (name === ".env" || name.startsWith(".env.")) files.add(import_path47.default.join(cwd, name));
|
|
23788
|
+
}
|
|
23789
|
+
} catch {
|
|
23790
|
+
}
|
|
23791
|
+
for (const spec of AGENT_SPECS) {
|
|
23792
|
+
if (spec.hookFile) files.add(spec.hookFile(home));
|
|
23793
|
+
if (spec.mcpFile) files.add(spec.mcpFile(home));
|
|
23794
|
+
}
|
|
23795
|
+
files.add(import_path47.default.join(home, ".env"));
|
|
23796
|
+
return [...files];
|
|
23797
|
+
}
|
|
23798
|
+
function credentialMaterial(home) {
|
|
23799
|
+
return [
|
|
23800
|
+
import_path47.default.join(home, ".ssh", "id_rsa"),
|
|
23801
|
+
import_path47.default.join(home, ".ssh", "id_dsa"),
|
|
23802
|
+
import_path47.default.join(home, ".ssh", "id_ecdsa"),
|
|
23803
|
+
import_path47.default.join(home, ".ssh", "id_ed25519"),
|
|
23804
|
+
import_path47.default.join(home, ".aws", "credentials"),
|
|
23805
|
+
import_path47.default.join(home, ".config", "gcloud", "application_default_credentials.json")
|
|
23806
|
+
];
|
|
23807
|
+
}
|
|
23808
|
+
function checkSecrets(ctx) {
|
|
23809
|
+
const home = ctx.home || import_os41.default.homedir();
|
|
23810
|
+
const findings = [];
|
|
23811
|
+
const plaintext = [];
|
|
23812
|
+
const plaintextPaths = [];
|
|
23813
|
+
for (const file of candidateFiles(home, ctx.cwd)) {
|
|
23814
|
+
const text = safeRead(file);
|
|
23815
|
+
if (!text) continue;
|
|
23816
|
+
const match = scanText(text);
|
|
23817
|
+
if (match) {
|
|
23818
|
+
plaintext.push(`${match.patternName} in ${displayPath(file, home)}`);
|
|
23819
|
+
plaintextPaths.push(file);
|
|
23820
|
+
}
|
|
23821
|
+
}
|
|
23822
|
+
if (plaintext.length > 0) {
|
|
23823
|
+
findings.push({
|
|
23824
|
+
category: "Secrets",
|
|
23825
|
+
severity: "critical",
|
|
23826
|
+
title: `${plaintext.length} plaintext secret${plaintext.length === 1 ? "" : "s"} on disk`,
|
|
23827
|
+
what: "API keys/tokens are sitting unencrypted in files on disk.",
|
|
23828
|
+
why: "They were saved in plaintext config / .env files.",
|
|
23829
|
+
who: "A tricked agent (or any program you run) could read and leak them.",
|
|
23830
|
+
detail: plaintext,
|
|
23831
|
+
fix: "Fix it now: run `node9 shield enable project-jail` (blocks credential-file reads in-path).",
|
|
23832
|
+
// Coverage is decided at the DLP layer — does node9 block the agent
|
|
23833
|
+
// reading these? (See enforcement.ts.)
|
|
23834
|
+
owner: "node9",
|
|
23835
|
+
coverageProbe: { kind: "fileRead", paths: plaintextPaths }
|
|
23836
|
+
});
|
|
23837
|
+
}
|
|
23838
|
+
const creds = [];
|
|
23839
|
+
const credPaths = [];
|
|
23840
|
+
for (const file of credentialMaterial(home)) {
|
|
23841
|
+
try {
|
|
23842
|
+
if (import_fs46.default.statSync(file).isFile()) {
|
|
23843
|
+
creds.push(displayPath(file, home));
|
|
23844
|
+
credPaths.push(file);
|
|
23845
|
+
}
|
|
23846
|
+
} catch {
|
|
23847
|
+
}
|
|
23848
|
+
}
|
|
23849
|
+
if (creds.length > 0) {
|
|
23850
|
+
findings.push({
|
|
23851
|
+
category: "Secrets",
|
|
23852
|
+
severity: "high",
|
|
23853
|
+
title: `${creds.length} credential file${creds.length === 1 ? "" : "s"} readable by the agent`,
|
|
23854
|
+
what: "Your SSH keys / cloud login files can be read by programs you run.",
|
|
23855
|
+
why: "They sit unlocked in your home folder.",
|
|
23856
|
+
who: "An unsandboxed agent could read them and use them to reach your servers / cloud.",
|
|
23857
|
+
detail: creds,
|
|
23858
|
+
fix: "Fix it now: run `node9 shield enable project-jail` (blocks ~/.ssh, ~/.aws, .env reads in-path).",
|
|
23859
|
+
owner: "node9",
|
|
23860
|
+
coverageProbe: { kind: "fileRead", paths: credPaths }
|
|
23861
|
+
});
|
|
23862
|
+
}
|
|
23863
|
+
return findings;
|
|
23864
|
+
}
|
|
23865
|
+
|
|
23866
|
+
// src/posture/egress.ts
|
|
23867
|
+
init_config();
|
|
23868
|
+
function evaluateEgressConfig(egress) {
|
|
23869
|
+
if (egress.enabled && egress.mode === "block") {
|
|
23870
|
+
return {
|
|
23871
|
+
category: "Egress",
|
|
23872
|
+
severity: "high",
|
|
23873
|
+
title: "Egress is locked, but node9 is not enforcing it",
|
|
23874
|
+
what: "Egress is set to block, but node9 is not applying the policy.",
|
|
23875
|
+
why: "node9 isn't wired in (or is in observe mode), so the lock has no effect.",
|
|
23876
|
+
who: "The lock protects nothing until node9 is enforcing in-path.",
|
|
23877
|
+
owner: "node9",
|
|
23878
|
+
detail: [],
|
|
23879
|
+
fix: "Run `node9 init` and ensure node9 is in enforcing mode.",
|
|
23880
|
+
coverageProbe: { kind: "egress" },
|
|
23881
|
+
// Open here means only "node9 isn't enforcing" — Coverage already says
|
|
23882
|
+
// that, so drop this row when open to avoid double-surfacing.
|
|
23883
|
+
redundantWhenOpen: true
|
|
23884
|
+
};
|
|
23885
|
+
}
|
|
23886
|
+
if (egress.enabled && egress.mode === "review") {
|
|
23887
|
+
return {
|
|
23888
|
+
category: "Egress",
|
|
23889
|
+
severity: "medium",
|
|
23890
|
+
title: "Egress is in review, but node9 is not enforcing it",
|
|
23891
|
+
what: "Egress is set to review (approval-gate), but node9 is not applying the policy.",
|
|
23892
|
+
why: "node9 isn't wired in (or is in observe mode), so the gate has no effect.",
|
|
23893
|
+
who: "Nothing gates outbound until node9 is enforcing in-path.",
|
|
23894
|
+
owner: "node9",
|
|
23895
|
+
detail: [],
|
|
23896
|
+
fix: "Run `node9 init` and ensure node9 is in enforcing mode.",
|
|
23897
|
+
coverageProbe: { kind: "egress" },
|
|
23898
|
+
// Open here means only "node9 isn't enforcing" — Coverage already says
|
|
23899
|
+
// that, so drop this row when open to avoid double-surfacing.
|
|
23900
|
+
redundantWhenOpen: true
|
|
23901
|
+
};
|
|
23902
|
+
}
|
|
23903
|
+
return {
|
|
23904
|
+
category: "Egress",
|
|
23905
|
+
severity: "high",
|
|
23906
|
+
title: "Egress is open",
|
|
23907
|
+
what: "Your agent can connect to any server on the internet.",
|
|
23908
|
+
why: "node9 isn't restricting where its network tools (curl, wget, ssh) can reach.",
|
|
23909
|
+
who: "If the agent is ever tricked, nothing stops it sending your data out.",
|
|
23910
|
+
owner: "node9",
|
|
23911
|
+
detail: [],
|
|
23912
|
+
fix: "Fix it now: run `node9 egress watch` (or `node9 egress lock` to hard-block).",
|
|
23913
|
+
coverageProbe: { kind: "egress" }
|
|
23914
|
+
};
|
|
23915
|
+
}
|
|
23916
|
+
function checkEgress(ctx) {
|
|
23917
|
+
const config = getConfig(ctx.cwd);
|
|
23918
|
+
const egress = config.policy.egress;
|
|
23919
|
+
return [evaluateEgressConfig({ enabled: egress.enabled, mode: egress.mode })];
|
|
23920
|
+
}
|
|
23921
|
+
|
|
23922
|
+
// src/posture/gate.ts
|
|
23923
|
+
init_policy();
|
|
23924
|
+
var BASELINE = ["rm", "-rf", "/"].join(" ");
|
|
23925
|
+
async function checkGate(ctx) {
|
|
23926
|
+
const verdict = await evaluatePolicy2("Bash", { command: BASELINE }, ctx.agent, ctx.cwd);
|
|
23927
|
+
if (verdict.decision !== "block") {
|
|
23928
|
+
return [
|
|
23929
|
+
{
|
|
23930
|
+
category: "Approval gate",
|
|
23931
|
+
severity: "critical",
|
|
23932
|
+
title: "No approval gate is active \u2014 destructive commands run unchecked",
|
|
23933
|
+
what: "Dangerous shell commands aren't gated \u2014 even `rm -rf /` would run.",
|
|
23934
|
+
why: "No enforcing shield or smart rule is gating Bash.",
|
|
23935
|
+
who: "A confused or tricked agent could damage the machine with one command.",
|
|
23936
|
+
detail: [],
|
|
23937
|
+
owner: "node9",
|
|
23938
|
+
fix: "Turn on the gate: run `node9 shield enable bash-safe` (or add a smart rule). node9 then blocks dangerous commands and the negotiation loop tells the agent what's allowed."
|
|
23939
|
+
}
|
|
23940
|
+
];
|
|
23941
|
+
}
|
|
23942
|
+
return [
|
|
23943
|
+
{
|
|
23944
|
+
category: "Approval gate",
|
|
23945
|
+
severity: "advisory",
|
|
23946
|
+
title: "node9 is your approval gate \u2014 destructive commands are blocked",
|
|
23947
|
+
what: "Dangerous shell commands are blocked in-path by your shields and smart rules; when node9 blocks, the negotiation loop tells the agent what is allowed.",
|
|
23948
|
+
detail: [],
|
|
23949
|
+
owner: "node9",
|
|
23950
|
+
coverageProbe: { kind: "command", command: BASELINE },
|
|
23951
|
+
redundantWhenOpen: true
|
|
23952
|
+
}
|
|
23953
|
+
];
|
|
23954
|
+
}
|
|
23955
|
+
|
|
23956
|
+
// src/posture/supply-chain.ts
|
|
23957
|
+
var import_fs47 = __toESM(require("fs"));
|
|
23958
|
+
var import_os42 = __toESM(require("os"));
|
|
23959
|
+
var import_path48 = __toESM(require("path"));
|
|
23960
|
+
var import_smol_toml3 = require("smol-toml");
|
|
23961
|
+
init_provenance();
|
|
23962
|
+
var PACKAGE_RUNNERS = /* @__PURE__ */ new Set(["npx", "pnpx", "bunx", "dlx", "yarn", "pnpm", "bun"]);
|
|
23963
|
+
function isNode9Managed(command, args = []) {
|
|
23964
|
+
if (!command) return false;
|
|
23965
|
+
if (import_path48.default.basename(command).toLowerCase() === "node9") return true;
|
|
23966
|
+
if (PACKAGE_RUNNERS.has(import_path48.default.basename(command).toLowerCase())) {
|
|
23967
|
+
return args.some((a) => a === "node9" || import_path48.default.basename(a).toLowerCase() === "node9");
|
|
23968
|
+
}
|
|
23969
|
+
return false;
|
|
23970
|
+
}
|
|
23971
|
+
var MAX_CONFIG_BYTES = 10 * 1024 * 1024;
|
|
23972
|
+
function readServers(file, format, agent) {
|
|
23973
|
+
try {
|
|
23974
|
+
const stat = import_fs47.default.statSync(file);
|
|
23975
|
+
if (!stat.isFile() || stat.size > MAX_CONFIG_BYTES) return [];
|
|
23976
|
+
const text = import_fs47.default.readFileSync(file, "utf8");
|
|
23977
|
+
const map = format === "toml" ? (0, import_smol_toml3.parse)(text)?.mcp_servers : JSON.parse(text)?.mcpServers;
|
|
23978
|
+
if (!map || typeof map !== "object") return [];
|
|
23979
|
+
return Object.entries(map).map(([name, v]) => ({
|
|
23980
|
+
name,
|
|
23981
|
+
command: v?.command,
|
|
23982
|
+
args: Array.isArray(v?.args) ? v.args : void 0,
|
|
23983
|
+
agent
|
|
23984
|
+
}));
|
|
23985
|
+
} catch {
|
|
23986
|
+
return [];
|
|
23987
|
+
}
|
|
23988
|
+
}
|
|
23989
|
+
function checkSupplyChain(ctx) {
|
|
23990
|
+
const home = ctx.home || import_os42.default.homedir();
|
|
23991
|
+
const servers = [];
|
|
23992
|
+
for (const spec of AGENT_SPECS) {
|
|
23993
|
+
if (!spec.mcpFile) continue;
|
|
23994
|
+
servers.push(...readServers(spec.mcpFile(home), spec.mcpFormat ?? "json", spec.label));
|
|
23995
|
+
}
|
|
23996
|
+
if (servers.length === 0) return [];
|
|
23997
|
+
const findings = [];
|
|
23998
|
+
const unmanaged = servers.filter((s) => s.command && !isNode9Managed(s.command, s.args));
|
|
23999
|
+
const suspect = unmanaged.filter(
|
|
24000
|
+
(s) => checkProvenance(s.command, ctx.cwd).trustLevel === "suspect"
|
|
24001
|
+
);
|
|
24002
|
+
if (suspect.length > 0) {
|
|
24003
|
+
findings.push({
|
|
24004
|
+
category: "Supply chain",
|
|
24005
|
+
severity: "high",
|
|
24006
|
+
title: `${suspect.length} MCP server${suspect.length === 1 ? "" : "s"} launched from an untrusted path`,
|
|
24007
|
+
what: "An MCP tool-server runs from an untrusted location.",
|
|
24008
|
+
why: "Its binary lives in /tmp or a world-writable directory.",
|
|
24009
|
+
who: "Anything on the machine could swap that binary for malware the agent then runs.",
|
|
24010
|
+
detail: suspect.map((s) => `${s.name} \u2192 ${s.command} (${s.agent})`),
|
|
24011
|
+
owner: "node9",
|
|
24012
|
+
fix: "node9 can pin + provenance-check MCP servers before they run."
|
|
24013
|
+
});
|
|
24014
|
+
}
|
|
24015
|
+
if (unmanaged.length > 0) {
|
|
24016
|
+
findings.push({
|
|
24017
|
+
category: "Supply chain",
|
|
24018
|
+
severity: "medium",
|
|
24019
|
+
title: `${unmanaged.length} of ${servers.length} MCP server${servers.length === 1 ? "" : "s"} run outside node9`,
|
|
24020
|
+
what: "Some MCP tool-servers run without node9 watching their tool calls.",
|
|
24021
|
+
why: "They're launched directly, not wrapped by node9.",
|
|
24022
|
+
who: "A poisoned or silently-updated server could act freely (tool-poisoning / rug-pull).",
|
|
24023
|
+
detail: unmanaged.slice(0, 5).map((s) => `${s.name} (${s.agent})`),
|
|
24024
|
+
owner: "node9",
|
|
24025
|
+
fix: "node9 can wrap MCP servers so every tool call is gated + pinned."
|
|
24026
|
+
});
|
|
24027
|
+
}
|
|
24028
|
+
return findings;
|
|
24029
|
+
}
|
|
24030
|
+
|
|
24031
|
+
// src/posture/privilege.ts
|
|
24032
|
+
init_policy();
|
|
24033
|
+
var SUDO_PROBE = "sudo chmod 777 /etc/passwd";
|
|
24034
|
+
async function checkPrivilege(ctx) {
|
|
24035
|
+
const findings = [];
|
|
24036
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
24037
|
+
const isRoot = uid === 0;
|
|
24038
|
+
if (isRoot) {
|
|
24039
|
+
findings.push({
|
|
24040
|
+
category: "Privilege",
|
|
24041
|
+
severity: "high",
|
|
24042
|
+
title: "Running as root",
|
|
24043
|
+
what: "The agent process is running as root (full system rights).",
|
|
24044
|
+
why: "It was started as uid 0.",
|
|
24045
|
+
who: "One bad command can change any file, user, or service on the machine.",
|
|
24046
|
+
detail: [],
|
|
24047
|
+
owner: "node9",
|
|
24048
|
+
fix: "node9 can block privileged commands (sudo, system-path writes) in-path."
|
|
24049
|
+
});
|
|
24050
|
+
}
|
|
24051
|
+
const verdict = await evaluatePolicy2("Bash", { command: SUDO_PROBE }, ctx.agent, ctx.cwd);
|
|
24052
|
+
if (verdict.decision !== "block") {
|
|
24053
|
+
findings.push({
|
|
24054
|
+
category: "Privilege",
|
|
24055
|
+
severity: isRoot ? "high" : "medium",
|
|
24056
|
+
title: "Privilege escalation is not gated",
|
|
24057
|
+
what: "node9 isn't gating `sudo`.",
|
|
24058
|
+
why: "No sudo rule is active in the current policy.",
|
|
24059
|
+
// Calibrated: don't claim the agent CAN become root — it depends on sudo config.
|
|
24060
|
+
who: "If `sudo` is passwordless (NOPASSWD), an agent could become root; with a password prompt the risk is lower.",
|
|
24061
|
+
detail: [],
|
|
24062
|
+
fix: "node9 can gate sudo / privilege-escalation in-path.",
|
|
24063
|
+
// Coverage probes the real policy: block OR review = gated (covered).
|
|
24064
|
+
owner: "node9",
|
|
24065
|
+
coverageProbe: { kind: "command", command: SUDO_PROBE }
|
|
24066
|
+
});
|
|
24067
|
+
}
|
|
24068
|
+
return findings;
|
|
24069
|
+
}
|
|
24070
|
+
|
|
24071
|
+
// src/posture/containment.ts
|
|
24072
|
+
var import_fs48 = __toESM(require("fs"));
|
|
24073
|
+
function inContainer() {
|
|
24074
|
+
if (import_fs48.default.existsSync("/.dockerenv") || import_fs48.default.existsSync("/run/.containerenv")) return true;
|
|
24075
|
+
try {
|
|
24076
|
+
const cgroup = import_fs48.default.readFileSync("/proc/1/cgroup", "utf8");
|
|
24077
|
+
if (/docker|kubepods|containerd|lxc|libpod/.test(cgroup)) return true;
|
|
24078
|
+
} catch {
|
|
24079
|
+
}
|
|
24080
|
+
return false;
|
|
24081
|
+
}
|
|
24082
|
+
function checkContainment(_ctx) {
|
|
24083
|
+
if (inContainer()) return [];
|
|
24084
|
+
return [
|
|
24085
|
+
{
|
|
24086
|
+
category: "Isolation",
|
|
24087
|
+
severity: "advisory",
|
|
24088
|
+
title: "Running directly on the host \u2014 no container",
|
|
24089
|
+
what: "The agent runs loose on your whole machine, not in a sandbox.",
|
|
24090
|
+
why: "It's started on the bare host, not inside a container or VM.",
|
|
24091
|
+
who: "If it gets tricked, the damage reaches every file and program \u2014 not one room.",
|
|
24092
|
+
detail: [],
|
|
24093
|
+
owner: "os",
|
|
24094
|
+
node9Reduces: true,
|
|
24095
|
+
fix: "node9 can shrink the blast radius without a container \u2014 you keep every tool:\n \u2022 node9 shield enable project-jail \u2014 block credential reads\n \u2022 node9 egress lock \u2014 block data exfil\nA container/VM adds full isolation, but you lose host access.",
|
|
24096
|
+
coverageProbe: { kind: "cantFix" }
|
|
24097
|
+
}
|
|
24098
|
+
];
|
|
24099
|
+
}
|
|
24100
|
+
|
|
24101
|
+
// src/posture/inbound.ts
|
|
24102
|
+
var import_fs49 = __toESM(require("fs"));
|
|
24103
|
+
var KNOWN_SERVICE_PORTS = {
|
|
24104
|
+
5432: "PostgreSQL",
|
|
24105
|
+
6379: "Redis",
|
|
24106
|
+
3306: "MySQL/MariaDB",
|
|
24107
|
+
27017: "MongoDB",
|
|
24108
|
+
9200: "Elasticsearch",
|
|
24109
|
+
11211: "Memcached",
|
|
24110
|
+
5672: "RabbitMQ",
|
|
24111
|
+
9092: "Kafka",
|
|
24112
|
+
2379: "etcd",
|
|
24113
|
+
8086: "InfluxDB"
|
|
24114
|
+
};
|
|
24115
|
+
var KNOWN_SERVICE_COMMS = {
|
|
24116
|
+
postgres: "PostgreSQL",
|
|
24117
|
+
"redis-server": "Redis",
|
|
24118
|
+
mysqld: "MySQL",
|
|
24119
|
+
mariadbd: "MariaDB",
|
|
24120
|
+
mongod: "MongoDB"
|
|
24121
|
+
};
|
|
24122
|
+
var DB_LABEL = /PostgreSQL|Redis|MySQL|MariaDB|MongoDB/;
|
|
24123
|
+
var SHIELD_FOR_SERVICE = {
|
|
24124
|
+
PostgreSQL: {
|
|
24125
|
+
shield: "postgres",
|
|
24126
|
+
blocks: "DROP TABLE / TRUNCATE",
|
|
24127
|
+
rebind: "PostgreSQL \u2192 listen_addresses='localhost'"
|
|
24128
|
+
},
|
|
24129
|
+
Redis: { shield: "redis", blocks: "FLUSHALL / FLUSHDB", rebind: "Redis \u2192 bind 127.0.0.1" }
|
|
24130
|
+
};
|
|
24131
|
+
function buildNetworkFix(labels) {
|
|
24132
|
+
const shielded = [
|
|
24133
|
+
...new Map(
|
|
24134
|
+
labels.map((label2) => {
|
|
24135
|
+
const key = Object.keys(SHIELD_FOR_SERVICE).find((k) => label2.includes(k));
|
|
24136
|
+
return key ? SHIELD_FOR_SERVICE[key] : null;
|
|
24137
|
+
}).filter((s) => s !== null).map((s) => [s.shield, s])
|
|
24138
|
+
).values()
|
|
24139
|
+
];
|
|
24140
|
+
if (shielded.length === 0) {
|
|
24141
|
+
return {
|
|
24142
|
+
fix: "Bind to 127.0.0.1 or firewall the port; node9 gates the agent, not the socket.",
|
|
24143
|
+
reduces: false
|
|
24144
|
+
};
|
|
24145
|
+
}
|
|
24146
|
+
const protectLines = shielded.map((s) => ` \u2022 node9 shield enable ${s.shield} \u2014 blocks ${s.blocks}`).join("\n");
|
|
24147
|
+
const rebindLines = shielded.map((s) => ` \u2022 ${s.rebind}`).join("\n");
|
|
24148
|
+
return {
|
|
24149
|
+
fix: "Protect the agent now \u2014 node9 blocks destructive DB ops:\n" + protectLines + "\nClose the port to other machines (your part):\n" + rebindLines + "\n \u2022 or firewall the port",
|
|
24150
|
+
reduces: true
|
|
24151
|
+
};
|
|
24152
|
+
}
|
|
24153
|
+
function parseListeners(procText) {
|
|
24154
|
+
const out = [];
|
|
24155
|
+
for (const line of procText.split("\n").slice(1)) {
|
|
24156
|
+
const cols = line.trim().split(/\s+/);
|
|
24157
|
+
if (cols.length < 10) continue;
|
|
24158
|
+
if (cols[3] !== "0A") continue;
|
|
24159
|
+
const local = cols[1];
|
|
24160
|
+
const sep = local.lastIndexOf(":");
|
|
24161
|
+
if (sep < 0) continue;
|
|
24162
|
+
const addrHex = local.slice(0, sep);
|
|
24163
|
+
const port = parseInt(local.slice(sep + 1), 16);
|
|
24164
|
+
if (!/^0+$/.test(addrHex) || !Number.isFinite(port)) continue;
|
|
24165
|
+
out.push({ port, inode: cols[9] });
|
|
24166
|
+
}
|
|
24167
|
+
return out;
|
|
24168
|
+
}
|
|
24169
|
+
function tiesToAgent(proc, agentName) {
|
|
24170
|
+
const needle = agentName.trim().toLowerCase();
|
|
24171
|
+
if (needle.length < 4) return false;
|
|
24172
|
+
const escaped = needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24173
|
+
const boundary = new RegExp(`(^|[^a-z0-9])${escaped}([^a-z0-9]|$)`);
|
|
24174
|
+
return boundary.test(proc.comm.toLowerCase()) || boundary.test(proc.cmdline.toLowerCase());
|
|
24175
|
+
}
|
|
24176
|
+
function classifyListener(port, proc, agentName) {
|
|
24177
|
+
if (agentName && proc && tiesToAgent(proc, agentName)) {
|
|
24178
|
+
return { kind: "agent", label: `${proc.comm} on :${port}` };
|
|
24179
|
+
}
|
|
24180
|
+
const service = KNOWN_SERVICE_PORTS[port] ?? (proc ? KNOWN_SERVICE_COMMS[proc.comm] : void 0);
|
|
24181
|
+
if (service) return { kind: "service", label: `${service} on :${port}` };
|
|
24182
|
+
return { kind: "unknown", label: `${proc?.comm || "unknown process"} on :${port}` };
|
|
24183
|
+
}
|
|
24184
|
+
function collectListeners() {
|
|
24185
|
+
const byPort = /* @__PURE__ */ new Map();
|
|
24186
|
+
for (const file of ["/proc/net/tcp", "/proc/net/tcp6"]) {
|
|
24187
|
+
try {
|
|
24188
|
+
for (const l of parseListeners(import_fs49.default.readFileSync(file, "utf8"))) {
|
|
24189
|
+
if (!byPort.has(l.port)) byPort.set(l.port, l);
|
|
24190
|
+
}
|
|
24191
|
+
} catch {
|
|
24192
|
+
}
|
|
24193
|
+
}
|
|
24194
|
+
return [...byPort.values()].sort((a, b) => a.port - b.port);
|
|
24195
|
+
}
|
|
24196
|
+
function readProc(pid) {
|
|
24197
|
+
let comm = "unknown";
|
|
24198
|
+
let cmdline = "";
|
|
24199
|
+
try {
|
|
24200
|
+
comm = import_fs49.default.readFileSync(`/proc/${pid}/comm`, "utf8").trim().replace(/-MainThread$/, "") || "unknown";
|
|
24201
|
+
} catch {
|
|
24202
|
+
}
|
|
24203
|
+
try {
|
|
24204
|
+
cmdline = import_fs49.default.readFileSync(`/proc/${pid}/cmdline`).toString().replace(/\0/g, " ").trim();
|
|
24205
|
+
} catch {
|
|
24206
|
+
}
|
|
24207
|
+
return { comm, cmdline };
|
|
24208
|
+
}
|
|
24209
|
+
function resolveProcesses(inodes) {
|
|
24210
|
+
const map = /* @__PURE__ */ new Map();
|
|
24211
|
+
if (inodes.size === 0) return map;
|
|
24212
|
+
let pids;
|
|
24213
|
+
try {
|
|
24214
|
+
pids = import_fs49.default.readdirSync("/proc").filter((d) => /^\d+$/.test(d));
|
|
24215
|
+
} catch {
|
|
24216
|
+
return map;
|
|
24217
|
+
}
|
|
24218
|
+
for (const pid of pids) {
|
|
24219
|
+
let fds;
|
|
24220
|
+
try {
|
|
24221
|
+
fds = import_fs49.default.readdirSync(`/proc/${pid}/fd`);
|
|
24222
|
+
} catch {
|
|
24223
|
+
continue;
|
|
24224
|
+
}
|
|
24225
|
+
for (const fd of fds) {
|
|
24226
|
+
let link;
|
|
24227
|
+
try {
|
|
24228
|
+
link = import_fs49.default.readlinkSync(`/proc/${pid}/fd/${fd}`);
|
|
24229
|
+
} catch {
|
|
24230
|
+
continue;
|
|
24231
|
+
}
|
|
24232
|
+
const m = /^socket:\[(\d+)\]$/.exec(link);
|
|
24233
|
+
if (m && inodes.has(m[1]) && !map.has(m[1])) {
|
|
24234
|
+
map.set(m[1], readProc(pid));
|
|
24235
|
+
}
|
|
24236
|
+
}
|
|
24237
|
+
if (map.size === inodes.size) break;
|
|
24238
|
+
}
|
|
24239
|
+
return map;
|
|
24240
|
+
}
|
|
24241
|
+
function checkInbound(ctx) {
|
|
24242
|
+
const listeners = collectListeners();
|
|
24243
|
+
if (listeners.length === 0) return [];
|
|
24244
|
+
const procByInode = resolveProcesses(new Set(listeners.map((l) => l.inode)));
|
|
24245
|
+
const classified = listeners.map((l) => ({
|
|
24246
|
+
port: l.port,
|
|
24247
|
+
...classifyListener(l.port, procByInode.get(l.inode) ?? null, ctx.agent)
|
|
24248
|
+
}));
|
|
24249
|
+
const findings = [];
|
|
24250
|
+
const agentPorts = classified.filter((c) => c.kind === "agent");
|
|
24251
|
+
if (agentPorts.length > 0) {
|
|
24252
|
+
findings.push({
|
|
24253
|
+
category: "Agent inbound",
|
|
24254
|
+
severity: "advisory",
|
|
24255
|
+
title: `Your agent is reachable on 0.0.0.0 (port${agentPorts.length === 1 ? "" : "s"} ${agentPorts.map((a) => a.port).join(", ")})`,
|
|
24256
|
+
what: "Your agent itself is listening for incoming network connections.",
|
|
24257
|
+
why: "It's bound to 0.0.0.0, so other devices on the network can reach it.",
|
|
24258
|
+
who: "Anyone who can reach the port could send it instructions (pilot it). Confirm it requires an auth token.",
|
|
24259
|
+
detail: agentPorts.map((a) => a.label),
|
|
24260
|
+
owner: "os",
|
|
24261
|
+
fix: "Bind the agent port to 127.0.0.1, or require an auth token on inbound requests.",
|
|
24262
|
+
coverageProbe: { kind: "cantFix" }
|
|
24263
|
+
});
|
|
24264
|
+
}
|
|
24265
|
+
const exposed = classified.filter((c) => c.kind !== "agent");
|
|
24266
|
+
if (exposed.length > 0) {
|
|
24267
|
+
const hasDb = exposed.some((e) => DB_LABEL.test(e.label));
|
|
24268
|
+
const { fix, reduces } = buildNetworkFix(exposed.map((e) => e.label));
|
|
24269
|
+
findings.push({
|
|
24270
|
+
category: "Network exposure",
|
|
24271
|
+
severity: "advisory",
|
|
24272
|
+
title: `${exposed.length} service${exposed.length === 1 ? "" : "s"} reachable on 0.0.0.0`,
|
|
24273
|
+
what: "These services accept connections from your whole network, not just this laptop.",
|
|
24274
|
+
why: "They listen on 0.0.0.0 (all interfaces) instead of 127.0.0.1 (this machine only).",
|
|
24275
|
+
// Calibrated: 0.0.0.0 = your local network (WiFi), not the public internet
|
|
24276
|
+
// unless the box has a public IP.
|
|
24277
|
+
who: "Other devices on your network (e.g. your WiFi) can connect \u2014 usually not the whole internet unless this box has a public IP." + (hasDb ? " An open, unauthenticated database is a direct data-theft path." : ""),
|
|
24278
|
+
detail: exposed.map((e) => e.label),
|
|
24279
|
+
owner: "os",
|
|
24280
|
+
// Only when node9 actually has a shield for an exposed service — otherwise
|
|
24281
|
+
// (bare dev servers) it stays purely the user's to rebind.
|
|
24282
|
+
node9Reduces: reduces,
|
|
24283
|
+
fix,
|
|
24284
|
+
coverageProbe: { kind: "cantFix" }
|
|
24285
|
+
});
|
|
24286
|
+
}
|
|
24287
|
+
return findings;
|
|
24288
|
+
}
|
|
24289
|
+
|
|
24290
|
+
// src/posture/coverage.ts
|
|
24291
|
+
var import_os43 = __toESM(require("os"));
|
|
24292
|
+
init_config();
|
|
24293
|
+
function checkCoverage(ctx) {
|
|
24294
|
+
const home = ctx.home || import_os43.default.homedir();
|
|
24295
|
+
const findings = [];
|
|
24296
|
+
const protectedAgents = getAgentWiring(home).filter((r) => r.isProtected);
|
|
24297
|
+
if (protectedAgents.length === 0) {
|
|
24298
|
+
findings.push({
|
|
24299
|
+
category: "Coverage",
|
|
24300
|
+
severity: "critical",
|
|
24301
|
+
title: "node9 is not in-path for any agent",
|
|
24302
|
+
what: "node9 isn't actually in the loop for any agent on this machine.",
|
|
24303
|
+
why: "No agent has node9 hooks or MCP wired in.",
|
|
24304
|
+
who: "Everything else here is unenforced \u2014 node9 can only report, not block.",
|
|
24305
|
+
detail: [],
|
|
24306
|
+
owner: "node9",
|
|
24307
|
+
fix: "Run `node9 init` to put node9 in-path for your agents."
|
|
24308
|
+
});
|
|
24309
|
+
return findings;
|
|
24310
|
+
}
|
|
24311
|
+
const mode = getConfig(ctx.cwd).settings.mode;
|
|
24312
|
+
if (mode === "observe" || mode === "audit") {
|
|
24313
|
+
findings.push({
|
|
24314
|
+
category: "Coverage",
|
|
24315
|
+
severity: "high",
|
|
24316
|
+
title: `node9 is in ${mode} mode \u2014 watching, not blocking`,
|
|
24317
|
+
what: "node9 is watching but not actually blocking anything.",
|
|
24318
|
+
why: `It's in ${mode} mode, which logs risky actions but lets them through.`,
|
|
24319
|
+
who: "The guardrails above are observed, not enforced.",
|
|
24320
|
+
detail: [],
|
|
24321
|
+
owner: "node9",
|
|
24322
|
+
fix: "Set mode to `standard` (or `strict`) to enforce in-path."
|
|
24323
|
+
});
|
|
24324
|
+
}
|
|
24325
|
+
return findings;
|
|
24326
|
+
}
|
|
24327
|
+
|
|
24328
|
+
// src/posture/score.ts
|
|
24329
|
+
init_dist();
|
|
24330
|
+
function scorePosture(findings, checksRun) {
|
|
24331
|
+
const open = findings.filter(
|
|
24332
|
+
(f) => f.coverage?.state !== "covered" && f.coverage?.state !== "cant-fix"
|
|
24333
|
+
);
|
|
24334
|
+
const count = (sev) => open.filter((f) => f.severity === sev).length;
|
|
24335
|
+
return computeSecurityScore({
|
|
24336
|
+
critical: count("critical"),
|
|
24337
|
+
high: count("high"),
|
|
24338
|
+
medium: count("medium"),
|
|
24339
|
+
// Denominator = number of checks evaluated. With computeSecurityScore's
|
|
24340
|
+
// caps this makes any critical → critical tier, any high → at-risk, and a
|
|
24341
|
+
// fully clean run (0 findings, checksRun > 0) → 100/good.
|
|
24342
|
+
total: Math.max(checksRun, 1)
|
|
24343
|
+
});
|
|
24344
|
+
}
|
|
24345
|
+
|
|
24346
|
+
// src/posture/headline.ts
|
|
24347
|
+
var SEVERITY_RANK = {
|
|
24348
|
+
critical: 0,
|
|
24349
|
+
high: 1,
|
|
24350
|
+
medium: 2,
|
|
24351
|
+
advisory: 3
|
|
24352
|
+
};
|
|
24353
|
+
function worstFinding(findings) {
|
|
24354
|
+
return [...findings].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity])[0];
|
|
24355
|
+
}
|
|
24356
|
+
function deriveHeadline(allFindings) {
|
|
24357
|
+
const findings = allFindings.filter(
|
|
24358
|
+
(f) => f.coverage?.state !== "covered" && f.coverage?.state !== "cant-fix"
|
|
24359
|
+
);
|
|
24360
|
+
if (findings.length === 0 || findings.every((f) => f.severity === "advisory")) return null;
|
|
24361
|
+
const has = (category) => findings.some((f) => f.category === category);
|
|
24362
|
+
const secrets = has("Secrets");
|
|
24363
|
+
const egressOpen = has("Egress");
|
|
24364
|
+
const noIsolation = has("Isolation");
|
|
24365
|
+
const gateWeak = has("Approval gate");
|
|
24366
|
+
const notWired = findings.some((f) => f.category === "Coverage" && f.severity === "critical");
|
|
24367
|
+
const observeOnly = findings.some((f) => f.category === "Coverage" && f.severity === "high");
|
|
24368
|
+
let risk;
|
|
24369
|
+
if (secrets && egressOpen) {
|
|
24370
|
+
risk = "An agent on this host can read the credentials on this box and send them to any host" + (noIsolation ? ", and there is no container around it" : "") + ". One poisoned input \u2014 a malicious file, or a prompt-injection in a page it reads \u2014 is all it takes.";
|
|
24371
|
+
} else if (secrets) {
|
|
24372
|
+
risk = "An agent on this host can read the credentials on this box" + (noIsolation ? " with no sandbox around it" : "") + ". A single poisoned instruction would expose those keys.";
|
|
24373
|
+
} else if (egressOpen && gateWeak) {
|
|
24374
|
+
risk = "An agent here can run unrestricted commands and reach any host \u2014 an open path for a poisoned instruction to exfiltrate data or damage the box.";
|
|
24375
|
+
} else if (egressOpen) {
|
|
24376
|
+
risk = "An agent here can reach any host on the internet \u2014 an open exfiltration path the moment it is compromised.";
|
|
24377
|
+
} else if (gateWeak) {
|
|
24378
|
+
risk = "Destructive commands are not reliably blocked here \u2014 an agent given a bad instruction could damage the box.";
|
|
24379
|
+
} else {
|
|
24380
|
+
risk = worstFinding(findings)?.title ?? "Review the findings below.";
|
|
24381
|
+
}
|
|
24382
|
+
let action;
|
|
24383
|
+
if (notWired) {
|
|
24384
|
+
action = "Run `node9 init` \u2014 node9 is not in-path yet, so nothing here is enforced.";
|
|
24385
|
+
} else if (observeOnly) {
|
|
24386
|
+
action = "Switch node9 to enforcing mode \u2014 right now it is only watching, not blocking.";
|
|
24387
|
+
} else if (egressOpen) {
|
|
24388
|
+
action = "lock egress to an allowlist (node9 can enforce it) \u2014 it closes the exit the exfiltration needs.";
|
|
24389
|
+
} else if (secrets) {
|
|
24390
|
+
action = "node9 can block reads of sensitive paths (~/.ssh, ~/.aws) in-path.";
|
|
24391
|
+
} else if (gateWeak) {
|
|
24392
|
+
action = "node9 can enforce destructive-command blocking in-path.";
|
|
24393
|
+
} else {
|
|
24394
|
+
action = worstFinding(findings)?.fix ?? "Review the findings below.";
|
|
24395
|
+
}
|
|
24396
|
+
return { risk, action };
|
|
24397
|
+
}
|
|
24398
|
+
|
|
24399
|
+
// src/posture/enforcement.ts
|
|
24400
|
+
init_dlp();
|
|
24401
|
+
init_policy();
|
|
24402
|
+
init_config();
|
|
24403
|
+
function egressCoverage(env) {
|
|
24404
|
+
if (env.enforcing && env.egressBlocking) {
|
|
24405
|
+
return { state: "covered", level: "block", via: "node9 egress" };
|
|
24406
|
+
}
|
|
24407
|
+
if (env.enforcing && env.egressReviewing) {
|
|
24408
|
+
return { state: "covered", level: "review", via: "node9 egress" };
|
|
24409
|
+
}
|
|
24410
|
+
return { state: "open" };
|
|
24411
|
+
}
|
|
24412
|
+
function coverageFromVerdict(verdict, env, via) {
|
|
24413
|
+
if (!env.enforcing) return { state: "open" };
|
|
24414
|
+
if (verdict === "block") return { state: "covered", level: "block", via };
|
|
24415
|
+
if (verdict === "review") return { state: "covered", level: "review", via };
|
|
24416
|
+
return { state: "open" };
|
|
24417
|
+
}
|
|
24418
|
+
function viaFromRule(ruleName) {
|
|
24419
|
+
if (!ruleName) return void 0;
|
|
24420
|
+
const m = /^shield:([^:]+):/.exec(ruleName);
|
|
24421
|
+
return m ? `${m[1]} shield` : void 0;
|
|
24422
|
+
}
|
|
24423
|
+
async function annotateCoverage(findings, ctx) {
|
|
24424
|
+
const config = getConfig(ctx.cwd);
|
|
24425
|
+
const mode = config.settings.mode;
|
|
24426
|
+
const wired = getAgentWiring(ctx.home).some((r) => r.isProtected);
|
|
24427
|
+
const env = {
|
|
24428
|
+
enforcing: wired && mode !== "observe" && mode !== "audit",
|
|
24429
|
+
egressBlocking: config.policy.egress.enabled && config.policy.egress.mode === "block",
|
|
24430
|
+
egressReviewing: config.policy.egress.enabled && config.policy.egress.mode === "review"
|
|
24431
|
+
};
|
|
24432
|
+
for (const f of findings) {
|
|
24433
|
+
const probe = f.coverageProbe;
|
|
24434
|
+
if (!probe) continue;
|
|
24435
|
+
if (probe.kind === "cantFix") {
|
|
24436
|
+
f.coverage = { state: "cant-fix" };
|
|
24437
|
+
continue;
|
|
24438
|
+
}
|
|
24439
|
+
if (probe.kind === "egress") {
|
|
24440
|
+
f.coverage = egressCoverage(env);
|
|
24441
|
+
continue;
|
|
24442
|
+
}
|
|
24443
|
+
if (probe.kind === "fileRead") {
|
|
24444
|
+
const verdicts = probe.paths.map((p) => scanFilePath(p)?.severity ?? null);
|
|
24445
|
+
if (verdicts.length === 0 || verdicts.some((v) => v === null)) {
|
|
24446
|
+
f.coverage = coverageFromVerdict("allow", env);
|
|
24447
|
+
} else {
|
|
24448
|
+
const worst = verdicts.some((v) => v === "review") ? "review" : "block";
|
|
24449
|
+
f.coverage = coverageFromVerdict(worst, env, "node9 DLP");
|
|
24450
|
+
}
|
|
24451
|
+
continue;
|
|
24452
|
+
}
|
|
24453
|
+
const verdict = await evaluatePolicy2("Bash", { command: probe.command }, ctx.agent, ctx.cwd);
|
|
24454
|
+
f.coverage = coverageFromVerdict(
|
|
24455
|
+
verdict.decision,
|
|
24456
|
+
env,
|
|
24457
|
+
viaFromRule(verdict.ruleName)
|
|
24458
|
+
);
|
|
24459
|
+
}
|
|
24460
|
+
}
|
|
24461
|
+
|
|
24462
|
+
// src/posture/index.ts
|
|
24463
|
+
var POSTURE_CHECKS = [
|
|
24464
|
+
{ category: "Secrets", run: checkSecrets },
|
|
24465
|
+
{ category: "Egress", run: checkEgress },
|
|
24466
|
+
{ category: "Approval gate", run: checkGate },
|
|
24467
|
+
{ category: "Supply chain", run: checkSupplyChain },
|
|
24468
|
+
{ category: "Privilege", run: checkPrivilege },
|
|
24469
|
+
{ category: "Isolation", run: checkContainment },
|
|
24470
|
+
{ category: "Inbound", run: checkInbound },
|
|
24471
|
+
{ category: "Coverage", run: checkCoverage }
|
|
24472
|
+
];
|
|
24473
|
+
function dropEnforcementRedundant(findings) {
|
|
24474
|
+
const coveragePresent = findings.some((f) => f.category === "Coverage");
|
|
24475
|
+
if (!coveragePresent) return findings;
|
|
24476
|
+
return findings.filter((f) => !(f.redundantWhenOpen && f.coverage?.state === "open"));
|
|
24477
|
+
}
|
|
24478
|
+
async function runChecks(checks, ctx) {
|
|
24479
|
+
const findings = [];
|
|
24480
|
+
const passedCategories = [];
|
|
24481
|
+
const erroredCategories = [];
|
|
24482
|
+
for (const check of checks) {
|
|
24483
|
+
try {
|
|
24484
|
+
const result = await check.run(ctx);
|
|
24485
|
+
if (result.length === 0) passedCategories.push(check.category);
|
|
24486
|
+
else findings.push(...result);
|
|
24487
|
+
} catch (err2) {
|
|
24488
|
+
erroredCategories.push(check.category);
|
|
24489
|
+
if (process.env.NODE9_DEBUG) {
|
|
24490
|
+
console.error(`[posture] check "${check.category}" failed:`, err2?.message);
|
|
24491
|
+
}
|
|
24492
|
+
}
|
|
24493
|
+
}
|
|
24494
|
+
return { findings, passedCategories, erroredCategories };
|
|
24495
|
+
}
|
|
24496
|
+
async function runPosture(opts = {}) {
|
|
24497
|
+
const ctx = {
|
|
24498
|
+
home: opts.home ?? import_os44.default.homedir(),
|
|
24499
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
24500
|
+
agent: opts.agent
|
|
24501
|
+
};
|
|
24502
|
+
const {
|
|
24503
|
+
findings: rawFindings,
|
|
24504
|
+
passedCategories,
|
|
24505
|
+
erroredCategories
|
|
24506
|
+
} = await runChecks(POSTURE_CHECKS, ctx);
|
|
24507
|
+
await annotateCoverage(rawFindings, ctx);
|
|
24508
|
+
const findings = dropEnforcementRedundant(rawFindings);
|
|
24509
|
+
const { score, tier } = scorePosture(findings, POSTURE_CHECKS.length);
|
|
24510
|
+
return {
|
|
24511
|
+
agent: opts.agent ? `${opts.agent} on this host` : "agent on this host",
|
|
24512
|
+
findings,
|
|
24513
|
+
passedCategories,
|
|
24514
|
+
erroredCategories,
|
|
24515
|
+
headline: deriveHeadline(findings),
|
|
24516
|
+
score,
|
|
24517
|
+
tier,
|
|
24518
|
+
checksRun: POSTURE_CHECKS.length
|
|
24519
|
+
};
|
|
24520
|
+
}
|
|
24521
|
+
|
|
24522
|
+
// src/posture/render.ts
|
|
24523
|
+
var import_chalk24 = __toESM(require("chalk"));
|
|
24524
|
+
var ICON = {
|
|
24525
|
+
critical: import_chalk24.default.red("\u274C"),
|
|
24526
|
+
high: import_chalk24.default.red("\u274C"),
|
|
24527
|
+
medium: import_chalk24.default.yellow("\u26A0\uFE0F "),
|
|
24528
|
+
advisory: import_chalk24.default.gray("\u26A0\uFE0F ")
|
|
24529
|
+
};
|
|
24530
|
+
var TIER_LABEL = {
|
|
24531
|
+
good: import_chalk24.default.green("Good"),
|
|
24532
|
+
"at-risk": import_chalk24.default.yellow("At risk"),
|
|
24533
|
+
critical: import_chalk24.default.red("Critical")
|
|
24534
|
+
};
|
|
24535
|
+
function wrap(text, width) {
|
|
24536
|
+
const out = [];
|
|
24537
|
+
let cur = "";
|
|
24538
|
+
for (const word of text.split(" ")) {
|
|
24539
|
+
if (cur && (cur + " " + word).length > width) {
|
|
24540
|
+
out.push(cur);
|
|
24541
|
+
cur = word;
|
|
24542
|
+
} else {
|
|
24543
|
+
cur = cur ? cur + " " + word : word;
|
|
24544
|
+
}
|
|
24545
|
+
}
|
|
24546
|
+
if (cur) out.push(cur);
|
|
24547
|
+
return out;
|
|
24548
|
+
}
|
|
24549
|
+
var LABEL_WIDTH = 14;
|
|
24550
|
+
function label(category) {
|
|
24551
|
+
return import_chalk24.default.bold(category.padEnd(Math.max(LABEL_WIDTH, category.length + 1)));
|
|
24552
|
+
}
|
|
24553
|
+
function renderFinding(f) {
|
|
24554
|
+
const lines = [];
|
|
24555
|
+
lines.push(` ${ICON[f.severity]} ${label(f.category)}${f.title}`);
|
|
24556
|
+
const indent = " ".repeat(2 + 3 + LABEL_WIDTH);
|
|
24557
|
+
const width = 80 - indent.length;
|
|
24558
|
+
for (const s of [f.what, f.why, f.who]) {
|
|
24559
|
+
if (s) for (const l of wrap(s, width)) lines.push(indent + import_chalk24.default.gray(l));
|
|
24560
|
+
}
|
|
24561
|
+
for (const d of f.detail) lines.push(indent + import_chalk24.default.gray(d));
|
|
24562
|
+
if (f.fix) {
|
|
24563
|
+
let first = true;
|
|
24564
|
+
for (const seg of f.fix.split("\n")) {
|
|
24565
|
+
for (const l of wrap(seg, width - 2)) {
|
|
24566
|
+
lines.push(indent + import_chalk24.default.cyan(first ? "\u2192 " + l : " " + l));
|
|
24567
|
+
first = false;
|
|
24568
|
+
}
|
|
24569
|
+
}
|
|
24570
|
+
}
|
|
24571
|
+
return lines;
|
|
24572
|
+
}
|
|
24573
|
+
function renderPosture(result) {
|
|
24574
|
+
const lines = [];
|
|
24575
|
+
const tier = TIER_LABEL[result.tier];
|
|
24576
|
+
lines.push("");
|
|
24577
|
+
lines.push(
|
|
24578
|
+
import_chalk24.default.cyan.bold(`\u{1F6E1}\uFE0F Node9 Posture`) + import_chalk24.default.gray(` \u2014 ${result.agent}`) + ` ${import_chalk24.default.bold(`Score: ${result.score}/100`)} (${tier})`
|
|
24579
|
+
);
|
|
24580
|
+
const advisories = result.findings.filter(
|
|
24581
|
+
(f) => f.severity === "advisory" && f.coverage?.state !== "covered"
|
|
24582
|
+
).length;
|
|
24583
|
+
if (advisories > 0) {
|
|
24584
|
+
const word = advisories === 1 ? "advisory" : "advisories";
|
|
24585
|
+
const verb = advisories === 1 ? "doesn't" : "don't";
|
|
24586
|
+
lines.push(
|
|
24587
|
+
" " + import_chalk24.default.gray(
|
|
24588
|
+
`${advisories} ${word} below ${verb} affect the score \u2014 OS-level exposure node9 can't enforce, yours to weigh.`
|
|
24589
|
+
)
|
|
24590
|
+
);
|
|
24591
|
+
}
|
|
24592
|
+
lines.push("");
|
|
24593
|
+
if (result.headline) {
|
|
24594
|
+
const indent = " ";
|
|
24595
|
+
lines.push(` ${import_chalk24.default.red.bold("\u{1F525} Biggest risk")}`);
|
|
24596
|
+
for (const l of wrap(result.headline.risk, 74)) lines.push(indent + import_chalk24.default.white(l));
|
|
24597
|
+
const action = wrap(`Do this first: ${result.headline.action}`, 72);
|
|
24598
|
+
action.forEach((l, i) => lines.push(indent + import_chalk24.default.cyan(i === 0 ? "\u2192 " + l : " " + l)));
|
|
24599
|
+
lines.push("");
|
|
24600
|
+
}
|
|
24601
|
+
const covered = result.findings.filter((f) => f.coverage?.state === "covered");
|
|
24602
|
+
const open = result.findings.filter((f) => f.coverage?.state !== "covered");
|
|
24603
|
+
if (covered.length > 0) {
|
|
24604
|
+
lines.push(" " + import_chalk24.default.green("\u{1F7E2} node9 is already protecting you"));
|
|
24605
|
+
for (const f of covered) {
|
|
24606
|
+
const gated = f.coverage?.level === "review" ? "approval-gating" : "blocking";
|
|
24607
|
+
const via = f.coverage?.via ?? "node9";
|
|
24608
|
+
lines.push(
|
|
24609
|
+
` ${import_chalk24.default.green("\u2705")} ${label(f.category)}${import_chalk24.default.gray(`${via} is ${gated} this`)}`
|
|
24610
|
+
);
|
|
24611
|
+
}
|
|
24612
|
+
lines.push("");
|
|
24613
|
+
}
|
|
24614
|
+
const node9Open = open.filter((f) => f.owner === "node9");
|
|
24615
|
+
const reduceOpen = open.filter((f) => f.owner !== "node9" && f.node9Reduces);
|
|
24616
|
+
const osOpen = open.filter((f) => f.owner !== "node9" && !f.node9Reduces);
|
|
24617
|
+
if (node9Open.length > 0) {
|
|
24618
|
+
lines.push(" " + import_chalk24.default.cyan.bold("\u{1F527} node9 can fix these \u2014 run the command"));
|
|
24619
|
+
for (const f of node9Open) lines.push(...renderFinding(f));
|
|
24620
|
+
}
|
|
24621
|
+
if (reduceOpen.length > 0) {
|
|
24622
|
+
if (node9Open.length > 0) lines.push("");
|
|
24623
|
+
lines.push(
|
|
24624
|
+
" " + import_chalk24.default.yellow.bold("\u{1F512} node9 reduces these \u2014 run the command, the rest is yours")
|
|
24625
|
+
);
|
|
24626
|
+
for (const f of reduceOpen) lines.push(...renderFinding(f));
|
|
24627
|
+
}
|
|
24628
|
+
if (osOpen.length > 0) {
|
|
24629
|
+
if (node9Open.length > 0 || reduceOpen.length > 0) lines.push("");
|
|
24630
|
+
lines.push(" " + import_chalk24.default.bold("\u{1F9F1} Only you can fix these \u2014 node9 can't"));
|
|
24631
|
+
for (const f of osOpen) lines.push(...renderFinding(f));
|
|
24632
|
+
}
|
|
24633
|
+
for (const cat of result.passedCategories) {
|
|
24634
|
+
lines.push(` ${import_chalk24.default.green("\u2705")} ${label(cat)}${import_chalk24.default.gray("no issues found")}`);
|
|
24635
|
+
}
|
|
24636
|
+
for (const cat of result.erroredCategories) {
|
|
24637
|
+
lines.push(` ${import_chalk24.default.gray("\u2022")} ${label(cat)}${import_chalk24.default.gray("could not be checked")}`);
|
|
24638
|
+
}
|
|
24639
|
+
lines.push("");
|
|
24640
|
+
const crit = open.filter((f) => f.severity === "critical").length;
|
|
24641
|
+
const high = open.filter((f) => f.severity === "high").length;
|
|
24642
|
+
const med = open.filter((f) => f.severity === "medium").length;
|
|
24643
|
+
const adv = open.filter((f) => f.severity === "advisory").length;
|
|
24644
|
+
const parts = [];
|
|
24645
|
+
if (crit) parts.push(import_chalk24.default.red(`${crit} critical`));
|
|
24646
|
+
if (high) parts.push(import_chalk24.default.red(`${high} high`));
|
|
24647
|
+
if (med) parts.push(import_chalk24.default.yellow(`${med} medium`));
|
|
24648
|
+
if (adv) parts.push(import_chalk24.default.gray(`${adv} advisory`));
|
|
24649
|
+
const summary = parts.length ? parts.join(" \xB7 ") : import_chalk24.default.green("no findings");
|
|
24650
|
+
lines.push(` ${summary} \xB7 ${import_chalk24.default.gray("track your fleet at app.node9.ai/posture")}`);
|
|
24651
|
+
lines.push("");
|
|
24652
|
+
return lines.join("\n");
|
|
24653
|
+
}
|
|
24654
|
+
|
|
24655
|
+
// src/posture/ship.ts
|
|
24656
|
+
var import_http2 = __toESM(require("http"));
|
|
24657
|
+
var import_https5 = __toESM(require("https"));
|
|
24658
|
+
var import_url = require("url");
|
|
24659
|
+
function buildShipBody(result) {
|
|
24660
|
+
return {
|
|
24661
|
+
score: result.score,
|
|
24662
|
+
tier: result.tier,
|
|
24663
|
+
agent: result.agent,
|
|
24664
|
+
headline: result.headline,
|
|
24665
|
+
// { risk, action } | null — both safe strings
|
|
24666
|
+
findings: result.findings.map((f) => ({
|
|
24667
|
+
category: f.category,
|
|
24668
|
+
severity: f.severity,
|
|
24669
|
+
title: f.title,
|
|
24670
|
+
// Coverage state so the SaaS counts OPEN-only (matching the local score).
|
|
24671
|
+
// A non-sensitive enum — no values or paths. Default 'open' if unannotated.
|
|
24672
|
+
coverage: f.coverage?.state ?? "open",
|
|
24673
|
+
// Plain-language parity with the CLI report. Prose only, no paths.
|
|
24674
|
+
what: f.what,
|
|
24675
|
+
why: f.why,
|
|
24676
|
+
who: f.who,
|
|
24677
|
+
// The runnable fix / OS action — commands + advice, never a path.
|
|
24678
|
+
fix: f.fix,
|
|
24679
|
+
// Whose job it is. Default 'os' so the SaaS never falsely claims node9 can fix it.
|
|
24680
|
+
owner: f.owner ?? "os"
|
|
24681
|
+
}))
|
|
24682
|
+
};
|
|
24683
|
+
}
|
|
24684
|
+
function postureUrlFrom(apiUrl) {
|
|
24685
|
+
return apiUrl.endsWith("/policies/sync") ? apiUrl.replace(/\/policies\/sync$/, "/posture/report") : null;
|
|
24686
|
+
}
|
|
24687
|
+
async function shipPosture(result, creds) {
|
|
24688
|
+
const url = postureUrlFrom(creds.apiUrl);
|
|
24689
|
+
if (!url) return false;
|
|
24690
|
+
const body = JSON.stringify(buildShipBody(result));
|
|
24691
|
+
const parsed = new import_url.URL(url);
|
|
24692
|
+
const transport = parsed.protocol === "http:" ? import_http2.default : import_https5.default;
|
|
24693
|
+
return new Promise((resolve) => {
|
|
24694
|
+
const req = transport.request(
|
|
24695
|
+
{
|
|
24696
|
+
hostname: parsed.hostname,
|
|
24697
|
+
port: parsed.port ? parseInt(parsed.port, 10) : void 0,
|
|
24698
|
+
path: parsed.pathname + parsed.search,
|
|
24699
|
+
method: "POST",
|
|
24700
|
+
headers: {
|
|
24701
|
+
"Content-Type": "application/json",
|
|
24702
|
+
"Content-Length": Buffer.byteLength(body),
|
|
24703
|
+
Authorization: `Bearer ${creds.apiKey}`
|
|
24704
|
+
},
|
|
24705
|
+
timeout: 1e4
|
|
24706
|
+
},
|
|
24707
|
+
(res) => {
|
|
24708
|
+
const ok2 = !!res.statusCode && res.statusCode >= 200 && res.statusCode < 300;
|
|
24709
|
+
res.resume();
|
|
24710
|
+
res.on("end", () => resolve(ok2));
|
|
24711
|
+
res.on("error", () => resolve(false));
|
|
24712
|
+
}
|
|
24713
|
+
);
|
|
24714
|
+
req.on("error", () => resolve(false));
|
|
24715
|
+
req.on("timeout", () => {
|
|
24716
|
+
req.destroy();
|
|
24717
|
+
resolve(false);
|
|
24718
|
+
});
|
|
24719
|
+
req.write(body);
|
|
24720
|
+
req.end();
|
|
24721
|
+
});
|
|
24722
|
+
}
|
|
24723
|
+
|
|
24724
|
+
// src/cli/commands/posture.ts
|
|
24725
|
+
init_sync();
|
|
24726
|
+
function registerPostureCommand(program2) {
|
|
24727
|
+
program2.command("posture").description("Security scorecard for the agent on this host (secrets, egress, gate)").option("--agent <name>", "label / policy scope for the agent being graded").option("--json", "emit the raw result as JSON instead of the scorecard").option("--ship", "send a redacted snapshot to your node9 dashboard").action(async (opts) => {
|
|
24728
|
+
const result = await runPosture({ agent: opts.agent });
|
|
24729
|
+
if (opts.json) {
|
|
24730
|
+
console.log(JSON.stringify(result, null, 2));
|
|
24731
|
+
} else {
|
|
24732
|
+
console.log(renderPosture(result));
|
|
24733
|
+
}
|
|
24734
|
+
if (opts.ship) {
|
|
24735
|
+
const creds = readCredentials();
|
|
24736
|
+
if (!creds) {
|
|
24737
|
+
console.error(import_chalk25.default.gray(" Run `node9 login` to ship this to your dashboard."));
|
|
24738
|
+
} else {
|
|
24739
|
+
const ok2 = await shipPosture(result, creds);
|
|
24740
|
+
console.error(
|
|
24741
|
+
ok2 ? import_chalk25.default.gray(" \u2713 Shipped to your node9 dashboard.") : import_chalk25.default.gray(" Could not reach the dashboard \u2014 saved locally only.")
|
|
24742
|
+
);
|
|
24743
|
+
}
|
|
24744
|
+
}
|
|
24745
|
+
if (result.tier === "critical") process.exitCode = 2;
|
|
24746
|
+
});
|
|
24747
|
+
}
|
|
24748
|
+
|
|
24749
|
+
// src/cli/commands/egress.ts
|
|
24750
|
+
var import_chalk26 = __toESM(require("chalk"));
|
|
24751
|
+
var import_fs50 = __toESM(require("fs"));
|
|
24752
|
+
var import_os45 = __toESM(require("os"));
|
|
24753
|
+
var import_path49 = __toESM(require("path"));
|
|
24754
|
+
init_config();
|
|
24755
|
+
init_dist();
|
|
24756
|
+
var DEFAULT_EGRESS = {
|
|
24757
|
+
enabled: false,
|
|
24758
|
+
mode: "review",
|
|
24759
|
+
allow: [],
|
|
24760
|
+
deny: [],
|
|
24761
|
+
allowPrivate: true
|
|
24762
|
+
};
|
|
24763
|
+
function configPath() {
|
|
24764
|
+
return import_path49.default.join(import_os45.default.homedir(), ".node9", "config.json");
|
|
24765
|
+
}
|
|
24766
|
+
function readRawConfig() {
|
|
24767
|
+
let text;
|
|
24768
|
+
try {
|
|
24769
|
+
text = import_fs50.default.readFileSync(configPath(), "utf8");
|
|
24770
|
+
} catch (err2) {
|
|
24771
|
+
if (err2.code === "ENOENT") return {};
|
|
24772
|
+
throw err2;
|
|
24773
|
+
}
|
|
24774
|
+
try {
|
|
24775
|
+
return JSON.parse(text);
|
|
24776
|
+
} catch {
|
|
24777
|
+
throw new Error(
|
|
24778
|
+
`${configPath()} is not valid JSON \u2014 fix it before changing egress (refusing to overwrite).`
|
|
24779
|
+
);
|
|
24780
|
+
}
|
|
24781
|
+
}
|
|
24782
|
+
function writeRawConfig(config) {
|
|
24783
|
+
const p = configPath();
|
|
24784
|
+
import_fs50.default.mkdirSync(import_path49.default.dirname(p), { recursive: true });
|
|
24785
|
+
import_fs50.default.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
|
|
24786
|
+
}
|
|
24787
|
+
function applyEgress(config, change) {
|
|
24788
|
+
const policy = config.policy = config.policy ?? {};
|
|
24789
|
+
const existing = policy.egress ?? {};
|
|
24790
|
+
policy.egress = { ...DEFAULT_EGRESS, ...existing, ...change };
|
|
24791
|
+
return config;
|
|
24792
|
+
}
|
|
24793
|
+
function withConfig(fn) {
|
|
24794
|
+
let config;
|
|
24795
|
+
try {
|
|
24796
|
+
config = readRawConfig();
|
|
24797
|
+
} catch (err2) {
|
|
24798
|
+
console.error(import_chalk26.default.red(`
|
|
24799
|
+
\u2717 ${err2.message}
|
|
24800
|
+
`));
|
|
24801
|
+
process.exitCode = 1;
|
|
24802
|
+
return false;
|
|
24803
|
+
}
|
|
24804
|
+
fn(config);
|
|
24805
|
+
writeRawConfig(config);
|
|
24806
|
+
return true;
|
|
24807
|
+
}
|
|
24808
|
+
function mutate(change) {
|
|
24809
|
+
return withConfig((config) => applyEgress(config, change));
|
|
24810
|
+
}
|
|
24811
|
+
function addHost(list, host) {
|
|
24812
|
+
return withConfig((config) => {
|
|
24813
|
+
const existing = config.policy?.egress ?? {};
|
|
24814
|
+
const current = { ...DEFAULT_EGRESS, ...existing };
|
|
24815
|
+
const updated = current[list].includes(host) ? current[list] : [...current[list], host];
|
|
24816
|
+
applyEgress(config, { [list]: updated });
|
|
24817
|
+
});
|
|
24818
|
+
}
|
|
24819
|
+
function showStatus() {
|
|
24820
|
+
const e = getConfig().policy.egress;
|
|
24821
|
+
const state = !e.enabled ? import_chalk26.default.red("OFF \u2014 your agent can reach any host") : e.mode === "block" ? import_chalk26.default.green("LOCKED (block) \u2014 unknown hosts are denied") : import_chalk26.default.yellow("WATCHING (review) \u2014 unknown hosts prompt you");
|
|
24822
|
+
console.log(import_chalk26.default.cyan.bold("\n\u{1F310} Egress control"));
|
|
24823
|
+
console.log(" State: " + state);
|
|
24824
|
+
console.log(
|
|
24825
|
+
import_chalk26.default.gray(
|
|
24826
|
+
` ${DEFAULT_EGRESS_ALLOWLIST.length} common dev/LLM hosts are always allowed (github, npm, pypi, anthropic, \u2026).`
|
|
24827
|
+
)
|
|
24828
|
+
);
|
|
24829
|
+
if (e.allow.length) console.log(" Your allow: " + e.allow.join(", "));
|
|
24830
|
+
if (e.deny.length) console.log(" Your deny: " + e.deny.join(", "));
|
|
24831
|
+
if (!e.enabled) {
|
|
24832
|
+
console.log(import_chalk26.default.gray("\n Turn it on: node9 egress watch (prompt on unknown hosts)"));
|
|
24833
|
+
console.log(import_chalk26.default.gray(" node9 egress lock (hard-block unknown hosts)"));
|
|
24834
|
+
}
|
|
24835
|
+
console.log("");
|
|
24836
|
+
}
|
|
24837
|
+
function registerEgressCommand(program2) {
|
|
24838
|
+
const egress = program2.command("egress").description("Control where your agent can send data (egress allowlist)");
|
|
24839
|
+
egress.command("watch").description("Prompt before the agent reaches an unknown host (review mode)").action(() => {
|
|
24840
|
+
if (!mutate({ enabled: true, mode: "review" })) return;
|
|
24841
|
+
console.log(import_chalk26.default.green("\n\u2713 Egress is now watched (review mode)."));
|
|
24842
|
+
console.log(
|
|
24843
|
+
import_chalk26.default.gray(" Routine hosts (LLM APIs, package registries, localhost) are allowed.")
|
|
24844
|
+
);
|
|
24845
|
+
console.log(
|
|
24846
|
+
import_chalk26.default.gray(" An unknown host will prompt you \u2014 run `node9 egress lock` to hard-block.\n")
|
|
24847
|
+
);
|
|
24848
|
+
});
|
|
24849
|
+
egress.command("lock").description("Block the agent from reaching unknown hosts (block mode)").action(() => {
|
|
24850
|
+
if (!mutate({ enabled: true, mode: "block" })) return;
|
|
24851
|
+
console.log(import_chalk26.default.green("\n\u2713 Egress is now locked (block mode)."));
|
|
24852
|
+
console.log(import_chalk26.default.gray(" Routine hosts are still allowed; unknown hosts are denied."));
|
|
24853
|
+
console.log(import_chalk26.default.gray(" Allow a specific host with `node9 egress allow <host>`.\n"));
|
|
24854
|
+
});
|
|
24855
|
+
egress.command("allow <host>").description("Allow an extra host (glob, e.g. *.mycorp.com)").action((host) => {
|
|
24856
|
+
if (!addHost("allow", host)) return;
|
|
24857
|
+
console.log(import_chalk26.default.green(`
|
|
24858
|
+
\u2713 Allowed egress to ${host}.
|
|
24859
|
+
`));
|
|
24860
|
+
});
|
|
24861
|
+
egress.command("deny <host>").description("Block an extra host (deny always wins)").action((host) => {
|
|
24862
|
+
if (!addHost("deny", host)) return;
|
|
24863
|
+
console.log(import_chalk26.default.green(`
|
|
24864
|
+
\u2713 Denied egress to ${host}.
|
|
24865
|
+
`));
|
|
24866
|
+
});
|
|
24867
|
+
egress.command("off").description("Turn egress control off").action(() => {
|
|
24868
|
+
if (!mutate({ enabled: false })) return;
|
|
24869
|
+
console.log(
|
|
24870
|
+
import_chalk26.default.yellow("\n\u2713 Egress control is off \u2014 the agent can reach any host again.\n")
|
|
24871
|
+
);
|
|
24872
|
+
});
|
|
24873
|
+
egress.action(showStatus);
|
|
24874
|
+
}
|
|
24875
|
+
|
|
24876
|
+
// src/cli/commands/sessions.ts
|
|
24877
|
+
var import_chalk27 = __toESM(require("chalk"));
|
|
24878
|
+
var import_fs51 = __toESM(require("fs"));
|
|
24879
|
+
var import_path50 = __toESM(require("path"));
|
|
24880
|
+
var import_os46 = __toESM(require("os"));
|
|
23751
24881
|
init_scan_summary();
|
|
23752
24882
|
init_litellm();
|
|
23753
24883
|
init_cost_gemini();
|
|
@@ -23768,10 +24898,10 @@ function encodeProjectPath(projectPath) {
|
|
|
23768
24898
|
}
|
|
23769
24899
|
function sessionJsonlPath(projectPath, sessionId) {
|
|
23770
24900
|
const encoded = encodeProjectPath(projectPath);
|
|
23771
|
-
return
|
|
24901
|
+
return import_path50.default.join(import_os46.default.homedir(), ".claude", "projects", encoded, `${sessionId}.jsonl`);
|
|
23772
24902
|
}
|
|
23773
24903
|
function projectLabel(projectPath) {
|
|
23774
|
-
return projectPath.replace(
|
|
24904
|
+
return projectPath.replace(import_os46.default.homedir(), "~");
|
|
23775
24905
|
}
|
|
23776
24906
|
function parseHistoryLines(lines) {
|
|
23777
24907
|
const entries = [];
|
|
@@ -23840,10 +24970,10 @@ function parseSessionLines(lines) {
|
|
|
23840
24970
|
return { toolCalls, costUSD, hasSnapshot, modifiedFiles };
|
|
23841
24971
|
}
|
|
23842
24972
|
function loadAuditEntries(auditPath) {
|
|
23843
|
-
const aPath = auditPath ??
|
|
24973
|
+
const aPath = auditPath ?? import_path50.default.join(import_os46.default.homedir(), ".node9", "audit.log");
|
|
23844
24974
|
let raw;
|
|
23845
24975
|
try {
|
|
23846
|
-
raw =
|
|
24976
|
+
raw = import_fs51.default.readFileSync(aPath, "utf-8");
|
|
23847
24977
|
} catch {
|
|
23848
24978
|
return [];
|
|
23849
24979
|
}
|
|
@@ -23879,8 +25009,8 @@ function auditEntriesInWindow(entries, windowStart, windowEnd) {
|
|
|
23879
25009
|
return result;
|
|
23880
25010
|
}
|
|
23881
25011
|
function buildGeminiSessions(days, allAuditEntries) {
|
|
23882
|
-
const tmpDir =
|
|
23883
|
-
if (!
|
|
25012
|
+
const tmpDir = import_path50.default.join(import_os46.default.homedir(), ".gemini", "tmp");
|
|
25013
|
+
if (!import_fs51.default.existsSync(tmpDir)) return [];
|
|
23884
25014
|
const cutoff = days !== null ? (() => {
|
|
23885
25015
|
const d = /* @__PURE__ */ new Date();
|
|
23886
25016
|
d.setDate(d.getDate() - days);
|
|
@@ -23889,35 +25019,35 @@ function buildGeminiSessions(days, allAuditEntries) {
|
|
|
23889
25019
|
})() : null;
|
|
23890
25020
|
let slugDirs;
|
|
23891
25021
|
try {
|
|
23892
|
-
slugDirs =
|
|
25022
|
+
slugDirs = import_fs51.default.readdirSync(tmpDir);
|
|
23893
25023
|
} catch {
|
|
23894
25024
|
return [];
|
|
23895
25025
|
}
|
|
23896
25026
|
const summaries = [];
|
|
23897
25027
|
for (const slug of slugDirs) {
|
|
23898
|
-
const slugPath =
|
|
25028
|
+
const slugPath = import_path50.default.join(tmpDir, slug);
|
|
23899
25029
|
try {
|
|
23900
|
-
if (!
|
|
25030
|
+
if (!import_fs51.default.statSync(slugPath).isDirectory()) continue;
|
|
23901
25031
|
} catch {
|
|
23902
25032
|
continue;
|
|
23903
25033
|
}
|
|
23904
|
-
let projectRoot =
|
|
25034
|
+
let projectRoot = import_path50.default.join(import_os46.default.homedir(), slug);
|
|
23905
25035
|
try {
|
|
23906
|
-
projectRoot =
|
|
25036
|
+
projectRoot = import_fs51.default.readFileSync(import_path50.default.join(slugPath, ".project_root"), "utf-8").trim();
|
|
23907
25037
|
} catch {
|
|
23908
25038
|
}
|
|
23909
|
-
const chatsDir =
|
|
23910
|
-
if (!
|
|
25039
|
+
const chatsDir = import_path50.default.join(slugPath, "chats");
|
|
25040
|
+
if (!import_fs51.default.existsSync(chatsDir)) continue;
|
|
23911
25041
|
let chatFiles;
|
|
23912
25042
|
try {
|
|
23913
|
-
chatFiles =
|
|
25043
|
+
chatFiles = import_fs51.default.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
|
|
23914
25044
|
} catch {
|
|
23915
25045
|
continue;
|
|
23916
25046
|
}
|
|
23917
25047
|
for (const chatFile of chatFiles) {
|
|
23918
25048
|
let raw;
|
|
23919
25049
|
try {
|
|
23920
|
-
raw =
|
|
25050
|
+
raw = import_fs51.default.readFileSync(import_path50.default.join(chatsDir, chatFile), "utf-8");
|
|
23921
25051
|
} catch {
|
|
23922
25052
|
continue;
|
|
23923
25053
|
}
|
|
@@ -23997,8 +25127,8 @@ function buildGeminiSessions(days, allAuditEntries) {
|
|
|
23997
25127
|
return summaries;
|
|
23998
25128
|
}
|
|
23999
25129
|
function buildCodexSessions(days, allAuditEntries) {
|
|
24000
|
-
const sessionsBase =
|
|
24001
|
-
if (!
|
|
25130
|
+
const sessionsBase = import_path50.default.join(import_os46.default.homedir(), ".codex", "sessions");
|
|
25131
|
+
if (!import_fs51.default.existsSync(sessionsBase)) return [];
|
|
24002
25132
|
const cutoff = days !== null ? (() => {
|
|
24003
25133
|
const d = /* @__PURE__ */ new Date();
|
|
24004
25134
|
d.setDate(d.getDate() - days);
|
|
@@ -24007,29 +25137,29 @@ function buildCodexSessions(days, allAuditEntries) {
|
|
|
24007
25137
|
})() : null;
|
|
24008
25138
|
const jsonlFiles = [];
|
|
24009
25139
|
try {
|
|
24010
|
-
for (const year of
|
|
24011
|
-
const yearPath =
|
|
25140
|
+
for (const year of import_fs51.default.readdirSync(sessionsBase)) {
|
|
25141
|
+
const yearPath = import_path50.default.join(sessionsBase, year);
|
|
24012
25142
|
try {
|
|
24013
|
-
if (!
|
|
25143
|
+
if (!import_fs51.default.statSync(yearPath).isDirectory()) continue;
|
|
24014
25144
|
} catch {
|
|
24015
25145
|
continue;
|
|
24016
25146
|
}
|
|
24017
|
-
for (const month of
|
|
24018
|
-
const monthPath =
|
|
25147
|
+
for (const month of import_fs51.default.readdirSync(yearPath)) {
|
|
25148
|
+
const monthPath = import_path50.default.join(yearPath, month);
|
|
24019
25149
|
try {
|
|
24020
|
-
if (!
|
|
25150
|
+
if (!import_fs51.default.statSync(monthPath).isDirectory()) continue;
|
|
24021
25151
|
} catch {
|
|
24022
25152
|
continue;
|
|
24023
25153
|
}
|
|
24024
|
-
for (const day of
|
|
24025
|
-
const dayPath =
|
|
25154
|
+
for (const day of import_fs51.default.readdirSync(monthPath)) {
|
|
25155
|
+
const dayPath = import_path50.default.join(monthPath, day);
|
|
24026
25156
|
try {
|
|
24027
|
-
if (!
|
|
25157
|
+
if (!import_fs51.default.statSync(dayPath).isDirectory()) continue;
|
|
24028
25158
|
} catch {
|
|
24029
25159
|
continue;
|
|
24030
25160
|
}
|
|
24031
|
-
for (const file of
|
|
24032
|
-
if (file.endsWith(".jsonl")) jsonlFiles.push(
|
|
25161
|
+
for (const file of import_fs51.default.readdirSync(dayPath)) {
|
|
25162
|
+
if (file.endsWith(".jsonl")) jsonlFiles.push(import_path50.default.join(dayPath, file));
|
|
24033
25163
|
}
|
|
24034
25164
|
}
|
|
24035
25165
|
}
|
|
@@ -24041,7 +25171,7 @@ function buildCodexSessions(days, allAuditEntries) {
|
|
|
24041
25171
|
for (const filePath of jsonlFiles) {
|
|
24042
25172
|
let lines;
|
|
24043
25173
|
try {
|
|
24044
|
-
lines =
|
|
25174
|
+
lines = import_fs51.default.readFileSync(filePath, "utf-8").split("\n");
|
|
24045
25175
|
} catch {
|
|
24046
25176
|
continue;
|
|
24047
25177
|
}
|
|
@@ -24127,10 +25257,10 @@ function buildCodexSessions(days, allAuditEntries) {
|
|
|
24127
25257
|
return summaries;
|
|
24128
25258
|
}
|
|
24129
25259
|
function buildSessions(days, historyPath) {
|
|
24130
|
-
const hPath = historyPath ??
|
|
25260
|
+
const hPath = historyPath ?? import_path50.default.join(import_os46.default.homedir(), ".claude", "history.jsonl");
|
|
24131
25261
|
let historyRaw = "";
|
|
24132
25262
|
try {
|
|
24133
|
-
historyRaw =
|
|
25263
|
+
historyRaw = import_fs51.default.readFileSync(hPath, "utf-8");
|
|
24134
25264
|
} catch {
|
|
24135
25265
|
}
|
|
24136
25266
|
const cutoff = days !== null ? (() => {
|
|
@@ -24154,7 +25284,7 @@ function buildSessions(days, historyPath) {
|
|
|
24154
25284
|
const jsonlFile = sessionJsonlPath(entry.project, entry.sessionId);
|
|
24155
25285
|
let sessionLines = [];
|
|
24156
25286
|
try {
|
|
24157
|
-
sessionLines =
|
|
25287
|
+
sessionLines = import_fs51.default.readFileSync(jsonlFile, "utf-8").split("\n");
|
|
24158
25288
|
} catch {
|
|
24159
25289
|
}
|
|
24160
25290
|
const { toolCalls, costUSD, hasSnapshot, modifiedFiles } = parseSessionLines(sessionLines);
|
|
@@ -24240,11 +25370,11 @@ function toolInputSummary(tool, input) {
|
|
|
24240
25370
|
}
|
|
24241
25371
|
function toolColor(tool) {
|
|
24242
25372
|
const t = tool.toLowerCase();
|
|
24243
|
-
if (t === "bash" || t === "execute_bash") return
|
|
24244
|
-
if (t === "write") return
|
|
24245
|
-
if (t === "edit" || t === "notebookedit") return
|
|
24246
|
-
if (t === "read") return
|
|
24247
|
-
return
|
|
25373
|
+
if (t === "bash" || t === "execute_bash") return import_chalk27.default.red;
|
|
25374
|
+
if (t === "write") return import_chalk27.default.green;
|
|
25375
|
+
if (t === "edit" || t === "notebookedit") return import_chalk27.default.yellow;
|
|
25376
|
+
if (t === "read") return import_chalk27.default.cyan;
|
|
25377
|
+
return import_chalk27.default.gray;
|
|
24248
25378
|
}
|
|
24249
25379
|
function barStr2(value, max, width) {
|
|
24250
25380
|
if (max === 0 || width <= 0) return "\u2591".repeat(width);
|
|
@@ -24254,7 +25384,7 @@ function barStr2(value, max, width) {
|
|
|
24254
25384
|
function colorBar2(value, max, width) {
|
|
24255
25385
|
const s = barStr2(value, max, width);
|
|
24256
25386
|
const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
|
|
24257
|
-
return
|
|
25387
|
+
return import_chalk27.default.cyan(s.slice(0, filled)) + import_chalk27.default.dim(s.slice(filled));
|
|
24258
25388
|
}
|
|
24259
25389
|
function renderSummary(summaries) {
|
|
24260
25390
|
const totalTools = summaries.reduce((n, s) => n + s.toolCalls.length, 0);
|
|
@@ -24284,45 +25414,45 @@ function renderSummary(summaries) {
|
|
|
24284
25414
|
}
|
|
24285
25415
|
const topProjects = [...projCosts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
24286
25416
|
const W = 20;
|
|
24287
|
-
console.log(
|
|
25417
|
+
console.log(import_chalk27.default.dim(" " + "\u2500".repeat(70)));
|
|
24288
25418
|
console.log(
|
|
24289
|
-
" " +
|
|
25419
|
+
" " + import_chalk27.default.bold.white(String(summaries.length).padEnd(4)) + import_chalk27.default.dim("sessions ") + import_chalk27.default.bold.yellow(fmtCost3(totalCost).padEnd(10)) + import_chalk27.default.dim("total ") + import_chalk27.default.bold.white(String(totalTools).padEnd(6)) + import_chalk27.default.dim("tool calls ") + import_chalk27.default.bold.white(String(totalFiles)) + import_chalk27.default.dim(" files modified") + (totalBlocked > 0 ? import_chalk27.default.dim(" ") + import_chalk27.default.red.bold(String(totalBlocked)) + import_chalk27.default.dim(" blocked by node9") : "")
|
|
24290
25420
|
);
|
|
24291
25421
|
console.log(
|
|
24292
|
-
" " +
|
|
25422
|
+
" " + import_chalk27.default.dim("avg ") + import_chalk27.default.white(fmtCost3(avgCost).padEnd(10)) + import_chalk27.default.dim("/session ") + import_chalk27.default.green(String(snapshots)) + import_chalk27.default.dim(` of ${summaries.length} sessions had snapshots`)
|
|
24293
25423
|
);
|
|
24294
25424
|
console.log("");
|
|
24295
|
-
console.log(" " +
|
|
25425
|
+
console.log(" " + import_chalk27.default.dim("Tool breakdown:"));
|
|
24296
25426
|
const maxGroup = Math.max(...Object.values(groups));
|
|
24297
|
-
for (const [
|
|
25427
|
+
for (const [label2, count] of Object.entries(groups)) {
|
|
24298
25428
|
if (count === 0) continue;
|
|
24299
25429
|
const pct = totalTools > 0 ? Math.round(count / totalTools * 100) : 0;
|
|
24300
25430
|
console.log(
|
|
24301
|
-
" " +
|
|
25431
|
+
" " + label2.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " + import_chalk27.default.white(String(count).padStart(4)) + import_chalk27.default.dim(` (${String(pct)}%)`)
|
|
24302
25432
|
);
|
|
24303
25433
|
}
|
|
24304
25434
|
console.log("");
|
|
24305
25435
|
if (topProjects.length > 1) {
|
|
24306
|
-
console.log(" " +
|
|
25436
|
+
console.log(" " + import_chalk27.default.dim("Cost by project:"));
|
|
24307
25437
|
const maxProjCost = topProjects[0][1];
|
|
24308
25438
|
for (const [proj, cost] of topProjects) {
|
|
24309
25439
|
console.log(
|
|
24310
|
-
" " + proj.slice(0, 28).padEnd(28) + " " + colorBar2(cost, maxProjCost, W) + " " +
|
|
25440
|
+
" " + proj.slice(0, 28).padEnd(28) + " " + colorBar2(cost, maxProjCost, W) + " " + import_chalk27.default.yellow(fmtCost3(cost))
|
|
24311
25441
|
);
|
|
24312
25442
|
}
|
|
24313
25443
|
console.log("");
|
|
24314
25444
|
}
|
|
24315
|
-
console.log(
|
|
25445
|
+
console.log(import_chalk27.default.dim(" " + "\u2500".repeat(70)));
|
|
24316
25446
|
console.log("");
|
|
24317
25447
|
}
|
|
24318
25448
|
function renderList(summaries, totalCost) {
|
|
24319
25449
|
if (summaries.length === 0) {
|
|
24320
|
-
console.log(
|
|
25450
|
+
console.log(import_chalk27.default.yellow(" No sessions found in the requested range.\n"));
|
|
24321
25451
|
return;
|
|
24322
25452
|
}
|
|
24323
|
-
const totalLabel = totalCost > 0 ?
|
|
25453
|
+
const totalLabel = totalCost > 0 ? import_chalk27.default.dim(" ~" + fmtCost3(totalCost) + " total") : "";
|
|
24324
25454
|
console.log(
|
|
24325
|
-
" " +
|
|
25455
|
+
" " + import_chalk27.default.white(String(summaries.length)) + import_chalk27.default.dim(` session${summaries.length !== 1 ? "s" : ""}`) + totalLabel
|
|
24326
25456
|
);
|
|
24327
25457
|
console.log("");
|
|
24328
25458
|
let lastGroup = "";
|
|
@@ -24330,51 +25460,51 @@ function renderList(summaries, totalCost) {
|
|
|
24330
25460
|
const activeDate = fmtDate2(s.lastActiveTime);
|
|
24331
25461
|
const group = activeDate + " " + s.projectLabel;
|
|
24332
25462
|
if (group !== lastGroup) {
|
|
24333
|
-
console.log(
|
|
25463
|
+
console.log(import_chalk27.default.dim(" \u2500\u2500\u2500 ") + import_chalk27.default.bold(activeDate) + import_chalk27.default.dim(" " + s.projectLabel));
|
|
24334
25464
|
lastGroup = group;
|
|
24335
25465
|
}
|
|
24336
25466
|
const startDate = fmtDate2(s.startTime);
|
|
24337
|
-
const dateRange = startDate !== activeDate ?
|
|
24338
|
-
const timeStr =
|
|
24339
|
-
const prompt =
|
|
24340
|
-
const tools = s.toolCalls.length > 0 ?
|
|
24341
|
-
const cost = s.costUSD > 0 ?
|
|
24342
|
-
const blocked = s.blockedCalls.length > 0 ?
|
|
24343
|
-
const snap = s.hasSnapshot ?
|
|
24344
|
-
const agentBadge =
|
|
25467
|
+
const dateRange = startDate !== activeDate ? import_chalk27.default.dim(" (" + startDate + " \u2192 " + activeDate + ")") : "";
|
|
25468
|
+
const timeStr = import_chalk27.default.dim(fmtTime(s.startTime));
|
|
25469
|
+
const prompt = import_chalk27.default.white(truncate(s.firstPrompt.replace(/\n/g, " "), 50).padEnd(50));
|
|
25470
|
+
const tools = s.toolCalls.length > 0 ? import_chalk27.default.dim(String(s.toolCalls.length).padStart(3) + " tools") : import_chalk27.default.dim(" 0 tools");
|
|
25471
|
+
const cost = s.costUSD > 0 ? import_chalk27.default.dim(" " + fmtCost3(s.costUSD).padEnd(8)) : " ";
|
|
25472
|
+
const blocked = s.blockedCalls.length > 0 ? import_chalk27.default.red(" \u{1F6D1} " + String(s.blockedCalls.length)) : "";
|
|
25473
|
+
const snap = s.hasSnapshot ? import_chalk27.default.green(" \u{1F4F8}") : "";
|
|
25474
|
+
const agentBadge = import_chalk27.default[agentColorName(s.agent ?? "claude")](
|
|
24345
25475
|
" " + agentBadgeText(s.agent ?? "claude", 0)
|
|
24346
25476
|
);
|
|
24347
|
-
const sid =
|
|
25477
|
+
const sid = import_chalk27.default.dim(" " + s.sessionId.slice(0, 8));
|
|
24348
25478
|
console.log(
|
|
24349
25479
|
` ${timeStr} ${prompt} ${tools}${cost}${blocked}${snap}${agentBadge}${sid}${dateRange}`
|
|
24350
25480
|
);
|
|
24351
25481
|
}
|
|
24352
25482
|
console.log("");
|
|
24353
25483
|
console.log(
|
|
24354
|
-
|
|
25484
|
+
import_chalk27.default.dim(" Run") + " " + import_chalk27.default.cyan("node9 sessions --detail <session-id>") + import_chalk27.default.dim(" for full tool trace.")
|
|
24355
25485
|
);
|
|
24356
25486
|
console.log("");
|
|
24357
25487
|
}
|
|
24358
25488
|
function renderDetail(s) {
|
|
24359
25489
|
console.log("");
|
|
24360
|
-
console.log(
|
|
25490
|
+
console.log(import_chalk27.default.bold(" Session ") + import_chalk27.default.dim(s.sessionId));
|
|
24361
25491
|
console.log(
|
|
24362
|
-
|
|
25492
|
+
import_chalk27.default.bold(" Prompt ") + import_chalk27.default.white(s.firstPrompt.replace(/\n/g, " ").slice(0, 120))
|
|
24363
25493
|
);
|
|
24364
|
-
console.log(
|
|
25494
|
+
console.log(import_chalk27.default.bold(" Project ") + import_chalk27.default.white(s.projectLabel));
|
|
24365
25495
|
if (s.agent) {
|
|
24366
|
-
const agentLabel2 =
|
|
24367
|
-
console.log(
|
|
25496
|
+
const agentLabel2 = import_chalk27.default[agentColorName(s.agent)](agentDisplayName(s.agent));
|
|
25497
|
+
console.log(import_chalk27.default.bold(" Agent ") + agentLabel2);
|
|
24368
25498
|
}
|
|
24369
|
-
console.log(
|
|
25499
|
+
console.log(import_chalk27.default.bold(" When ") + import_chalk27.default.white(fmtDateTime(s.startTime)));
|
|
24370
25500
|
if (s.costUSD > 0)
|
|
24371
|
-
console.log(
|
|
25501
|
+
console.log(import_chalk27.default.bold(" Cost ") + import_chalk27.default.yellow("~" + fmtCost3(s.costUSD)));
|
|
24372
25502
|
console.log(
|
|
24373
|
-
|
|
25503
|
+
import_chalk27.default.bold(" Snapshot ") + (s.hasSnapshot ? import_chalk27.default.green("\u2713 taken") : import_chalk27.default.dim("none"))
|
|
24374
25504
|
);
|
|
24375
25505
|
console.log("");
|
|
24376
25506
|
if (s.toolCalls.length === 0 && s.blockedCalls.length === 0) {
|
|
24377
|
-
console.log(
|
|
25507
|
+
console.log(import_chalk27.default.dim(" No tool calls recorded.\n"));
|
|
24378
25508
|
return;
|
|
24379
25509
|
}
|
|
24380
25510
|
const timeline = [
|
|
@@ -24387,32 +25517,32 @@ function renderDetail(s) {
|
|
|
24387
25517
|
});
|
|
24388
25518
|
const headerParts = [`Tool calls (${s.toolCalls.length})`];
|
|
24389
25519
|
if (s.blockedCalls.length > 0)
|
|
24390
|
-
headerParts.push(
|
|
24391
|
-
console.log(
|
|
25520
|
+
headerParts.push(import_chalk27.default.red(`${s.blockedCalls.length} blocked by node9`));
|
|
25521
|
+
console.log(import_chalk27.default.bold(" " + headerParts.join(" \xB7 ")));
|
|
24392
25522
|
console.log("");
|
|
24393
25523
|
for (const entry of timeline) {
|
|
24394
25524
|
if (entry.kind === "tool") {
|
|
24395
25525
|
const tc = entry.tc;
|
|
24396
25526
|
const colorFn = toolColor(tc.tool);
|
|
24397
25527
|
const toolPad = colorFn(tc.tool.padEnd(16));
|
|
24398
|
-
const detail =
|
|
24399
|
-
const ts = tc.timestamp ?
|
|
25528
|
+
const detail = import_chalk27.default.gray(truncate(toolInputSummary(tc.tool, tc.input), 70));
|
|
25529
|
+
const ts = tc.timestamp ? import_chalk27.default.dim(fmtTime(tc.timestamp) + " ") : " ";
|
|
24400
25530
|
console.log(` ${ts}${toolPad} ${detail}`);
|
|
24401
25531
|
} else {
|
|
24402
25532
|
const bc = entry.bc;
|
|
24403
|
-
const ts = bc.timestamp ?
|
|
24404
|
-
const
|
|
24405
|
-
const toolName =
|
|
24406
|
-
const argsSummary = bc.args ?
|
|
24407
|
-
const reason = bc.checkedBy ?
|
|
24408
|
-
console.log(` ${ts}${
|
|
25533
|
+
const ts = bc.timestamp ? import_chalk27.default.dim(fmtTime(bc.timestamp) + " ") : " ";
|
|
25534
|
+
const label2 = import_chalk27.default.red("\u{1F6D1} BLOCKED".padEnd(16));
|
|
25535
|
+
const toolName = import_chalk27.default.red(bc.tool.padEnd(10));
|
|
25536
|
+
const argsSummary = bc.args ? import_chalk27.default.gray(truncate(toolInputSummary(bc.tool, bc.args), 40)) : import_chalk27.default.dim("[args not logged]");
|
|
25537
|
+
const reason = bc.checkedBy ? import_chalk27.default.dim(" \u2190 " + bc.checkedBy) : "";
|
|
25538
|
+
console.log(` ${ts}${label2} ${toolName} ${argsSummary}${reason}`);
|
|
24409
25539
|
}
|
|
24410
25540
|
}
|
|
24411
25541
|
console.log("");
|
|
24412
25542
|
if (s.modifiedFiles.length > 0) {
|
|
24413
|
-
console.log(
|
|
25543
|
+
console.log(import_chalk27.default.bold(` Files modified (${s.modifiedFiles.length}):`));
|
|
24414
25544
|
for (const f of s.modifiedFiles) {
|
|
24415
|
-
console.log(" " +
|
|
25545
|
+
console.log(" " + import_chalk27.default.yellow(f));
|
|
24416
25546
|
}
|
|
24417
25547
|
console.log("");
|
|
24418
25548
|
}
|
|
@@ -24420,13 +25550,13 @@ function renderDetail(s) {
|
|
|
24420
25550
|
function registerSessionsCommand(program2) {
|
|
24421
25551
|
program2.command("sessions").description("Show what your AI agent did \u2014 sessions, tool calls, cost, and file changes").option("--all", "Show all sessions (default: last 7 days)").option("--days <n>", "Show last N days of sessions", "7").option("--detail <sessionId>", "Show full tool trace for a session").action((options) => {
|
|
24422
25552
|
console.log("");
|
|
24423
|
-
console.log(
|
|
25553
|
+
console.log(import_chalk27.default.cyan.bold("\u{1F4CB} node9 sessions") + import_chalk27.default.dim(" \u2014 what your AI agent did"));
|
|
24424
25554
|
console.log("");
|
|
24425
25555
|
const days = options.detail || options.all ? null : Math.max(1, parseInt(options.days, 10) || 7);
|
|
24426
25556
|
const rangeLabel = options.detail ? "all time" : options.all ? "all time" : `last ${String(days)} days`;
|
|
24427
|
-
console.log(
|
|
25557
|
+
console.log(import_chalk27.default.dim(" " + rangeLabel));
|
|
24428
25558
|
console.log("");
|
|
24429
|
-
process.stdout.write(
|
|
25559
|
+
process.stdout.write(import_chalk27.default.dim(" Loading\u2026"));
|
|
24430
25560
|
const summaries = buildSessions(days);
|
|
24431
25561
|
if (process.stdout.isTTY) {
|
|
24432
25562
|
process.stdout.clearLine(0);
|
|
@@ -24439,8 +25569,8 @@ function registerSessionsCommand(program2) {
|
|
|
24439
25569
|
(s) => s.sessionId === options.detail || s.sessionId.startsWith(options.detail)
|
|
24440
25570
|
);
|
|
24441
25571
|
if (!target) {
|
|
24442
|
-
console.log(
|
|
24443
|
-
console.log(
|
|
25572
|
+
console.log(import_chalk27.default.red(` Session not found: ${options.detail}`));
|
|
25573
|
+
console.log(import_chalk27.default.dim(" Run `node9 sessions` to list recent sessions.\n"));
|
|
24444
25574
|
return;
|
|
24445
25575
|
}
|
|
24446
25576
|
renderDetail(target);
|
|
@@ -24453,13 +25583,13 @@ function registerSessionsCommand(program2) {
|
|
|
24453
25583
|
}
|
|
24454
25584
|
|
|
24455
25585
|
// src/cli/commands/skill-pin.ts
|
|
24456
|
-
var
|
|
24457
|
-
var
|
|
24458
|
-
var
|
|
24459
|
-
var
|
|
25586
|
+
var import_chalk28 = __toESM(require("chalk"));
|
|
25587
|
+
var import_fs52 = __toESM(require("fs"));
|
|
25588
|
+
var import_os47 = __toESM(require("os"));
|
|
25589
|
+
var import_path51 = __toESM(require("path"));
|
|
24460
25590
|
function wipeSkillSessions() {
|
|
24461
25591
|
try {
|
|
24462
|
-
|
|
25592
|
+
import_fs52.default.rmSync(import_path51.default.join(import_os47.default.homedir(), ".node9", "skill-sessions"), {
|
|
24463
25593
|
recursive: true,
|
|
24464
25594
|
force: true
|
|
24465
25595
|
});
|
|
@@ -24473,29 +25603,29 @@ function registerSkillPinCommand(program2) {
|
|
|
24473
25603
|
const result = readSkillPinsSafe();
|
|
24474
25604
|
if (!result.ok) {
|
|
24475
25605
|
if (result.reason === "missing") {
|
|
24476
|
-
console.log(
|
|
25606
|
+
console.log(import_chalk28.default.gray("\nNo skill roots are pinned yet."));
|
|
24477
25607
|
console.log(
|
|
24478
|
-
|
|
25608
|
+
import_chalk28.default.gray("Pins are created automatically on the first tool call of each session.\n")
|
|
24479
25609
|
);
|
|
24480
25610
|
return;
|
|
24481
25611
|
}
|
|
24482
|
-
console.error(
|
|
25612
|
+
console.error(import_chalk28.default.red(`
|
|
24483
25613
|
\u274C Pin file is corrupt: ${result.detail}`));
|
|
24484
|
-
console.error(
|
|
25614
|
+
console.error(import_chalk28.default.yellow(" Run: node9 skill pin reset\n"));
|
|
24485
25615
|
process.exit(1);
|
|
24486
25616
|
}
|
|
24487
25617
|
const entries = Object.entries(result.pins.roots);
|
|
24488
25618
|
if (entries.length === 0) {
|
|
24489
|
-
console.log(
|
|
25619
|
+
console.log(import_chalk28.default.gray("\nNo skill roots are pinned yet.\n"));
|
|
24490
25620
|
return;
|
|
24491
25621
|
}
|
|
24492
|
-
console.log(
|
|
25622
|
+
console.log(import_chalk28.default.bold("\n\u{1F512} Pinned Skill Roots\n"));
|
|
24493
25623
|
for (const [key, entry] of entries) {
|
|
24494
|
-
const missing = entry.exists ? "" :
|
|
24495
|
-
console.log(` ${
|
|
25624
|
+
const missing = entry.exists ? "" : import_chalk28.default.yellow(" (not present at pin time)");
|
|
25625
|
+
console.log(` ${import_chalk28.default.cyan(key)} ${import_chalk28.default.gray(entry.rootPath)}${missing}`);
|
|
24496
25626
|
console.log(` Files (${entry.fileCount})`);
|
|
24497
|
-
console.log(` Hash: ${
|
|
24498
|
-
console.log(` Pinned: ${
|
|
25627
|
+
console.log(` Hash: ${import_chalk28.default.gray(entry.contentHash.slice(0, 16))}...`);
|
|
25628
|
+
console.log(` Pinned: ${import_chalk28.default.gray(entry.pinnedAt)}
|
|
24499
25629
|
`);
|
|
24500
25630
|
}
|
|
24501
25631
|
});
|
|
@@ -24504,52 +25634,52 @@ function registerSkillPinCommand(program2) {
|
|
|
24504
25634
|
try {
|
|
24505
25635
|
pins = readSkillPins();
|
|
24506
25636
|
} catch {
|
|
24507
|
-
console.error(
|
|
24508
|
-
console.error(
|
|
25637
|
+
console.error(import_chalk28.default.red("\n\u274C Pin file is corrupt."));
|
|
25638
|
+
console.error(import_chalk28.default.yellow(" Run: node9 skill pin reset\n"));
|
|
24509
25639
|
process.exit(1);
|
|
24510
25640
|
}
|
|
24511
25641
|
if (!pins.roots[rootKey]) {
|
|
24512
|
-
console.error(
|
|
25642
|
+
console.error(import_chalk28.default.red(`
|
|
24513
25643
|
\u274C No pin found for root key "${rootKey}"
|
|
24514
25644
|
`));
|
|
24515
|
-
console.error(`Run ${
|
|
25645
|
+
console.error(`Run ${import_chalk28.default.cyan("node9 skill pin list")} to see pinned roots.
|
|
24516
25646
|
`);
|
|
24517
25647
|
process.exit(1);
|
|
24518
25648
|
}
|
|
24519
25649
|
const rootPath = pins.roots[rootKey].rootPath;
|
|
24520
25650
|
removePin2(rootKey);
|
|
24521
25651
|
wipeSkillSessions();
|
|
24522
|
-
console.log(
|
|
24523
|
-
\u{1F513} Pin removed for ${
|
|
24524
|
-
console.log(
|
|
24525
|
-
console.log(
|
|
25652
|
+
console.log(import_chalk28.default.green(`
|
|
25653
|
+
\u{1F513} Pin removed for ${import_chalk28.default.cyan(rootKey)}`));
|
|
25654
|
+
console.log(import_chalk28.default.gray(` ${rootPath}`));
|
|
25655
|
+
console.log(import_chalk28.default.gray(" Next session will re-pin with current state.\n"));
|
|
24526
25656
|
});
|
|
24527
25657
|
pinSubCmd.command("reset").description("Clear all skill pins and wipe session verification flags").action(() => {
|
|
24528
25658
|
const result = readSkillPinsSafe();
|
|
24529
25659
|
if (!result.ok && result.reason === "missing") {
|
|
24530
25660
|
wipeSkillSessions();
|
|
24531
|
-
console.log(
|
|
25661
|
+
console.log(import_chalk28.default.gray("\nNo pins to clear.\n"));
|
|
24532
25662
|
return;
|
|
24533
25663
|
}
|
|
24534
25664
|
const count = result.ok ? Object.keys(result.pins.roots).length : "?";
|
|
24535
25665
|
clearAllPins2();
|
|
24536
25666
|
wipeSkillSessions();
|
|
24537
|
-
console.log(
|
|
25667
|
+
console.log(import_chalk28.default.green(`
|
|
24538
25668
|
\u{1F513} Cleared ${count} skill pin(s).`));
|
|
24539
|
-
console.log(
|
|
25669
|
+
console.log(import_chalk28.default.gray(" Next session will re-pin with current state.\n"));
|
|
24540
25670
|
});
|
|
24541
25671
|
}
|
|
24542
25672
|
|
|
24543
25673
|
// src/cli/commands/decisions.ts
|
|
24544
|
-
var
|
|
24545
|
-
var
|
|
24546
|
-
var
|
|
24547
|
-
var
|
|
24548
|
-
var DECISIONS_FILE2 =
|
|
25674
|
+
var import_fs53 = __toESM(require("fs"));
|
|
25675
|
+
var import_os48 = __toESM(require("os"));
|
|
25676
|
+
var import_path52 = __toESM(require("path"));
|
|
25677
|
+
var import_chalk29 = __toESM(require("chalk"));
|
|
25678
|
+
var DECISIONS_FILE2 = import_path52.default.join(import_os48.default.homedir(), ".node9", "decisions.json");
|
|
24549
25679
|
function readDecisions() {
|
|
24550
25680
|
try {
|
|
24551
|
-
if (!
|
|
24552
|
-
const raw =
|
|
25681
|
+
if (!import_fs53.default.existsSync(DECISIONS_FILE2)) return {};
|
|
25682
|
+
const raw = import_fs53.default.readFileSync(DECISIONS_FILE2, "utf-8");
|
|
24553
25683
|
const parsed = JSON.parse(raw);
|
|
24554
25684
|
const out = {};
|
|
24555
25685
|
for (const [k, v] of Object.entries(parsed)) {
|
|
@@ -24561,11 +25691,11 @@ function readDecisions() {
|
|
|
24561
25691
|
}
|
|
24562
25692
|
}
|
|
24563
25693
|
function writeDecisions(d) {
|
|
24564
|
-
const dir =
|
|
24565
|
-
if (!
|
|
25694
|
+
const dir = import_path52.default.dirname(DECISIONS_FILE2);
|
|
25695
|
+
if (!import_fs53.default.existsSync(dir)) import_fs53.default.mkdirSync(dir, { recursive: true });
|
|
24566
25696
|
const tmp = `${DECISIONS_FILE2}.${process.pid}.tmp`;
|
|
24567
|
-
|
|
24568
|
-
|
|
25697
|
+
import_fs53.default.writeFileSync(tmp, JSON.stringify(d, null, 2));
|
|
25698
|
+
import_fs53.default.renameSync(tmp, DECISIONS_FILE2);
|
|
24569
25699
|
}
|
|
24570
25700
|
function registerDecisionsCommand(program2) {
|
|
24571
25701
|
const cmd = program2.command("decisions").description('Manage persistent "Always Allow" / "Always Deny" tool decisions');
|
|
@@ -24573,67 +25703,67 @@ function registerDecisionsCommand(program2) {
|
|
|
24573
25703
|
const decisions = readDecisions();
|
|
24574
25704
|
const entries = Object.entries(decisions);
|
|
24575
25705
|
if (entries.length === 0) {
|
|
24576
|
-
console.log(
|
|
25706
|
+
console.log(import_chalk29.default.gray(" No persistent decisions stored."));
|
|
24577
25707
|
console.log(
|
|
24578
|
-
|
|
24579
|
-
`) +
|
|
25708
|
+
import_chalk29.default.gray(` File: ${DECISIONS_FILE2}
|
|
25709
|
+
`) + import_chalk29.default.gray(' Decisions are written when you click "Always Allow" or')
|
|
24580
25710
|
);
|
|
24581
|
-
console.log(
|
|
25711
|
+
console.log(import_chalk29.default.gray(' "Always Deny" in node9 tail or the native popup.'));
|
|
24582
25712
|
return;
|
|
24583
25713
|
}
|
|
24584
|
-
console.log(
|
|
25714
|
+
console.log(import_chalk29.default.bold(`
|
|
24585
25715
|
Persistent decisions (${entries.length})
|
|
24586
25716
|
`));
|
|
24587
25717
|
const w = Math.max(...entries.map(([k]) => k.length));
|
|
24588
25718
|
for (const [tool, verdict] of entries.sort()) {
|
|
24589
|
-
const colored = verdict === "allow" ?
|
|
25719
|
+
const colored = verdict === "allow" ? import_chalk29.default.green(verdict) : import_chalk29.default.red(verdict);
|
|
24590
25720
|
console.log(` ${tool.padEnd(w)} ${colored}`);
|
|
24591
25721
|
}
|
|
24592
25722
|
console.log(
|
|
24593
|
-
|
|
25723
|
+
import_chalk29.default.gray(`
|
|
24594
25724
|
Stored in ${DECISIONS_FILE2}
|
|
24595
|
-
`) +
|
|
25725
|
+
`) + import_chalk29.default.gray(" Run `node9 decisions clear <tool>` to remove an entry.")
|
|
24596
25726
|
);
|
|
24597
25727
|
});
|
|
24598
25728
|
cmd.command("clear <toolName>").description("Remove a persistent decision for one tool").action((toolName) => {
|
|
24599
25729
|
const decisions = readDecisions();
|
|
24600
25730
|
if (!(toolName in decisions)) {
|
|
24601
|
-
console.log(
|
|
25731
|
+
console.log(import_chalk29.default.yellow(` No persistent decision for "${toolName}". Nothing to clear.`));
|
|
24602
25732
|
process.exitCode = 1;
|
|
24603
25733
|
return;
|
|
24604
25734
|
}
|
|
24605
25735
|
delete decisions[toolName];
|
|
24606
25736
|
writeDecisions(decisions);
|
|
24607
|
-
console.log(
|
|
25737
|
+
console.log(import_chalk29.default.green(` \u2713 Cleared persistent decision for "${toolName}".`));
|
|
24608
25738
|
});
|
|
24609
25739
|
cmd.command("clear-all").description("Remove every persistent decision (irreversible)").action(() => {
|
|
24610
25740
|
const decisions = readDecisions();
|
|
24611
25741
|
const count = Object.keys(decisions).length;
|
|
24612
25742
|
if (count === 0) {
|
|
24613
|
-
console.log(
|
|
25743
|
+
console.log(import_chalk29.default.gray(" Nothing to clear \u2014 no persistent decisions stored."));
|
|
24614
25744
|
return;
|
|
24615
25745
|
}
|
|
24616
25746
|
writeDecisions({});
|
|
24617
25747
|
console.log(
|
|
24618
|
-
|
|
25748
|
+
import_chalk29.default.green(` \u2713 Cleared ${count} persistent decision${count === 1 ? "" : "s"}.`)
|
|
24619
25749
|
);
|
|
24620
25750
|
});
|
|
24621
25751
|
}
|
|
24622
25752
|
|
|
24623
25753
|
// src/cli/commands/dlp.ts
|
|
24624
|
-
var
|
|
24625
|
-
var
|
|
24626
|
-
var
|
|
24627
|
-
var
|
|
24628
|
-
var AUDIT_LOG =
|
|
24629
|
-
var RESOLVED_FILE =
|
|
25754
|
+
var import_chalk30 = __toESM(require("chalk"));
|
|
25755
|
+
var import_fs54 = __toESM(require("fs"));
|
|
25756
|
+
var import_path53 = __toESM(require("path"));
|
|
25757
|
+
var import_os49 = __toESM(require("os"));
|
|
25758
|
+
var AUDIT_LOG = import_path53.default.join(import_os49.default.homedir(), ".node9", "audit.log");
|
|
25759
|
+
var RESOLVED_FILE = import_path53.default.join(import_os49.default.homedir(), ".node9", "dlp-resolved.json");
|
|
24630
25760
|
var ANSI_RE = /\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g;
|
|
24631
25761
|
function stripAnsi(s) {
|
|
24632
25762
|
return s.replace(ANSI_RE, "");
|
|
24633
25763
|
}
|
|
24634
25764
|
function loadResolved() {
|
|
24635
25765
|
try {
|
|
24636
|
-
const raw = JSON.parse(
|
|
25766
|
+
const raw = JSON.parse(import_fs54.default.readFileSync(RESOLVED_FILE, "utf-8"));
|
|
24637
25767
|
return new Set(raw);
|
|
24638
25768
|
} catch {
|
|
24639
25769
|
return /* @__PURE__ */ new Set();
|
|
@@ -24641,13 +25771,13 @@ function loadResolved() {
|
|
|
24641
25771
|
}
|
|
24642
25772
|
function saveResolved(resolved) {
|
|
24643
25773
|
try {
|
|
24644
|
-
|
|
25774
|
+
import_fs54.default.writeFileSync(RESOLVED_FILE, JSON.stringify([...resolved], null, 2), { mode: 384 });
|
|
24645
25775
|
} catch {
|
|
24646
25776
|
}
|
|
24647
25777
|
}
|
|
24648
25778
|
function loadDlpFindings() {
|
|
24649
|
-
if (!
|
|
24650
|
-
return
|
|
25779
|
+
if (!import_fs54.default.existsSync(AUDIT_LOG)) return [];
|
|
25780
|
+
return import_fs54.default.readFileSync(AUDIT_LOG, "utf-8").split("\n").flatMap((line) => {
|
|
24651
25781
|
if (!line.trim()) return [];
|
|
24652
25782
|
try {
|
|
24653
25783
|
const e = JSON.parse(line);
|
|
@@ -24676,14 +25806,14 @@ function registerDlpCommand(program2) {
|
|
|
24676
25806
|
cmd.command("resolve").description("Mark all current DLP findings as resolved").action(() => {
|
|
24677
25807
|
const findings = loadDlpFindings();
|
|
24678
25808
|
if (findings.length === 0) {
|
|
24679
|
-
console.log(
|
|
25809
|
+
console.log(import_chalk30.default.green("\n \u2705 No response-DLP findings to resolve.\n"));
|
|
24680
25810
|
return;
|
|
24681
25811
|
}
|
|
24682
25812
|
const resolved = loadResolved();
|
|
24683
25813
|
for (const e of findings) resolved.add(entryKey2(e));
|
|
24684
25814
|
saveResolved(resolved);
|
|
24685
25815
|
console.log(
|
|
24686
|
-
|
|
25816
|
+
import_chalk30.default.green(
|
|
24687
25817
|
`
|
|
24688
25818
|
\u2705 ${findings.length} finding${findings.length !== 1 ? "s" : ""} marked as resolved.
|
|
24689
25819
|
`
|
|
@@ -24697,63 +25827,63 @@ function registerDlpCommand(program2) {
|
|
|
24697
25827
|
const resolvedCount = findings.length - open.length;
|
|
24698
25828
|
console.log("");
|
|
24699
25829
|
console.log(
|
|
24700
|
-
|
|
25830
|
+
import_chalk30.default.bold.cyan("\u{1F510} node9 dlp") + import_chalk30.default.dim(" \u2014 secrets found in Claude response text")
|
|
24701
25831
|
);
|
|
24702
25832
|
console.log("");
|
|
24703
25833
|
if (open.length === 0) {
|
|
24704
25834
|
if (resolvedCount > 0) {
|
|
24705
|
-
console.log(
|
|
25835
|
+
console.log(import_chalk30.default.green(` \u2705 No open findings \xB7 ${resolvedCount} previously resolved`));
|
|
24706
25836
|
} else {
|
|
24707
25837
|
console.log(
|
|
24708
|
-
|
|
25838
|
+
import_chalk30.default.green(" \u2705 No findings \u2014 Claude has not leaked secrets in response text")
|
|
24709
25839
|
);
|
|
24710
25840
|
}
|
|
24711
25841
|
console.log("");
|
|
24712
25842
|
return;
|
|
24713
25843
|
}
|
|
24714
25844
|
console.log(
|
|
24715
|
-
|
|
25845
|
+
import_chalk30.default.bgRed.white.bold(` \u26A0\uFE0F ${open.length} open finding${open.length !== 1 ? "s" : ""} `) + import_chalk30.default.dim(resolvedCount > 0 ? ` (${resolvedCount} resolved)` : "")
|
|
24716
25846
|
);
|
|
24717
25847
|
console.log("");
|
|
24718
25848
|
console.log(
|
|
24719
|
-
|
|
25849
|
+
import_chalk30.default.dim(" These secrets were included in Claude's response text \u2014 NOT blocked.")
|
|
24720
25850
|
);
|
|
24721
|
-
console.log(
|
|
25851
|
+
console.log(import_chalk30.default.dim(" Rotate each affected key immediately.\n"));
|
|
24722
25852
|
for (const e of open) {
|
|
24723
25853
|
console.log(
|
|
24724
|
-
" " +
|
|
25854
|
+
" " + import_chalk30.default.red("\u25CF") + " " + import_chalk30.default.white(e.dlpPattern ?? "Secret") + import_chalk30.default.dim(" " + fmtDate3(e.ts))
|
|
24725
25855
|
);
|
|
24726
25856
|
if (e.dlpSample) {
|
|
24727
|
-
console.log(" " +
|
|
25857
|
+
console.log(" " + import_chalk30.default.dim("Sample: ") + import_chalk30.default.yellow(stripAnsi(e.dlpSample)));
|
|
24728
25858
|
}
|
|
24729
25859
|
if (e.project) {
|
|
24730
|
-
console.log(" " +
|
|
25860
|
+
console.log(" " + import_chalk30.default.dim("Project: ") + import_chalk30.default.dim(stripAnsi(e.project)));
|
|
24731
25861
|
}
|
|
24732
25862
|
console.log("");
|
|
24733
25863
|
}
|
|
24734
|
-
console.log(" " +
|
|
24735
|
-
console.log(" " +
|
|
25864
|
+
console.log(" " + import_chalk30.default.bold("Next steps:"));
|
|
25865
|
+
console.log(" " + import_chalk30.default.cyan("1.") + " Rotate any exposed keys shown above");
|
|
24736
25866
|
console.log(
|
|
24737
|
-
" " +
|
|
25867
|
+
" " + import_chalk30.default.cyan("2.") + " Run " + import_chalk30.default.white("node9 dlp resolve") + " to acknowledge"
|
|
24738
25868
|
);
|
|
24739
25869
|
console.log(
|
|
24740
|
-
" " +
|
|
25870
|
+
" " + import_chalk30.default.cyan("3.") + " Run " + import_chalk30.default.white("node9 report") + " for full audit history"
|
|
24741
25871
|
);
|
|
24742
25872
|
console.log("");
|
|
24743
25873
|
});
|
|
24744
25874
|
}
|
|
24745
25875
|
|
|
24746
25876
|
// src/cli/commands/mask.ts
|
|
24747
|
-
var
|
|
24748
|
-
var
|
|
24749
|
-
var
|
|
24750
|
-
var
|
|
25877
|
+
var import_chalk31 = __toESM(require("chalk"));
|
|
25878
|
+
var import_fs55 = __toESM(require("fs"));
|
|
25879
|
+
var import_path54 = __toESM(require("path"));
|
|
25880
|
+
var import_os50 = __toESM(require("os"));
|
|
24751
25881
|
init_dlp();
|
|
24752
25882
|
function findJsonlFiles(dir) {
|
|
24753
25883
|
const results = [];
|
|
24754
|
-
if (!
|
|
24755
|
-
for (const entry of
|
|
24756
|
-
const full =
|
|
25884
|
+
if (!import_fs55.default.existsSync(dir)) return results;
|
|
25885
|
+
for (const entry of import_fs55.default.readdirSync(dir, { withFileTypes: true })) {
|
|
25886
|
+
const full = import_path54.default.join(dir, entry.name);
|
|
24757
25887
|
if (entry.isDirectory()) results.push(...findJsonlFiles(full));
|
|
24758
25888
|
else if (entry.isFile() && entry.name.endsWith(".jsonl")) results.push(full);
|
|
24759
25889
|
}
|
|
@@ -24796,7 +25926,7 @@ function redactJson(obj) {
|
|
|
24796
25926
|
function processFile(filePath, dryRun) {
|
|
24797
25927
|
let raw;
|
|
24798
25928
|
try {
|
|
24799
|
-
raw =
|
|
25929
|
+
raw = import_fs55.default.readFileSync(filePath, "utf-8");
|
|
24800
25930
|
} catch {
|
|
24801
25931
|
return { redactedLines: 0, patterns: [] };
|
|
24802
25932
|
}
|
|
@@ -24828,14 +25958,14 @@ function processFile(filePath, dryRun) {
|
|
|
24828
25958
|
}
|
|
24829
25959
|
}
|
|
24830
25960
|
if (!dryRun && redactedLines > 0) {
|
|
24831
|
-
|
|
25961
|
+
import_fs55.default.writeFileSync(filePath, newLines.join("\n"), "utf-8");
|
|
24832
25962
|
}
|
|
24833
25963
|
return { redactedLines, patterns };
|
|
24834
25964
|
}
|
|
24835
25965
|
function processJsonFile(filePath, dryRun) {
|
|
24836
25966
|
let raw;
|
|
24837
25967
|
try {
|
|
24838
|
-
raw =
|
|
25968
|
+
raw = import_fs55.default.readFileSync(filePath, "utf-8");
|
|
24839
25969
|
} catch {
|
|
24840
25970
|
return { redactedLines: 0, patterns: [] };
|
|
24841
25971
|
}
|
|
@@ -24848,15 +25978,15 @@ function processJsonFile(filePath, dryRun) {
|
|
|
24848
25978
|
const { value, modified, found } = redactJson(parsed);
|
|
24849
25979
|
if (!modified) return { redactedLines: 0, patterns: [] };
|
|
24850
25980
|
if (!dryRun) {
|
|
24851
|
-
|
|
25981
|
+
import_fs55.default.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
|
|
24852
25982
|
}
|
|
24853
25983
|
return { redactedLines: 1, patterns: found };
|
|
24854
25984
|
}
|
|
24855
25985
|
function findJsonFiles(dir) {
|
|
24856
25986
|
const results = [];
|
|
24857
|
-
if (!
|
|
24858
|
-
for (const entry of
|
|
24859
|
-
const full =
|
|
25987
|
+
if (!import_fs55.default.existsSync(dir)) return results;
|
|
25988
|
+
for (const entry of import_fs55.default.readdirSync(dir, { withFileTypes: true })) {
|
|
25989
|
+
const full = import_path54.default.join(dir, entry.name);
|
|
24860
25990
|
if (entry.isDirectory()) results.push(...findJsonFiles(full));
|
|
24861
25991
|
else if (entry.isFile() && entry.name.endsWith(".json")) results.push(full);
|
|
24862
25992
|
}
|
|
@@ -24865,9 +25995,9 @@ function findJsonFiles(dir) {
|
|
|
24865
25995
|
function registerMaskCommand(program2) {
|
|
24866
25996
|
program2.command("mask").description("Redact plaintext secrets from local AI session history files").option("--dry-run", "show what would be redacted without making changes").option("--all", "scan all history (default: last 30 days)").action(async (options) => {
|
|
24867
25997
|
const dryRun = !!options.dryRun;
|
|
24868
|
-
const home =
|
|
24869
|
-
const claudeDir =
|
|
24870
|
-
const geminiDir =
|
|
25998
|
+
const home = import_os50.default.homedir();
|
|
25999
|
+
const claudeDir = import_path54.default.join(home, ".claude", "projects");
|
|
26000
|
+
const geminiDir = import_path54.default.join(home, ".gemini", "tmp");
|
|
24871
26001
|
const allFiles = [
|
|
24872
26002
|
...findJsonlFiles(claudeDir).map((p) => ({ path: p, type: "jsonl" })),
|
|
24873
26003
|
...findJsonFiles(geminiDir).map((p) => ({ path: p, type: "json" }))
|
|
@@ -24875,18 +26005,18 @@ function registerMaskCommand(program2) {
|
|
|
24875
26005
|
const cutoff = options.all ? null : new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3);
|
|
24876
26006
|
const filtered = cutoff ? allFiles.filter((f) => {
|
|
24877
26007
|
try {
|
|
24878
|
-
return
|
|
26008
|
+
return import_fs55.default.statSync(f.path).mtime >= cutoff;
|
|
24879
26009
|
} catch {
|
|
24880
26010
|
return false;
|
|
24881
26011
|
}
|
|
24882
26012
|
}) : allFiles;
|
|
24883
26013
|
if (filtered.length === 0) {
|
|
24884
|
-
console.log(
|
|
26014
|
+
console.log(import_chalk31.default.yellow(" No session files found."));
|
|
24885
26015
|
return;
|
|
24886
26016
|
}
|
|
24887
26017
|
console.log("");
|
|
24888
26018
|
if (dryRun) {
|
|
24889
|
-
console.log(
|
|
26019
|
+
console.log(import_chalk31.default.dim(" Dry run \u2014 no files will be modified.\n"));
|
|
24890
26020
|
}
|
|
24891
26021
|
let totalFiles = 0;
|
|
24892
26022
|
let totalLines = 0;
|
|
@@ -24902,23 +26032,23 @@ function registerMaskCommand(program2) {
|
|
|
24902
26032
|
});
|
|
24903
26033
|
const verb = dryRun ? "Would redact" : "Redacted";
|
|
24904
26034
|
console.log(
|
|
24905
|
-
" " +
|
|
26035
|
+
" " + import_chalk31.default.dim(shortPath.slice(0, 60).padEnd(62)) + import_chalk31.default.red(`${verb}: `) + import_chalk31.default.yellow(patterns.join(", ")) + import_chalk31.default.dim(` (${redactedLines} line${redactedLines !== 1 ? "s" : ""})`)
|
|
24906
26036
|
);
|
|
24907
26037
|
}
|
|
24908
26038
|
}
|
|
24909
26039
|
console.log("");
|
|
24910
26040
|
if (totalFiles === 0) {
|
|
24911
|
-
console.log(
|
|
26041
|
+
console.log(import_chalk31.default.green(" No secrets found in session history."));
|
|
24912
26042
|
} else {
|
|
24913
26043
|
const verb = dryRun ? "would be modified" : "modified";
|
|
24914
26044
|
console.log(
|
|
24915
|
-
|
|
26045
|
+
import_chalk31.default.bold(` ${totalFiles} file${totalFiles !== 1 ? "s" : ""} ${verb}`) + import_chalk31.default.dim(`, ${totalLines} line${totalLines !== 1 ? "s" : ""} redacted`)
|
|
24916
26046
|
);
|
|
24917
|
-
console.log(" Patterns: " +
|
|
26047
|
+
console.log(" Patterns: " + import_chalk31.default.yellow(totalPatterns.join(", ")));
|
|
24918
26048
|
if (!dryRun) {
|
|
24919
26049
|
console.log("");
|
|
24920
26050
|
console.log(
|
|
24921
|
-
|
|
26051
|
+
import_chalk31.default.dim(
|
|
24922
26052
|
" Note: secrets were already sent to the AI provider during the active session.\n This cleans your local disk only. Rotate any exposed keys."
|
|
24923
26053
|
)
|
|
24924
26054
|
);
|
|
@@ -24931,20 +26061,20 @@ function registerMaskCommand(program2) {
|
|
|
24931
26061
|
// src/cli.ts
|
|
24932
26062
|
init_blast();
|
|
24933
26063
|
var { version } = JSON.parse(
|
|
24934
|
-
|
|
26064
|
+
import_fs58.default.readFileSync(import_path57.default.join(__dirname, "../package.json"), "utf-8")
|
|
24935
26065
|
);
|
|
24936
26066
|
var program = new import_commander.Command();
|
|
24937
26067
|
program.name("node9").description("The Sudo Command for AI Agents").version(version);
|
|
24938
26068
|
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) => {
|
|
24939
26069
|
const DEFAULT_API_URL2 = "https://api.node9.ai/api/v1/intercept";
|
|
24940
|
-
const credPath =
|
|
24941
|
-
if (!
|
|
24942
|
-
|
|
26070
|
+
const credPath = import_path57.default.join(import_os53.default.homedir(), ".node9", "credentials.json");
|
|
26071
|
+
if (!import_fs58.default.existsSync(import_path57.default.dirname(credPath)))
|
|
26072
|
+
import_fs58.default.mkdirSync(import_path57.default.dirname(credPath), { recursive: true });
|
|
24943
26073
|
const profileName = options.profile || "default";
|
|
24944
26074
|
let existingCreds = {};
|
|
24945
26075
|
try {
|
|
24946
|
-
if (
|
|
24947
|
-
const raw = JSON.parse(
|
|
26076
|
+
if (import_fs58.default.existsSync(credPath)) {
|
|
26077
|
+
const raw = JSON.parse(import_fs58.default.readFileSync(credPath, "utf-8"));
|
|
24948
26078
|
if (raw.apiKey) {
|
|
24949
26079
|
existingCreds = {
|
|
24950
26080
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL2 }
|
|
@@ -24956,14 +26086,14 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
24956
26086
|
} catch {
|
|
24957
26087
|
}
|
|
24958
26088
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL2 };
|
|
24959
|
-
|
|
26089
|
+
import_fs58.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
24960
26090
|
let effectiveCloud = null;
|
|
24961
26091
|
if (profileName === "default") {
|
|
24962
|
-
const
|
|
26092
|
+
const configPath2 = import_path57.default.join(import_os53.default.homedir(), ".node9", "config.json");
|
|
24963
26093
|
let config = {};
|
|
24964
26094
|
try {
|
|
24965
|
-
if (
|
|
24966
|
-
config = JSON.parse(
|
|
26095
|
+
if (import_fs58.default.existsSync(configPath2))
|
|
26096
|
+
config = JSON.parse(import_fs58.default.readFileSync(configPath2, "utf-8"));
|
|
24967
26097
|
} catch {
|
|
24968
26098
|
}
|
|
24969
26099
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -24978,28 +26108,28 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
24978
26108
|
approvers.cloud = false;
|
|
24979
26109
|
}
|
|
24980
26110
|
s.approvers = approvers;
|
|
24981
|
-
if (!
|
|
24982
|
-
|
|
24983
|
-
|
|
26111
|
+
if (!import_fs58.default.existsSync(import_path57.default.dirname(configPath2)))
|
|
26112
|
+
import_fs58.default.mkdirSync(import_path57.default.dirname(configPath2), { recursive: true });
|
|
26113
|
+
import_fs58.default.writeFileSync(configPath2, JSON.stringify(config, null, 2), { mode: 384 });
|
|
24984
26114
|
effectiveCloud = approvers.cloud === true;
|
|
24985
26115
|
}
|
|
24986
26116
|
if (options.profile && profileName !== "default") {
|
|
24987
|
-
console.log(
|
|
24988
|
-
console.log(
|
|
26117
|
+
console.log(import_chalk33.default.green(`\u2705 Profile "${profileName}" saved`));
|
|
26118
|
+
console.log(import_chalk33.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
24989
26119
|
} else if (options.local || effectiveCloud === false) {
|
|
24990
|
-
console.log(
|
|
24991
|
-
console.log(
|
|
26120
|
+
console.log(import_chalk33.default.green(`\u2705 Key saved \u2014 Privacy mode \u{1F6E1}\uFE0F`));
|
|
26121
|
+
console.log(import_chalk33.default.gray(` All decisions stay on this machine. Nothing syncs to the cloud.`));
|
|
24992
26122
|
if (!options.local) {
|
|
24993
26123
|
console.log(
|
|
24994
|
-
|
|
26124
|
+
import_chalk33.default.yellow(` Your config has cloud approvals OFF (settings.approvers.cloud).`)
|
|
24995
26125
|
);
|
|
24996
26126
|
console.log(
|
|
24997
|
-
|
|
26127
|
+
import_chalk33.default.gray(` To enable team policy + dashboard sync: set it to true, or re-init.`)
|
|
24998
26128
|
);
|
|
24999
26129
|
}
|
|
25000
26130
|
} else {
|
|
25001
|
-
console.log(
|
|
25002
|
-
console.log(
|
|
26131
|
+
console.log(import_chalk33.default.green(`\u2705 Logged in \u2014 agent mode`));
|
|
26132
|
+
console.log(import_chalk33.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
25003
26133
|
}
|
|
25004
26134
|
});
|
|
25005
26135
|
program.command("addto", { hidden: true }).description("Integrate Node9 with an AI agent").addHelpText(
|
|
@@ -25020,7 +26150,7 @@ program.command("addto", { hidden: true }).description("Integrate Node9 with an
|
|
|
25020
26150
|
if (target === "hermes") return setupHermes();
|
|
25021
26151
|
if (target === "hud") return setupHud();
|
|
25022
26152
|
console.error(
|
|
25023
|
-
|
|
26153
|
+
import_chalk33.default.red(
|
|
25024
26154
|
`Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
|
|
25025
26155
|
)
|
|
25026
26156
|
);
|
|
@@ -25034,20 +26164,20 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
|
|
|
25034
26164
|
"The agent to protect: claude | antigravity | copilot | gemini | cursor | codex | windsurf | vscode | hud"
|
|
25035
26165
|
).action(async (target) => {
|
|
25036
26166
|
if (!target) {
|
|
25037
|
-
console.log(
|
|
25038
|
-
console.log(" Usage: " +
|
|
26167
|
+
console.log(import_chalk33.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
26168
|
+
console.log(" Usage: " + import_chalk33.default.white("node9 setup <target>") + "\n");
|
|
25039
26169
|
console.log(" Targets:");
|
|
25040
|
-
console.log(" " +
|
|
25041
|
-
console.log(" " +
|
|
25042
|
-
console.log(" " +
|
|
25043
|
-
console.log(" " +
|
|
25044
|
-
console.log(" " +
|
|
25045
|
-
console.log(" " +
|
|
25046
|
-
console.log(" " +
|
|
25047
|
-
console.log(" " +
|
|
25048
|
-
console.log(" " +
|
|
26170
|
+
console.log(" " + import_chalk33.default.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
26171
|
+
console.log(" " + import_chalk33.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
26172
|
+
console.log(" " + import_chalk33.default.green("antigravity") + " \u2014 Antigravity / agy (hook mode)");
|
|
26173
|
+
console.log(" " + import_chalk33.default.green("copilot") + " \u2014 GitHub Copilot CLI (hook mode)");
|
|
26174
|
+
console.log(" " + import_chalk33.default.green("cursor") + " \u2014 Cursor (MCP proxy)");
|
|
26175
|
+
console.log(" " + import_chalk33.default.green("codex") + " \u2014 OpenAI Codex CLI (MCP proxy)");
|
|
26176
|
+
console.log(" " + import_chalk33.default.green("windsurf") + " \u2014 Windsurf (MCP proxy)");
|
|
26177
|
+
console.log(" " + import_chalk33.default.green("vscode") + " \u2014 VSCode / Copilot (MCP proxy)");
|
|
26178
|
+
console.log(" " + import_chalk33.default.green("hermes") + " \u2014 Hermes Agent (hook mode)");
|
|
25049
26179
|
process.stdout.write(
|
|
25050
|
-
" " +
|
|
26180
|
+
" " + import_chalk33.default.green("hud") + " \u2014 Claude Code security statusline\n"
|
|
25051
26181
|
);
|
|
25052
26182
|
console.log("");
|
|
25053
26183
|
return;
|
|
@@ -25064,7 +26194,7 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
|
|
|
25064
26194
|
if (t === "hermes") return setupHermes();
|
|
25065
26195
|
if (t === "hud") return setupHud();
|
|
25066
26196
|
console.error(
|
|
25067
|
-
|
|
26197
|
+
import_chalk33.default.red(
|
|
25068
26198
|
`Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
|
|
25069
26199
|
)
|
|
25070
26200
|
);
|
|
@@ -25090,35 +26220,35 @@ program.command("removefrom", { hidden: true }).description("Remove Node9 hooks
|
|
|
25090
26220
|
else if (target === "hud") fn = teardownHud;
|
|
25091
26221
|
else {
|
|
25092
26222
|
console.error(
|
|
25093
|
-
|
|
26223
|
+
import_chalk33.default.red(
|
|
25094
26224
|
`Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
|
|
25095
26225
|
)
|
|
25096
26226
|
);
|
|
25097
26227
|
process.exit(1);
|
|
25098
26228
|
}
|
|
25099
|
-
console.log(
|
|
26229
|
+
console.log(import_chalk33.default.cyan(`
|
|
25100
26230
|
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
25101
26231
|
`));
|
|
25102
26232
|
try {
|
|
25103
26233
|
fn();
|
|
25104
26234
|
} catch (err2) {
|
|
25105
|
-
console.error(
|
|
26235
|
+
console.error(import_chalk33.default.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
25106
26236
|
process.exit(1);
|
|
25107
26237
|
}
|
|
25108
|
-
console.log(
|
|
26238
|
+
console.log(import_chalk33.default.gray("\n Restart the agent for changes to take effect."));
|
|
25109
26239
|
});
|
|
25110
26240
|
program.command("uninstall").description("Remove all Node9 hooks and optionally delete config files").option("--purge", "Also delete ~/.node9/ directory (config, audit log, credentials)").action(async (options) => {
|
|
25111
|
-
console.log(
|
|
25112
|
-
console.log(
|
|
26241
|
+
console.log(import_chalk33.default.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
26242
|
+
console.log(import_chalk33.default.bold("Stopping daemon..."));
|
|
25113
26243
|
try {
|
|
25114
26244
|
stopDaemon();
|
|
25115
|
-
console.log(
|
|
26245
|
+
console.log(import_chalk33.default.green(" \u2705 Daemon stopped"));
|
|
25116
26246
|
} catch {
|
|
25117
|
-
console.log(
|
|
26247
|
+
console.log(import_chalk33.default.blue(" \u2139\uFE0F Daemon was not running"));
|
|
25118
26248
|
}
|
|
25119
|
-
console.log(
|
|
26249
|
+
console.log(import_chalk33.default.bold("\nRemoving hooks..."));
|
|
25120
26250
|
let teardownFailed = false;
|
|
25121
|
-
for (const [
|
|
26251
|
+
for (const [label2, fn] of [
|
|
25122
26252
|
["Claude", teardownClaude],
|
|
25123
26253
|
["Gemini", teardownGemini],
|
|
25124
26254
|
["Cursor", teardownCursor],
|
|
@@ -25132,45 +26262,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
25132
26262
|
} catch (err2) {
|
|
25133
26263
|
teardownFailed = true;
|
|
25134
26264
|
console.error(
|
|
25135
|
-
|
|
25136
|
-
` \u26A0\uFE0F Failed to remove ${
|
|
26265
|
+
import_chalk33.default.red(
|
|
26266
|
+
` \u26A0\uFE0F Failed to remove ${label2} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
25137
26267
|
)
|
|
25138
26268
|
);
|
|
25139
26269
|
}
|
|
25140
26270
|
}
|
|
25141
26271
|
if (options.purge) {
|
|
25142
|
-
const node9Dir =
|
|
25143
|
-
if (
|
|
26272
|
+
const node9Dir = import_path57.default.join(import_os53.default.homedir(), ".node9");
|
|
26273
|
+
if (import_fs58.default.existsSync(node9Dir)) {
|
|
25144
26274
|
const confirmed = await (0, import_prompts2.confirm)({
|
|
25145
26275
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
25146
26276
|
default: false
|
|
25147
26277
|
});
|
|
25148
26278
|
if (confirmed) {
|
|
25149
|
-
|
|
25150
|
-
if (
|
|
26279
|
+
import_fs58.default.rmSync(node9Dir, { recursive: true });
|
|
26280
|
+
if (import_fs58.default.existsSync(node9Dir)) {
|
|
25151
26281
|
console.error(
|
|
25152
|
-
|
|
26282
|
+
import_chalk33.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
25153
26283
|
);
|
|
25154
26284
|
} else {
|
|
25155
|
-
console.log(
|
|
26285
|
+
console.log(import_chalk33.default.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
25156
26286
|
}
|
|
25157
26287
|
} else {
|
|
25158
|
-
console.log(
|
|
26288
|
+
console.log(import_chalk33.default.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
25159
26289
|
}
|
|
25160
26290
|
} else {
|
|
25161
|
-
console.log(
|
|
26291
|
+
console.log(import_chalk33.default.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
25162
26292
|
}
|
|
25163
26293
|
} else {
|
|
25164
26294
|
console.log(
|
|
25165
|
-
|
|
26295
|
+
import_chalk33.default.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
25166
26296
|
);
|
|
25167
26297
|
}
|
|
25168
26298
|
if (teardownFailed) {
|
|
25169
|
-
console.error(
|
|
26299
|
+
console.error(import_chalk33.default.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
25170
26300
|
process.exit(1);
|
|
25171
26301
|
}
|
|
25172
|
-
console.log(
|
|
25173
|
-
console.log(
|
|
26302
|
+
console.log(import_chalk33.default.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g node9-ai"));
|
|
26303
|
+
console.log(import_chalk33.default.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
|
|
25174
26304
|
});
|
|
25175
26305
|
registerDoctorCommand(program, version);
|
|
25176
26306
|
program.command("explain").description(
|
|
@@ -25183,7 +26313,7 @@ program.command("explain").description(
|
|
|
25183
26313
|
try {
|
|
25184
26314
|
args = JSON.parse(trimmed);
|
|
25185
26315
|
} catch {
|
|
25186
|
-
console.error(
|
|
26316
|
+
console.error(import_chalk33.default.red(`
|
|
25187
26317
|
\u274C Invalid JSON: ${trimmed}
|
|
25188
26318
|
`));
|
|
25189
26319
|
process.exit(1);
|
|
@@ -25194,54 +26324,54 @@ program.command("explain").description(
|
|
|
25194
26324
|
}
|
|
25195
26325
|
const result = await explainPolicy(tool, args);
|
|
25196
26326
|
console.log("");
|
|
25197
|
-
console.log(
|
|
26327
|
+
console.log(import_chalk33.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
25198
26328
|
console.log("");
|
|
25199
|
-
console.log(` ${
|
|
26329
|
+
console.log(` ${import_chalk33.default.bold("Tool:")} ${import_chalk33.default.white(result.tool)}`);
|
|
25200
26330
|
if (argsRaw) {
|
|
25201
26331
|
const preview2 = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
25202
|
-
console.log(` ${
|
|
26332
|
+
console.log(` ${import_chalk33.default.bold("Input:")} ${import_chalk33.default.gray(preview2)}`);
|
|
25203
26333
|
}
|
|
25204
26334
|
console.log("");
|
|
25205
|
-
console.log(
|
|
26335
|
+
console.log(import_chalk33.default.bold("Config Sources (Waterfall):"));
|
|
25206
26336
|
for (const tier of result.waterfall) {
|
|
25207
|
-
const num3 =
|
|
25208
|
-
const
|
|
26337
|
+
const num3 = import_chalk33.default.gray(` ${tier.tier}.`);
|
|
26338
|
+
const label2 = tier.label.padEnd(16);
|
|
25209
26339
|
let statusStr;
|
|
25210
26340
|
if (tier.tier === 1) {
|
|
25211
|
-
statusStr =
|
|
26341
|
+
statusStr = import_chalk33.default.gray(tier.note ?? "");
|
|
25212
26342
|
} else if (tier.status === "active") {
|
|
25213
|
-
const loc = tier.path ?
|
|
25214
|
-
const note = tier.note ?
|
|
25215
|
-
statusStr =
|
|
26343
|
+
const loc = tier.path ? import_chalk33.default.gray(tier.path) : "";
|
|
26344
|
+
const note = tier.note ? import_chalk33.default.gray(`(${tier.note})`) : "";
|
|
26345
|
+
statusStr = import_chalk33.default.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
|
|
25216
26346
|
} else {
|
|
25217
|
-
statusStr =
|
|
26347
|
+
statusStr = import_chalk33.default.gray("\u25CB " + (tier.note ?? "not found"));
|
|
25218
26348
|
}
|
|
25219
|
-
console.log(`${num3} ${
|
|
26349
|
+
console.log(`${num3} ${import_chalk33.default.white(label2)} ${statusStr}`);
|
|
25220
26350
|
}
|
|
25221
26351
|
console.log("");
|
|
25222
|
-
console.log(
|
|
26352
|
+
console.log(import_chalk33.default.bold("Policy Evaluation:"));
|
|
25223
26353
|
for (const step of result.steps) {
|
|
25224
26354
|
const isFinal = step.isFinal;
|
|
25225
26355
|
let icon;
|
|
25226
|
-
if (step.outcome === "allow") icon =
|
|
25227
|
-
else if (step.outcome === "review") icon =
|
|
25228
|
-
else if (step.outcome === "skip") icon =
|
|
25229
|
-
else icon =
|
|
26356
|
+
if (step.outcome === "allow") icon = import_chalk33.default.green(" \u2705");
|
|
26357
|
+
else if (step.outcome === "review") icon = import_chalk33.default.red(" \u{1F534}");
|
|
26358
|
+
else if (step.outcome === "skip") icon = import_chalk33.default.gray(" \u2500 ");
|
|
26359
|
+
else icon = import_chalk33.default.gray(" \u25CB ");
|
|
25230
26360
|
const name = step.name.padEnd(18);
|
|
25231
|
-
const nameStr = isFinal ?
|
|
25232
|
-
const detail = isFinal ?
|
|
25233
|
-
const arrow = isFinal ?
|
|
26361
|
+
const nameStr = isFinal ? import_chalk33.default.white.bold(name) : import_chalk33.default.white(name);
|
|
26362
|
+
const detail = isFinal ? import_chalk33.default.white(step.detail) : import_chalk33.default.gray(step.detail);
|
|
26363
|
+
const arrow = isFinal ? import_chalk33.default.yellow(" \u2190 STOP") : "";
|
|
25234
26364
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
25235
26365
|
}
|
|
25236
26366
|
console.log("");
|
|
25237
26367
|
if (result.decision === "allow") {
|
|
25238
|
-
console.log(
|
|
26368
|
+
console.log(import_chalk33.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk33.default.gray(" \u2014 no approval needed"));
|
|
25239
26369
|
} else {
|
|
25240
26370
|
console.log(
|
|
25241
|
-
|
|
26371
|
+
import_chalk33.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk33.default.gray(" \u2014 human approval required")
|
|
25242
26372
|
);
|
|
25243
26373
|
if (result.blockedByLabel) {
|
|
25244
|
-
console.log(
|
|
26374
|
+
console.log(import_chalk33.default.gray(` Reason: ${result.blockedByLabel}`));
|
|
25245
26375
|
}
|
|
25246
26376
|
}
|
|
25247
26377
|
console.log("");
|
|
@@ -25256,18 +26386,18 @@ program.command("tail").description("Stream live agent activity to the terminal"
|
|
|
25256
26386
|
try {
|
|
25257
26387
|
await startTail2(options);
|
|
25258
26388
|
} catch (err2) {
|
|
25259
|
-
console.error(
|
|
26389
|
+
console.error(import_chalk33.default.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
25260
26390
|
process.exit(1);
|
|
25261
26391
|
}
|
|
25262
26392
|
});
|
|
25263
26393
|
program.command("monitor").description("Live interactive dashboard \u2014 activity feed, approvals, security signals").action(async () => {
|
|
25264
26394
|
try {
|
|
25265
|
-
const dashboardPath =
|
|
26395
|
+
const dashboardPath = import_path57.default.join(__dirname, "dashboard.mjs");
|
|
25266
26396
|
const dynamicImport = new Function("id", "return import(id)");
|
|
25267
26397
|
const mod = await dynamicImport(`file://${dashboardPath}`);
|
|
25268
26398
|
await mod.startMonitor();
|
|
25269
26399
|
} catch (err2) {
|
|
25270
|
-
console.error(
|
|
26400
|
+
console.error(import_chalk33.default.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
25271
26401
|
process.exit(1);
|
|
25272
26402
|
}
|
|
25273
26403
|
});
|
|
@@ -25300,14 +26430,14 @@ Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
|
|
|
25300
26430
|
Run "node9 addto claude" to register it as the statusLine.`
|
|
25301
26431
|
).argument("[subcommand]", 'Optional: "debug on" / "debug off" to toggle stdin logging').argument("[state]", 'on|off \u2014 used with "debug" subcommand').action(async (subcommand, state) => {
|
|
25302
26432
|
if (subcommand === "debug") {
|
|
25303
|
-
const flagFile =
|
|
26433
|
+
const flagFile = import_path57.default.join(import_os53.default.homedir(), ".node9", "hud-debug");
|
|
25304
26434
|
if (state === "on") {
|
|
25305
|
-
|
|
25306
|
-
|
|
26435
|
+
import_fs58.default.mkdirSync(import_path57.default.dirname(flagFile), { recursive: true });
|
|
26436
|
+
import_fs58.default.writeFileSync(flagFile, "");
|
|
25307
26437
|
console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
|
|
25308
26438
|
console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
|
|
25309
26439
|
} else if (state === "off") {
|
|
25310
|
-
if (
|
|
26440
|
+
if (import_fs58.default.existsSync(flagFile)) import_fs58.default.unlinkSync(flagFile);
|
|
25311
26441
|
console.log("HUD debug logging disabled.");
|
|
25312
26442
|
} else {
|
|
25313
26443
|
console.error("Usage: node9 hud debug on|off");
|
|
@@ -25322,7 +26452,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
25322
26452
|
const ms = parseDuration(options.duration);
|
|
25323
26453
|
if (ms === null) {
|
|
25324
26454
|
console.error(
|
|
25325
|
-
|
|
26455
|
+
import_chalk33.default.red(`
|
|
25326
26456
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
25327
26457
|
`)
|
|
25328
26458
|
);
|
|
@@ -25330,20 +26460,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
25330
26460
|
}
|
|
25331
26461
|
pauseNode9(ms, options.duration);
|
|
25332
26462
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
25333
|
-
console.log(
|
|
26463
|
+
console.log(import_chalk33.default.yellow(`
|
|
25334
26464
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
25335
|
-
console.log(
|
|
25336
|
-
console.log(
|
|
26465
|
+
console.log(import_chalk33.default.gray(` All tool calls will be allowed without review.`));
|
|
26466
|
+
console.log(import_chalk33.default.gray(` Run "node9 resume" to re-enable early.
|
|
25337
26467
|
`));
|
|
25338
26468
|
});
|
|
25339
26469
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
25340
26470
|
const { paused } = checkPause();
|
|
25341
26471
|
if (!paused) {
|
|
25342
|
-
console.log(
|
|
26472
|
+
console.log(import_chalk33.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
25343
26473
|
return;
|
|
25344
26474
|
}
|
|
25345
26475
|
resumeNode9();
|
|
25346
|
-
console.log(
|
|
26476
|
+
console.log(import_chalk33.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
25347
26477
|
});
|
|
25348
26478
|
var HOOK_BASED_AGENTS = {
|
|
25349
26479
|
claude: "claude",
|
|
@@ -25359,15 +26489,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
25359
26489
|
if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
|
|
25360
26490
|
const target = HOOK_BASED_AGENTS[firstArg2];
|
|
25361
26491
|
console.error(
|
|
25362
|
-
|
|
26492
|
+
import_chalk33.default.yellow(`
|
|
25363
26493
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
25364
26494
|
);
|
|
25365
|
-
console.error(
|
|
26495
|
+
console.error(import_chalk33.default.white(`
|
|
25366
26496
|
"${target}" uses its own hook system. Use:`));
|
|
25367
26497
|
console.error(
|
|
25368
|
-
|
|
26498
|
+
import_chalk33.default.green(` node9 addto ${target} `) + import_chalk33.default.gray("# one-time setup")
|
|
25369
26499
|
);
|
|
25370
|
-
console.error(
|
|
26500
|
+
console.error(import_chalk33.default.green(` ${target} `) + import_chalk33.default.gray("# run normally"));
|
|
25371
26501
|
process.exit(1);
|
|
25372
26502
|
}
|
|
25373
26503
|
const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
|
|
@@ -25384,7 +26514,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
25384
26514
|
}
|
|
25385
26515
|
);
|
|
25386
26516
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
25387
|
-
console.error(
|
|
26517
|
+
console.error(import_chalk33.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
25388
26518
|
const daemonReady = await autoStartDaemonAndWait();
|
|
25389
26519
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
25390
26520
|
}
|
|
@@ -25397,12 +26527,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
25397
26527
|
}
|
|
25398
26528
|
if (!result.approved) {
|
|
25399
26529
|
console.error(
|
|
25400
|
-
|
|
26530
|
+
import_chalk33.default.red(`
|
|
25401
26531
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
25402
26532
|
);
|
|
25403
26533
|
process.exit(1);
|
|
25404
26534
|
}
|
|
25405
|
-
console.error(
|
|
26535
|
+
console.error(import_chalk33.default.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
25406
26536
|
await runProxy(fullCommand);
|
|
25407
26537
|
} else {
|
|
25408
26538
|
program.help();
|
|
@@ -25415,6 +26545,8 @@ registerTrustCommand(program);
|
|
|
25415
26545
|
registerSyncCommand(program);
|
|
25416
26546
|
registerAgentsCommand(program);
|
|
25417
26547
|
registerScanCommand(program);
|
|
26548
|
+
registerPostureCommand(program);
|
|
26549
|
+
registerEgressCommand(program);
|
|
25418
26550
|
registerSessionsCommand(program);
|
|
25419
26551
|
registerDlpCommand(program);
|
|
25420
26552
|
registerMaskCommand(program);
|
|
@@ -25424,9 +26556,9 @@ if (process.argv[2] !== "daemon") {
|
|
|
25424
26556
|
const isCheckHook = process.argv[2] === "check";
|
|
25425
26557
|
if (isCheckHook) {
|
|
25426
26558
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
25427
|
-
const logPath =
|
|
26559
|
+
const logPath = import_path57.default.join(import_os53.default.homedir(), ".node9", "hook-debug.log");
|
|
25428
26560
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
25429
|
-
|
|
26561
|
+
import_fs58.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
25430
26562
|
`);
|
|
25431
26563
|
}
|
|
25432
26564
|
process.exit(0);
|