@node9/proxy 1.44.0 → 1.45.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 +1171 -666
- package/dist/cli.mjs +1171 -666
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -219,8 +219,8 @@ function sanitizeConfig(raw) {
|
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
const lines = result.error.issues.map((issue) => {
|
|
222
|
-
const
|
|
223
|
-
return ` \u2022 ${
|
|
222
|
+
const path64 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
223
|
+
return ` \u2022 ${path64}: ${issue.message}`;
|
|
224
224
|
});
|
|
225
225
|
return {
|
|
226
226
|
sanitized,
|
|
@@ -1341,9 +1341,9 @@ function matchesPattern(text, patterns) {
|
|
|
1341
1341
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1342
1342
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1343
1343
|
}
|
|
1344
|
-
function getNestedValue(obj,
|
|
1344
|
+
function getNestedValue(obj, path64) {
|
|
1345
1345
|
if (!obj || typeof obj !== "object") return null;
|
|
1346
|
-
const segments =
|
|
1346
|
+
const segments = path64.split(".");
|
|
1347
1347
|
for (const seg of segments) {
|
|
1348
1348
|
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
1349
1349
|
}
|
|
@@ -4153,7 +4153,7 @@ function installShield(name, shieldJson) {
|
|
|
4153
4153
|
fs2.writeFileSync(tmp, JSON.stringify(shieldJson, null, 2), { mode: 384 });
|
|
4154
4154
|
fs2.renameSync(tmp, filePath);
|
|
4155
4155
|
}
|
|
4156
|
-
var USER_SHIELDS_DIR, SHIELDS, SHIELDS_STATE_FILE, RULE_KEY_MIGRATIONS;
|
|
4156
|
+
var USER_SHIELDS_DIR, SHIELDS, SHIELDS_STATE_FILE, RULE_KEY_MIGRATIONS, USER_SHIELDS_DIR_PATH;
|
|
4157
4157
|
var init_shields = __esm({
|
|
4158
4158
|
"src/shields.ts"() {
|
|
4159
4159
|
"use strict";
|
|
@@ -4170,6 +4170,7 @@ var init_shields = __esm({
|
|
|
4170
4170
|
// around the review-read-credentials rule.
|
|
4171
4171
|
["shield:project-jail:review-read-env-any-tool", "shield:project-jail:block-read-env-any-tool"]
|
|
4172
4172
|
];
|
|
4173
|
+
USER_SHIELDS_DIR_PATH = USER_SHIELDS_DIR;
|
|
4173
4174
|
}
|
|
4174
4175
|
});
|
|
4175
4176
|
|
|
@@ -5040,7 +5041,30 @@ function explainIsSqlTool(toolName, toolInspection) {
|
|
|
5040
5041
|
const fieldName = toolInspection[matchingPattern];
|
|
5041
5042
|
return fieldName === "sql" || fieldName === "query";
|
|
5042
5043
|
}
|
|
5043
|
-
async function explainPolicy(toolName, args) {
|
|
5044
|
+
async function explainPolicy(toolName, args, agent = EXPLAIN_AGENT) {
|
|
5045
|
+
const derived = await deriveExplainTrace(toolName, args);
|
|
5046
|
+
const engine = await evaluatePolicy2(toolName, args, agent);
|
|
5047
|
+
if (derived.decision === engine.decision) return derived;
|
|
5048
|
+
if (process.env.NODE9_DEBUG) {
|
|
5049
|
+
console.error(
|
|
5050
|
+
`[node9 explain] decision drift: trace=${derived.decision} engine=${engine.decision} for ${toolName} \u2014 engine wins (${engine.blockedByLabel ?? "engine"}).`
|
|
5051
|
+
);
|
|
5052
|
+
}
|
|
5053
|
+
const engineStep = {
|
|
5054
|
+
name: "Engine verdict (authoritative)",
|
|
5055
|
+
outcome: engine.decision,
|
|
5056
|
+
detail: `${engine.blockedByLabel ?? "policy engine"}${engine.reason ? `: ${engine.reason}` : ""} \u2014 this is the verdict the live hook actually enforces. It differs from the preview trace above, which models the policy tiers separately and can drift; the engine is authoritative.`,
|
|
5057
|
+
isFinal: true
|
|
5058
|
+
};
|
|
5059
|
+
return {
|
|
5060
|
+
...derived,
|
|
5061
|
+
steps: [...derived.steps, engineStep],
|
|
5062
|
+
decision: engine.decision,
|
|
5063
|
+
blockedByLabel: engine.blockedByLabel ?? derived.blockedByLabel,
|
|
5064
|
+
ruleDescription: engine.ruleDescription ?? derived.ruleDescription
|
|
5065
|
+
};
|
|
5066
|
+
}
|
|
5067
|
+
async function deriveExplainTrace(toolName, args) {
|
|
5044
5068
|
const steps = [];
|
|
5045
5069
|
const globalPath = path7.join(os6.homedir(), ".node9", "config.json");
|
|
5046
5070
|
const projectPath = path7.join(process.cwd(), "node9.config.json");
|
|
@@ -5322,7 +5346,7 @@ async function explainPolicy(toolName, args) {
|
|
|
5322
5346
|
});
|
|
5323
5347
|
return { tool: toolName, args, waterfall, steps, decision: "allow" };
|
|
5324
5348
|
}
|
|
5325
|
-
var SQL_DML_KEYWORDS2;
|
|
5349
|
+
var SQL_DML_KEYWORDS2, EXPLAIN_AGENT;
|
|
5326
5350
|
var init_policy = __esm({
|
|
5327
5351
|
"src/policy/index.ts"() {
|
|
5328
5352
|
"use strict";
|
|
@@ -5333,6 +5357,7 @@ var init_policy = __esm({
|
|
|
5333
5357
|
init_dist();
|
|
5334
5358
|
init_dist();
|
|
5335
5359
|
SQL_DML_KEYWORDS2 = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
5360
|
+
EXPLAIN_AGENT = "agent";
|
|
5336
5361
|
}
|
|
5337
5362
|
});
|
|
5338
5363
|
|
|
@@ -8611,9 +8636,9 @@ function writeToml(filePath, data) {
|
|
|
8611
8636
|
async function setupCodex() {
|
|
8612
8637
|
seedMcpPinsIfMissing();
|
|
8613
8638
|
const homeDir2 = os12.homedir();
|
|
8614
|
-
const
|
|
8639
|
+
const configPath = path15.join(homeDir2, ".codex", "config.toml");
|
|
8615
8640
|
const hooksPath = path15.join(homeDir2, ".codex", "hooks.json");
|
|
8616
|
-
const config = readToml(
|
|
8641
|
+
const config = readToml(configPath) ?? {};
|
|
8617
8642
|
const servers = config.mcp_servers ?? {};
|
|
8618
8643
|
let anythingChanged = false;
|
|
8619
8644
|
const hooksFile = readJson(hooksPath) ?? {};
|
|
@@ -8688,7 +8713,7 @@ async function setupCodex() {
|
|
|
8688
8713
|
if (!hasNode9McpServer(servers)) {
|
|
8689
8714
|
servers["node9"] = NODE9_MCP_SERVER_ENTRY;
|
|
8690
8715
|
config.mcp_servers = servers;
|
|
8691
|
-
writeToml(
|
|
8716
|
+
writeToml(configPath, config);
|
|
8692
8717
|
console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
|
|
8693
8718
|
anythingChanged = true;
|
|
8694
8719
|
}
|
|
@@ -8700,7 +8725,7 @@ async function setupCodex() {
|
|
|
8700
8725
|
}
|
|
8701
8726
|
if (serversToWrap.length > 0) {
|
|
8702
8727
|
console.log(chalk.bold("The following existing entries will be modified:\n"));
|
|
8703
|
-
console.log(chalk.white(` ${
|
|
8728
|
+
console.log(chalk.white(` ${configPath}`));
|
|
8704
8729
|
for (const { name, upstream } of serversToWrap) {
|
|
8705
8730
|
console.log(chalk.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
|
|
8706
8731
|
}
|
|
@@ -8715,7 +8740,7 @@ async function setupCodex() {
|
|
|
8715
8740
|
};
|
|
8716
8741
|
}
|
|
8717
8742
|
config.mcp_servers = servers;
|
|
8718
|
-
writeToml(
|
|
8743
|
+
writeToml(configPath, config);
|
|
8719
8744
|
console.log(chalk.green(`
|
|
8720
8745
|
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
8721
8746
|
anythingChanged = true;
|
|
@@ -8763,7 +8788,7 @@ async function setupCodex() {
|
|
|
8763
8788
|
}
|
|
8764
8789
|
function teardownCodex() {
|
|
8765
8790
|
const homeDir2 = os12.homedir();
|
|
8766
|
-
const
|
|
8791
|
+
const configPath = path15.join(homeDir2, ".codex", "config.toml");
|
|
8767
8792
|
const hooksPath = path15.join(homeDir2, ".codex", "hooks.json");
|
|
8768
8793
|
const hooksFile = readJson(hooksPath);
|
|
8769
8794
|
if (hooksFile?.hooks) {
|
|
@@ -8781,7 +8806,7 @@ function teardownCodex() {
|
|
|
8781
8806
|
console.log(chalk.green(" \u2705 Removed Node9 hooks from ~/.codex/hooks.json"));
|
|
8782
8807
|
}
|
|
8783
8808
|
}
|
|
8784
|
-
const config = readToml(
|
|
8809
|
+
const config = readToml(configPath);
|
|
8785
8810
|
if (!config?.mcp_servers) {
|
|
8786
8811
|
console.log(chalk.blue(" \u2139\uFE0F ~/.codex/config.toml not found \u2014 nothing to remove"));
|
|
8787
8812
|
return;
|
|
@@ -8804,7 +8829,7 @@ function teardownCodex() {
|
|
|
8804
8829
|
}
|
|
8805
8830
|
}
|
|
8806
8831
|
if (changed) {
|
|
8807
|
-
writeToml(
|
|
8832
|
+
writeToml(configPath, config);
|
|
8808
8833
|
console.log(chalk.green(" \u2705 Unwrapped MCP servers in ~/.codex/config.toml"));
|
|
8809
8834
|
} else {
|
|
8810
8835
|
console.log(chalk.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in ~/.codex/config.toml"));
|
|
@@ -9059,18 +9084,18 @@ function teardownVSCode() {
|
|
|
9059
9084
|
}
|
|
9060
9085
|
async function setupClaudeDesktop() {
|
|
9061
9086
|
seedMcpPinsIfMissing();
|
|
9062
|
-
const
|
|
9063
|
-
if (!
|
|
9087
|
+
const configPath = claudeDesktopConfigPath();
|
|
9088
|
+
if (!configPath) {
|
|
9064
9089
|
console.log(chalk.yellow(" \u26A0\uFE0F Claude Desktop is not supported on this platform."));
|
|
9065
9090
|
return;
|
|
9066
9091
|
}
|
|
9067
|
-
const config = readJson(
|
|
9092
|
+
const config = readJson(configPath) ?? {};
|
|
9068
9093
|
const servers = config.mcpServers ?? {};
|
|
9069
9094
|
let anythingChanged = false;
|
|
9070
9095
|
if (!hasNode9McpServer(servers)) {
|
|
9071
9096
|
servers["node9"] = NODE9_MCP_SERVER_ENTRY;
|
|
9072
9097
|
config.mcpServers = servers;
|
|
9073
|
-
writeJson(
|
|
9098
|
+
writeJson(configPath, config);
|
|
9074
9099
|
console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
|
|
9075
9100
|
anythingChanged = true;
|
|
9076
9101
|
}
|
|
@@ -9081,7 +9106,7 @@ async function setupClaudeDesktop() {
|
|
|
9081
9106
|
}
|
|
9082
9107
|
if (serversToWrap.length > 0) {
|
|
9083
9108
|
console.log(chalk.bold("The following existing entries will be modified:\n"));
|
|
9084
|
-
console.log(chalk.white(` ${
|
|
9109
|
+
console.log(chalk.white(` ${configPath}`));
|
|
9085
9110
|
for (const { name, upstream } of serversToWrap) {
|
|
9086
9111
|
console.log(chalk.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
|
|
9087
9112
|
}
|
|
@@ -9096,7 +9121,7 @@ async function setupClaudeDesktop() {
|
|
|
9096
9121
|
};
|
|
9097
9122
|
}
|
|
9098
9123
|
config.mcpServers = servers;
|
|
9099
|
-
writeJson(
|
|
9124
|
+
writeJson(configPath, config);
|
|
9100
9125
|
console.log(chalk.green(`
|
|
9101
9126
|
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
9102
9127
|
anythingChanged = true;
|
|
@@ -9123,12 +9148,12 @@ async function setupClaudeDesktop() {
|
|
|
9123
9148
|
}
|
|
9124
9149
|
}
|
|
9125
9150
|
function teardownClaudeDesktop() {
|
|
9126
|
-
const
|
|
9127
|
-
if (!
|
|
9151
|
+
const configPath = claudeDesktopConfigPath();
|
|
9152
|
+
if (!configPath) {
|
|
9128
9153
|
console.log(chalk.yellow(" \u26A0\uFE0F Claude Desktop is not supported on this platform."));
|
|
9129
9154
|
return;
|
|
9130
9155
|
}
|
|
9131
|
-
const config = readJson(
|
|
9156
|
+
const config = readJson(configPath);
|
|
9132
9157
|
if (!config?.mcpServers) {
|
|
9133
9158
|
console.log(chalk.blue(" \u2139\uFE0F Claude Desktop config not found \u2014 nothing to remove"));
|
|
9134
9159
|
return;
|
|
@@ -9136,7 +9161,7 @@ function teardownClaudeDesktop() {
|
|
|
9136
9161
|
let changed = false;
|
|
9137
9162
|
if (removeNode9McpServer(config.mcpServers)) {
|
|
9138
9163
|
changed = true;
|
|
9139
|
-
console.log(chalk.green(` \u2705 Removed node9 MCP server entry from ${
|
|
9164
|
+
console.log(chalk.green(` \u2705 Removed node9 MCP server entry from ${configPath}`));
|
|
9140
9165
|
}
|
|
9141
9166
|
for (const [name, server] of Object.entries(config.mcpServers)) {
|
|
9142
9167
|
const args = server.args;
|
|
@@ -9151,7 +9176,7 @@ function teardownClaudeDesktop() {
|
|
|
9151
9176
|
}
|
|
9152
9177
|
}
|
|
9153
9178
|
if (changed) {
|
|
9154
|
-
writeJson(
|
|
9179
|
+
writeJson(configPath, config);
|
|
9155
9180
|
console.log(chalk.green(" \u2705 Unwrapped MCP servers in Claude Desktop config"));
|
|
9156
9181
|
} else {
|
|
9157
9182
|
console.log(chalk.blue(" \u2139\uFE0F No Node9-wrapped MCP servers found in Claude Desktop config"));
|
|
@@ -9179,7 +9204,7 @@ async function setupOpencode() {
|
|
|
9179
9204
|
const homeDir2 = os12.homedir();
|
|
9180
9205
|
const configDir = path15.join(homeDir2, ".config", "opencode");
|
|
9181
9206
|
const pluginsDir = path15.join(configDir, "plugins");
|
|
9182
|
-
const
|
|
9207
|
+
const configPath = path15.join(configDir, "opencode.json");
|
|
9183
9208
|
const pluginPath = path15.join(pluginsDir, OPENCODE_PLUGIN_NAME);
|
|
9184
9209
|
try {
|
|
9185
9210
|
fs13.mkdirSync(pluginsDir, { recursive: true });
|
|
@@ -9213,7 +9238,7 @@ async function setupOpencode() {
|
|
|
9213
9238
|
);
|
|
9214
9239
|
}
|
|
9215
9240
|
}
|
|
9216
|
-
const config = readJson(
|
|
9241
|
+
const config = readJson(configPath) ?? {};
|
|
9217
9242
|
const mcp = config.mcp ?? {};
|
|
9218
9243
|
let configChanged = false;
|
|
9219
9244
|
const desiredCommand = [...node9ArgvForShim(), "mcp-server"];
|
|
@@ -9234,7 +9259,7 @@ async function setupOpencode() {
|
|
|
9234
9259
|
console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
|
|
9235
9260
|
}
|
|
9236
9261
|
}
|
|
9237
|
-
if (configChanged) writeJson(
|
|
9262
|
+
if (configChanged) writeJson(configPath, config);
|
|
9238
9263
|
if (pluginChanged || configChanged) {
|
|
9239
9264
|
console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Opencode!"));
|
|
9240
9265
|
console.log(chalk.gray(" Restart Opencode for changes to take effect."));
|
|
@@ -9247,7 +9272,7 @@ function teardownOpencode() {
|
|
|
9247
9272
|
const homeDir2 = os12.homedir();
|
|
9248
9273
|
const configDir = path15.join(homeDir2, ".config", "opencode");
|
|
9249
9274
|
const pluginsDir = path15.join(configDir, "plugins");
|
|
9250
|
-
const
|
|
9275
|
+
const configPath = path15.join(configDir, "opencode.json");
|
|
9251
9276
|
const pluginPath = path15.join(pluginsDir, OPENCODE_PLUGIN_NAME);
|
|
9252
9277
|
try {
|
|
9253
9278
|
if (fs13.existsSync(pluginPath)) {
|
|
@@ -9257,7 +9282,7 @@ function teardownOpencode() {
|
|
|
9257
9282
|
} catch (err2) {
|
|
9258
9283
|
console.log(chalk.yellow(` \u26A0\uFE0F Could not remove ${pluginPath}: ${String(err2)}`));
|
|
9259
9284
|
}
|
|
9260
|
-
const config = readJson(
|
|
9285
|
+
const config = readJson(configPath);
|
|
9261
9286
|
if (!config) {
|
|
9262
9287
|
console.log(chalk.blue(" \u2139\uFE0F ~/.config/opencode/opencode.json not found \u2014 nothing to remove"));
|
|
9263
9288
|
return;
|
|
@@ -9273,7 +9298,7 @@ function teardownOpencode() {
|
|
|
9273
9298
|
}
|
|
9274
9299
|
if (changed) {
|
|
9275
9300
|
config.mcp = mcp;
|
|
9276
|
-
writeJson(
|
|
9301
|
+
writeJson(configPath, config);
|
|
9277
9302
|
} else {
|
|
9278
9303
|
console.log(chalk.blue(" \u2139\uFE0F No node9 entries found in ~/.config/opencode/opencode.json"));
|
|
9279
9304
|
}
|
|
@@ -9344,15 +9369,15 @@ function hermesAllowlistPath(homeDir2 = os12.homedir()) {
|
|
|
9344
9369
|
}
|
|
9345
9370
|
function setupHermes() {
|
|
9346
9371
|
const homeDir2 = os12.homedir();
|
|
9347
|
-
const
|
|
9372
|
+
const configPath = hermesConfigPath(homeDir2);
|
|
9348
9373
|
const allowlistPath = hermesAllowlistPath(homeDir2);
|
|
9349
|
-
if (!fs13.existsSync(
|
|
9350
|
-
console.log(chalk.yellow(` \u26A0\uFE0F Hermes config not found at ${
|
|
9374
|
+
if (!fs13.existsSync(configPath)) {
|
|
9375
|
+
console.log(chalk.yellow(` \u26A0\uFE0F Hermes config not found at ${configPath}`));
|
|
9351
9376
|
console.log(chalk.gray(" Run `hermes setup` first, then re-run node9 agents add hermes."));
|
|
9352
9377
|
return;
|
|
9353
9378
|
}
|
|
9354
9379
|
let anythingChanged = false;
|
|
9355
|
-
const raw = fs13.readFileSync(
|
|
9380
|
+
const raw = fs13.readFileSync(configPath, "utf-8");
|
|
9356
9381
|
const doc = yaml.parseDocument(raw);
|
|
9357
9382
|
if (doc.errors.length > 0) {
|
|
9358
9383
|
console.log(chalk.yellow(` \u26A0\uFE0F Hermes config.yaml has YAML parse errors:`));
|
|
@@ -9392,7 +9417,7 @@ function setupHermes() {
|
|
|
9392
9417
|
anythingChanged = true;
|
|
9393
9418
|
}
|
|
9394
9419
|
if (anythingChanged) {
|
|
9395
|
-
fs13.writeFileSync(
|
|
9420
|
+
fs13.writeFileSync(configPath, doc.toString());
|
|
9396
9421
|
}
|
|
9397
9422
|
let allowlist = {};
|
|
9398
9423
|
if (fs13.existsSync(allowlistPath)) {
|
|
@@ -9435,24 +9460,24 @@ function setupHermes() {
|
|
|
9435
9460
|
}
|
|
9436
9461
|
function teardownHermes() {
|
|
9437
9462
|
const homeDir2 = os12.homedir();
|
|
9438
|
-
const
|
|
9463
|
+
const configPath = hermesConfigPath(homeDir2);
|
|
9439
9464
|
const allowlistPath = hermesAllowlistPath(homeDir2);
|
|
9440
|
-
if (!fs13.existsSync(
|
|
9441
|
-
console.log(chalk.blue(` \u2139\uFE0F ${
|
|
9465
|
+
if (!fs13.existsSync(configPath)) {
|
|
9466
|
+
console.log(chalk.blue(` \u2139\uFE0F ${configPath} not found \u2014 nothing to remove`));
|
|
9442
9467
|
return;
|
|
9443
9468
|
}
|
|
9444
|
-
const raw = fs13.readFileSync(
|
|
9469
|
+
const raw = fs13.readFileSync(configPath, "utf-8");
|
|
9445
9470
|
const doc = yaml.parseDocument(raw);
|
|
9446
9471
|
if (doc.errors.length > 0) {
|
|
9447
9472
|
console.log(
|
|
9448
|
-
chalk.yellow(` \u26A0\uFE0F Skipping ${
|
|
9473
|
+
chalk.yellow(` \u26A0\uFE0F Skipping ${configPath} \u2014 file has YAML parse errors, fix it manually.`)
|
|
9449
9474
|
);
|
|
9450
9475
|
} else {
|
|
9451
|
-
teardownHermesConfigDoc(doc,
|
|
9476
|
+
teardownHermesConfigDoc(doc, configPath);
|
|
9452
9477
|
}
|
|
9453
9478
|
teardownHermesAllowlist(allowlistPath);
|
|
9454
9479
|
}
|
|
9455
|
-
function teardownHermesConfigDoc(doc,
|
|
9480
|
+
function teardownHermesConfigDoc(doc, configPath) {
|
|
9456
9481
|
let anythingChanged = false;
|
|
9457
9482
|
const current = doc.toJS() ?? {};
|
|
9458
9483
|
for (const { event } of HERMES_HOOK_PLAN) {
|
|
@@ -9474,10 +9499,10 @@ function teardownHermesConfigDoc(doc, configPath2) {
|
|
|
9474
9499
|
anythingChanged = true;
|
|
9475
9500
|
}
|
|
9476
9501
|
if (anythingChanged) {
|
|
9477
|
-
fs13.writeFileSync(
|
|
9478
|
-
console.log(chalk.green(` \u2705 Removed Node9 hooks from ${
|
|
9502
|
+
fs13.writeFileSync(configPath, doc.toString());
|
|
9503
|
+
console.log(chalk.green(` \u2705 Removed Node9 hooks from ${configPath}`));
|
|
9479
9504
|
} else {
|
|
9480
|
-
console.log(chalk.blue(` \u2139\uFE0F No Node9 hooks found in ${
|
|
9505
|
+
console.log(chalk.blue(` \u2139\uFE0F No Node9 hooks found in ${configPath}`));
|
|
9481
9506
|
}
|
|
9482
9507
|
}
|
|
9483
9508
|
function teardownHermesAllowlist(allowlistPath) {
|
|
@@ -12089,8 +12114,8 @@ function countScanFiles() {
|
|
|
12089
12114
|
const geminiDir = path25.join(os22.homedir(), ".gemini", "tmp");
|
|
12090
12115
|
if (fs23.existsSync(geminiDir)) {
|
|
12091
12116
|
try {
|
|
12092
|
-
for (const
|
|
12093
|
-
const p = path25.join(geminiDir,
|
|
12117
|
+
for (const slug2 of fs23.readdirSync(geminiDir)) {
|
|
12118
|
+
const p = path25.join(geminiDir, slug2);
|
|
12094
12119
|
try {
|
|
12095
12120
|
if (!fs23.statSync(p).isDirectory()) continue;
|
|
12096
12121
|
const chatsDir = path25.join(p, "chats");
|
|
@@ -12487,14 +12512,14 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
|
|
|
12487
12512
|
return result;
|
|
12488
12513
|
}
|
|
12489
12514
|
const ruleSources = buildRuleSources();
|
|
12490
|
-
for (const
|
|
12491
|
-
const slugPath = path25.join(tmpDir,
|
|
12515
|
+
for (const slug2 of slugDirs) {
|
|
12516
|
+
const slugPath = path25.join(tmpDir, slug2);
|
|
12492
12517
|
try {
|
|
12493
12518
|
if (!fs23.statSync(slugPath).isDirectory()) continue;
|
|
12494
12519
|
} catch {
|
|
12495
12520
|
continue;
|
|
12496
12521
|
}
|
|
12497
|
-
let projLabel = stripTerminalEscapes(
|
|
12522
|
+
let projLabel = stripTerminalEscapes(slug2).slice(0, 40);
|
|
12498
12523
|
try {
|
|
12499
12524
|
projLabel = stripTerminalEscapes(
|
|
12500
12525
|
fs23.readFileSync(path25.join(slugPath, ".project_root"), "utf-8").trim()
|
|
@@ -13833,7 +13858,7 @@ function renderPanelScorecard(input, now = /* @__PURE__ */ new Date()) {
|
|
|
13833
13858
|
const hitShieldSet = new Set(
|
|
13834
13859
|
shieldImpacts.filter((i) => i.totalCatches > 0).map((i) => i.shieldName)
|
|
13835
13860
|
);
|
|
13836
|
-
const zeroHitBuiltins = Object.keys(
|
|
13861
|
+
const zeroHitBuiltins = Object.keys(BUILTIN_SHIELDS).filter((name) => !hitShieldSet.has(name)).sort();
|
|
13837
13862
|
if (zeroHitBuiltins.length > 0) {
|
|
13838
13863
|
shieldLines.push(mkLine([""]));
|
|
13839
13864
|
shieldLines.push(mkLine([zeroHitBuiltins.join(" \xB7 "), chalk5.dim]));
|
|
@@ -18696,10 +18721,10 @@ __export(tail_exports, {
|
|
|
18696
18721
|
startTail: () => startTail
|
|
18697
18722
|
});
|
|
18698
18723
|
import http3 from "http";
|
|
18699
|
-
import
|
|
18700
|
-
import
|
|
18701
|
-
import
|
|
18702
|
-
import
|
|
18724
|
+
import chalk35 from "chalk";
|
|
18725
|
+
import fs64 from "fs";
|
|
18726
|
+
import os54 from "os";
|
|
18727
|
+
import path61 from "path";
|
|
18703
18728
|
import readline6 from "readline";
|
|
18704
18729
|
import { spawn as spawn8 } from "child_process";
|
|
18705
18730
|
function shortenPathSummary(s) {
|
|
@@ -18723,20 +18748,20 @@ function getModelContextLimit(model) {
|
|
|
18723
18748
|
return 2e5;
|
|
18724
18749
|
}
|
|
18725
18750
|
function readSessionUsage() {
|
|
18726
|
-
const projectsDir =
|
|
18727
|
-
if (!
|
|
18751
|
+
const projectsDir = path61.join(os54.homedir(), ".claude", "projects");
|
|
18752
|
+
if (!fs64.existsSync(projectsDir)) return null;
|
|
18728
18753
|
let latestFile = null;
|
|
18729
18754
|
let latestMtime = 0;
|
|
18730
18755
|
try {
|
|
18731
|
-
for (const dir of
|
|
18732
|
-
const dirPath =
|
|
18756
|
+
for (const dir of fs64.readdirSync(projectsDir)) {
|
|
18757
|
+
const dirPath = path61.join(projectsDir, dir);
|
|
18733
18758
|
try {
|
|
18734
|
-
if (!
|
|
18735
|
-
for (const file of
|
|
18759
|
+
if (!fs64.statSync(dirPath).isDirectory()) continue;
|
|
18760
|
+
for (const file of fs64.readdirSync(dirPath)) {
|
|
18736
18761
|
if (!file.endsWith(".jsonl") || file.startsWith("agent-")) continue;
|
|
18737
|
-
const filePath =
|
|
18762
|
+
const filePath = path61.join(dirPath, file);
|
|
18738
18763
|
try {
|
|
18739
|
-
const mtime =
|
|
18764
|
+
const mtime = fs64.statSync(filePath).mtimeMs;
|
|
18740
18765
|
if (mtime > latestMtime) {
|
|
18741
18766
|
latestMtime = mtime;
|
|
18742
18767
|
latestFile = filePath;
|
|
@@ -18751,7 +18776,7 @@ function readSessionUsage() {
|
|
|
18751
18776
|
}
|
|
18752
18777
|
if (!latestFile) return null;
|
|
18753
18778
|
try {
|
|
18754
|
-
const lines =
|
|
18779
|
+
const lines = fs64.readFileSync(latestFile, "utf-8").split("\n");
|
|
18755
18780
|
let lastModel = "";
|
|
18756
18781
|
let lastInput = 0;
|
|
18757
18782
|
let lastOutput = 0;
|
|
@@ -18776,10 +18801,10 @@ function readSessionUsage() {
|
|
|
18776
18801
|
}
|
|
18777
18802
|
}
|
|
18778
18803
|
function formatContextStat(stat) {
|
|
18779
|
-
const pctColor = stat.fillPct >= 80 ?
|
|
18804
|
+
const pctColor = stat.fillPct >= 80 ? chalk35.red : stat.fillPct >= 50 ? chalk35.yellow : chalk35.cyan;
|
|
18780
18805
|
const k = (n) => `${Math.round(n / 1e3)}k`;
|
|
18781
18806
|
const modelShort = stat.model.replace(/@.*$/, "").replace(/-\d{8}$/, "").replace(/^claude-/, "");
|
|
18782
|
-
return
|
|
18807
|
+
return chalk35.dim("ctx: ") + pctColor(`${stat.fillPct}%`) + chalk35.dim(
|
|
18783
18808
|
` (${k(stat.inputTokens)}/${k(getModelContextLimit(stat.model))} out ${k(stat.outputTokens)} \xB7 ${modelShort})`
|
|
18784
18809
|
);
|
|
18785
18810
|
}
|
|
@@ -18802,32 +18827,32 @@ function agentLabel(agent, mcpServer, sessionId) {
|
|
|
18802
18827
|
const tag = sessionTag(sessionId);
|
|
18803
18828
|
const tagSuffix = tag ? `\xB7${tag}` : "";
|
|
18804
18829
|
if (!agent || agent === "Terminal") {
|
|
18805
|
-
return mcpServer ?
|
|
18830
|
+
return mcpServer ? chalk35.dim(`[\u2192 ${mcpServer}] `) : "";
|
|
18806
18831
|
}
|
|
18807
18832
|
const short = agent === "Claude Code" ? "Claude" : agent === "Gemini CLI" ? "Gemini" : agent === "Unknown Agent" ? "" : agent.split(" ")[0];
|
|
18808
|
-
if (!short) return mcpServer ?
|
|
18809
|
-
return mcpServer ?
|
|
18833
|
+
if (!short) return mcpServer ? chalk35.dim(`[\u2192 ${mcpServer}] `) : "";
|
|
18834
|
+
return mcpServer ? chalk35.dim(`[${short}${tagSuffix} \u2192 ${mcpServer}] `) : chalk35.dim(`[${short}${tagSuffix}] `);
|
|
18810
18835
|
}
|
|
18811
18836
|
function formatBase(activity) {
|
|
18812
18837
|
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
18813
18838
|
const icon = getIcon(activity.tool);
|
|
18814
18839
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
18815
|
-
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(
|
|
18840
|
+
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os54.homedir(), "~");
|
|
18816
18841
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
18817
|
-
return `${
|
|
18842
|
+
return `${chalk35.gray(time)} ${icon} ${agentLabel(activity.agent, activity.mcpServer, activity.sessionId)}${chalk35.white.bold(toolName)} ${chalk35.dim(argsPreview)}`;
|
|
18818
18843
|
}
|
|
18819
18844
|
function renderResult(activity, result) {
|
|
18820
18845
|
const base = formatBase(activity);
|
|
18821
18846
|
let status;
|
|
18822
18847
|
if (result.status === "allow") {
|
|
18823
|
-
status =
|
|
18848
|
+
status = chalk35.green("\u2713 ALLOW");
|
|
18824
18849
|
} else if (result.status === "dlp") {
|
|
18825
|
-
status =
|
|
18850
|
+
status = chalk35.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
18826
18851
|
} else {
|
|
18827
|
-
status =
|
|
18852
|
+
status = chalk35.red("\u2717 BLOCK");
|
|
18828
18853
|
}
|
|
18829
18854
|
const cost = result.costEstimate ?? activity.costEstimate;
|
|
18830
|
-
const costSuffix = cost == null ? "" :
|
|
18855
|
+
const costSuffix = cost == null ? "" : chalk35.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
|
|
18831
18856
|
if (process.stdout.isTTY) {
|
|
18832
18857
|
if (pendingShownForId === activity.id && pendingWrappedLines > 1) {
|
|
18833
18858
|
readline6.moveCursor(process.stdout, 0, -(pendingWrappedLines - 1));
|
|
@@ -18844,19 +18869,19 @@ function renderResult(activity, result) {
|
|
|
18844
18869
|
}
|
|
18845
18870
|
function renderPending(activity) {
|
|
18846
18871
|
if (!process.stdout.isTTY) return;
|
|
18847
|
-
const line = `${formatBase(activity)} ${
|
|
18872
|
+
const line = `${formatBase(activity)} ${chalk35.yellow("\u25CF \u2026")}`;
|
|
18848
18873
|
pendingShownForId = activity.id;
|
|
18849
18874
|
pendingWrappedLines = wrappedLineCount(line);
|
|
18850
18875
|
process.stdout.write(`${line}\r`);
|
|
18851
18876
|
}
|
|
18852
18877
|
async function ensureDaemon() {
|
|
18853
18878
|
let pidPort = null;
|
|
18854
|
-
if (
|
|
18879
|
+
if (fs64.existsSync(PID_FILE)) {
|
|
18855
18880
|
try {
|
|
18856
|
-
const { port } = JSON.parse(
|
|
18881
|
+
const { port } = JSON.parse(fs64.readFileSync(PID_FILE, "utf-8"));
|
|
18857
18882
|
pidPort = port;
|
|
18858
18883
|
} catch {
|
|
18859
|
-
console.error(
|
|
18884
|
+
console.error(chalk35.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
18860
18885
|
}
|
|
18861
18886
|
}
|
|
18862
18887
|
const checkPort = pidPort ?? DAEMON_PORT;
|
|
@@ -18867,7 +18892,7 @@ async function ensureDaemon() {
|
|
|
18867
18892
|
if (res.ok) return checkPort;
|
|
18868
18893
|
} catch {
|
|
18869
18894
|
}
|
|
18870
|
-
console.log(
|
|
18895
|
+
console.log(chalk35.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
18871
18896
|
const child = spawn8(process.execPath, [process.argv[1], "daemon"], {
|
|
18872
18897
|
detached: true,
|
|
18873
18898
|
stdio: "ignore",
|
|
@@ -18884,7 +18909,7 @@ async function ensureDaemon() {
|
|
|
18884
18909
|
} catch {
|
|
18885
18910
|
}
|
|
18886
18911
|
}
|
|
18887
|
-
console.error(
|
|
18912
|
+
console.error(chalk35.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
18888
18913
|
process.exit(1);
|
|
18889
18914
|
}
|
|
18890
18915
|
function postDecisionHttp(id, decision, authToken, port, opts) {
|
|
@@ -18953,7 +18978,7 @@ function buildCardLines(req, localCount = 0) {
|
|
|
18953
18978
|
const severityIcon = isBlock ? `${RED}\u{1F6D1}` : `${YELLOW}\u26A0 `;
|
|
18954
18979
|
const rawDesc = req.riskMetadata?.ruleDescription ?? "";
|
|
18955
18980
|
const description = rawDesc ? cleanReason(rawDesc) : "";
|
|
18956
|
-
const agentSuffix = req.agent && req.agent !== "Terminal" ? ` ${RESET2}${
|
|
18981
|
+
const agentSuffix = req.agent && req.agent !== "Terminal" ? ` ${RESET2}${chalk35.dim(`(${req.agent})`)}` : "";
|
|
18957
18982
|
const lines = [
|
|
18958
18983
|
``,
|
|
18959
18984
|
`${BOLD2}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET2}`,
|
|
@@ -19009,9 +19034,9 @@ function buildRecoveryCardLines(req) {
|
|
|
19009
19034
|
];
|
|
19010
19035
|
}
|
|
19011
19036
|
function readApproversFromDisk() {
|
|
19012
|
-
const
|
|
19037
|
+
const configPath = path61.join(os54.homedir(), ".node9", "config.json");
|
|
19013
19038
|
try {
|
|
19014
|
-
const raw = JSON.parse(
|
|
19039
|
+
const raw = JSON.parse(fs64.readFileSync(configPath, "utf-8"));
|
|
19015
19040
|
const settings = raw.settings ?? {};
|
|
19016
19041
|
return settings.approvers ?? {};
|
|
19017
19042
|
} catch {
|
|
@@ -19022,20 +19047,20 @@ function approverStatusLine() {
|
|
|
19022
19047
|
const a = readApproversFromDisk();
|
|
19023
19048
|
const fmt = (label2, key) => {
|
|
19024
19049
|
const on = a[key] !== false;
|
|
19025
|
-
return `[${key[0]}]${label2.slice(1)} ${on ?
|
|
19050
|
+
return `[${key[0]}]${label2.slice(1)} ${on ? chalk35.green("\u2713") : chalk35.dim("\u2717")}`;
|
|
19026
19051
|
};
|
|
19027
19052
|
return `${fmt("native", "native")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
|
|
19028
19053
|
}
|
|
19029
19054
|
function toggleApprover(channel) {
|
|
19030
|
-
const
|
|
19055
|
+
const configPath = path61.join(os54.homedir(), ".node9", "config.json");
|
|
19031
19056
|
try {
|
|
19032
|
-
const raw = JSON.parse(
|
|
19057
|
+
const raw = JSON.parse(fs64.readFileSync(configPath, "utf-8"));
|
|
19033
19058
|
const settings = raw.settings ?? {};
|
|
19034
19059
|
const approvers = settings.approvers ?? {};
|
|
19035
19060
|
approvers[channel] = approvers[channel] === false;
|
|
19036
19061
|
settings.approvers = approvers;
|
|
19037
19062
|
raw.settings = settings;
|
|
19038
|
-
|
|
19063
|
+
fs64.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
|
|
19039
19064
|
} catch (err2) {
|
|
19040
19065
|
process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
|
|
19041
19066
|
`);
|
|
@@ -19067,7 +19092,7 @@ async function startTail(options = {}) {
|
|
|
19067
19092
|
req2.end();
|
|
19068
19093
|
});
|
|
19069
19094
|
if (result.ok) {
|
|
19070
|
-
console.log(
|
|
19095
|
+
console.log(chalk35.green("\u2713 Flight Recorder buffer cleared."));
|
|
19071
19096
|
} else if (result.code === "ECONNREFUSED") {
|
|
19072
19097
|
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
19073
19098
|
} else if (result.code === "ETIMEDOUT") {
|
|
@@ -19113,7 +19138,7 @@ async function startTail(options = {}) {
|
|
|
19113
19138
|
const channel = name === "n" ? "native" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
|
|
19114
19139
|
if (channel) {
|
|
19115
19140
|
toggleApprover(channel);
|
|
19116
|
-
console.log(
|
|
19141
|
+
console.log(chalk35.dim(` Approvers: ${approverStatusLine()}`));
|
|
19117
19142
|
}
|
|
19118
19143
|
};
|
|
19119
19144
|
process.stdin.on("keypress", idleKeypressHandler);
|
|
@@ -19179,7 +19204,7 @@ async function startTail(options = {}) {
|
|
|
19179
19204
|
localAllowCounts.get(req2.toolName) ?? 0
|
|
19180
19205
|
)
|
|
19181
19206
|
);
|
|
19182
|
-
const decisionStamp = action === "always-allow" ?
|
|
19207
|
+
const decisionStamp = action === "always-allow" ? chalk35.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk35.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk35.green("\u2713 ALLOWED") : action === "redirect" ? chalk35.yellow("\u21A9 REDIRECT AI") : chalk35.red("\u2717 DENIED");
|
|
19183
19208
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
|
|
19184
19209
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
19185
19210
|
process.stdout.write(SHOW_CURSOR);
|
|
@@ -19207,8 +19232,8 @@ async function startTail(options = {}) {
|
|
|
19207
19232
|
}
|
|
19208
19233
|
postDecisionHttp(req2.id, httpDecision, authToken, port, httpOpts).catch((err2) => {
|
|
19209
19234
|
try {
|
|
19210
|
-
|
|
19211
|
-
|
|
19235
|
+
fs64.appendFileSync(
|
|
19236
|
+
path61.join(os54.homedir(), ".node9", "hook-debug.log"),
|
|
19212
19237
|
`[tail] POST /decision failed: ${String(err2)}
|
|
19213
19238
|
`
|
|
19214
19239
|
);
|
|
@@ -19230,7 +19255,7 @@ async function startTail(options = {}) {
|
|
|
19230
19255
|
);
|
|
19231
19256
|
const stampedLines = buildCardLines(req2, priorCount);
|
|
19232
19257
|
if (externalDecision) {
|
|
19233
|
-
const source = externalDecision === "allow" ?
|
|
19258
|
+
const source = externalDecision === "allow" ? chalk35.green("\u2713 ALLOWED") : chalk35.red("\u2717 DENIED");
|
|
19234
19259
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
|
|
19235
19260
|
}
|
|
19236
19261
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
@@ -19272,31 +19297,31 @@ async function startTail(options = {}) {
|
|
|
19272
19297
|
};
|
|
19273
19298
|
process.stdin.on("keypress", onKeypress);
|
|
19274
19299
|
}
|
|
19275
|
-
const auditLog =
|
|
19300
|
+
const auditLog = path61.join(os54.homedir(), ".node9", "audit.log");
|
|
19276
19301
|
try {
|
|
19277
|
-
const unackedDlp =
|
|
19302
|
+
const unackedDlp = fs64.readFileSync(auditLog, "utf-8").split("\n").filter((l) => l.includes('"response-dlp"')).length;
|
|
19278
19303
|
if (unackedDlp > 0) {
|
|
19279
19304
|
console.log("");
|
|
19280
19305
|
console.log(
|
|
19281
|
-
|
|
19306
|
+
chalk35.bgRed.white.bold(
|
|
19282
19307
|
` \u26A0\uFE0F DLP ALERT: ${unackedDlp} secret${unackedDlp !== 1 ? "s" : ""} found in Claude response text \u2014 run: node9 dlp `
|
|
19283
19308
|
)
|
|
19284
19309
|
);
|
|
19285
19310
|
}
|
|
19286
19311
|
} catch {
|
|
19287
19312
|
}
|
|
19288
|
-
console.log(
|
|
19313
|
+
console.log(chalk35.cyan.bold(`
|
|
19289
19314
|
\u{1F6F0}\uFE0F Node9 tail`));
|
|
19290
19315
|
if (canApprove) {
|
|
19291
|
-
console.log(
|
|
19292
|
-
console.log(
|
|
19316
|
+
console.log(chalk35.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
|
|
19317
|
+
console.log(chalk35.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
|
|
19293
19318
|
}
|
|
19294
19319
|
const ctxStat = readSessionUsage();
|
|
19295
19320
|
if (ctxStat) console.log(" " + formatContextStat(ctxStat));
|
|
19296
19321
|
if (options.history) {
|
|
19297
|
-
console.log(
|
|
19322
|
+
console.log(chalk35.dim("Showing history + live events.\n"));
|
|
19298
19323
|
} else {
|
|
19299
|
-
console.log(
|
|
19324
|
+
console.log(chalk35.dim("Showing live events only. Use --history to include past.\n"));
|
|
19300
19325
|
}
|
|
19301
19326
|
process.on("SIGINT", () => {
|
|
19302
19327
|
exitIdleMode();
|
|
@@ -19306,7 +19331,7 @@ async function startTail(options = {}) {
|
|
|
19306
19331
|
readline6.clearLine(process.stdout, 0);
|
|
19307
19332
|
readline6.cursorTo(process.stdout, 0);
|
|
19308
19333
|
}
|
|
19309
|
-
console.log(
|
|
19334
|
+
console.log(chalk35.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
19310
19335
|
process.exit(0);
|
|
19311
19336
|
});
|
|
19312
19337
|
const STALL_THRESHOLD_MS = 6e4;
|
|
@@ -19314,11 +19339,11 @@ async function startTail(options = {}) {
|
|
|
19314
19339
|
if (stallWarned) return;
|
|
19315
19340
|
if (Date.now() - lastActivityFromDaemon < STALL_THRESHOLD_MS) return;
|
|
19316
19341
|
try {
|
|
19317
|
-
const auditMtime =
|
|
19342
|
+
const auditMtime = fs64.statSync(auditLog).mtimeMs;
|
|
19318
19343
|
if (Date.now() - auditMtime >= STALL_THRESHOLD_MS) return;
|
|
19319
19344
|
console.log("");
|
|
19320
19345
|
console.log(
|
|
19321
|
-
|
|
19346
|
+
chalk35.yellow(
|
|
19322
19347
|
"\u26A0\uFE0F Tail appears stalled \u2014 hooks are firing but no events are arriving. Try: node9 daemon restart"
|
|
19323
19348
|
)
|
|
19324
19349
|
);
|
|
@@ -19335,7 +19360,7 @@ async function startTail(options = {}) {
|
|
|
19335
19360
|
},
|
|
19336
19361
|
(res) => {
|
|
19337
19362
|
if (res.statusCode !== 200) {
|
|
19338
|
-
console.error(
|
|
19363
|
+
console.error(chalk35.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
19339
19364
|
process.exit(1);
|
|
19340
19365
|
}
|
|
19341
19366
|
if (canApprove) enterIdleMode();
|
|
@@ -19366,7 +19391,7 @@ async function startTail(options = {}) {
|
|
|
19366
19391
|
readline6.clearLine(process.stdout, 0);
|
|
19367
19392
|
readline6.cursorTo(process.stdout, 0);
|
|
19368
19393
|
}
|
|
19369
|
-
console.log(
|
|
19394
|
+
console.log(chalk35.red("\n\u274C Daemon disconnected."));
|
|
19370
19395
|
process.exit(1);
|
|
19371
19396
|
});
|
|
19372
19397
|
}
|
|
@@ -19379,7 +19404,7 @@ async function startTail(options = {}) {
|
|
|
19379
19404
|
const parsed = JSON.parse(rawData);
|
|
19380
19405
|
const msg = parsed.message ?? "Flight recorder is down \u2014 run: node9 daemon restart";
|
|
19381
19406
|
console.log("");
|
|
19382
|
-
console.log(
|
|
19407
|
+
console.log(chalk35.bgRed.white.bold(` \u26A0\uFE0F ${msg} `));
|
|
19383
19408
|
} catch {
|
|
19384
19409
|
}
|
|
19385
19410
|
return;
|
|
@@ -19464,9 +19489,9 @@ async function startTail(options = {}) {
|
|
|
19464
19489
|
const rawSummary = data.argsSummary ?? data.tool;
|
|
19465
19490
|
const summary = shortenPathSummary(rawSummary);
|
|
19466
19491
|
const fileCount = data.fileCount ?? 0;
|
|
19467
|
-
const files = fileCount > 0 ?
|
|
19492
|
+
const files = fileCount > 0 ? chalk35.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
|
|
19468
19493
|
process.stdout.write(
|
|
19469
|
-
`${
|
|
19494
|
+
`${chalk35.dim(time)} ${chalk35.cyan("\u{1F4F8} snapshot")} ${chalk35.dim(hash)} ${summary}${files}
|
|
19470
19495
|
`
|
|
19471
19496
|
);
|
|
19472
19497
|
return;
|
|
@@ -19483,18 +19508,18 @@ async function startTail(options = {}) {
|
|
|
19483
19508
|
if (event === "execution-result") {
|
|
19484
19509
|
const exec = data;
|
|
19485
19510
|
const time = new Date(Date.now()).toLocaleTimeString([], { hour12: false });
|
|
19486
|
-
const arrow = exec.isError ?
|
|
19511
|
+
const arrow = exec.isError ? chalk35.red(" \u21B3 \u2717") : chalk35.green(" \u21B3 \u2713");
|
|
19487
19512
|
const label2 = agentLabel(exec.agent, exec.mcpServer);
|
|
19488
19513
|
const tool = (exec.tool ?? "").slice(0, 16);
|
|
19489
|
-
const duration = typeof exec.durationMs === "number" ?
|
|
19514
|
+
const duration = typeof exec.durationMs === "number" ? chalk35.dim(` (${exec.durationMs}ms)`) : "";
|
|
19490
19515
|
console.log(
|
|
19491
|
-
`${
|
|
19516
|
+
`${chalk35.gray(time)} ${arrow} ${label2}${chalk35.dim(tool)}${chalk35.dim(" completed")}${duration}`
|
|
19492
19517
|
);
|
|
19493
19518
|
}
|
|
19494
19519
|
}
|
|
19495
19520
|
req.on("error", (err2) => {
|
|
19496
19521
|
const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
|
|
19497
|
-
console.error(
|
|
19522
|
+
console.error(chalk35.red(`
|
|
19498
19523
|
\u274C ${msg}`));
|
|
19499
19524
|
process.exit(1);
|
|
19500
19525
|
});
|
|
@@ -19505,7 +19530,7 @@ var init_tail = __esm({
|
|
|
19505
19530
|
"use strict";
|
|
19506
19531
|
init_daemon2();
|
|
19507
19532
|
init_daemon();
|
|
19508
|
-
PID_FILE =
|
|
19533
|
+
PID_FILE = path61.join(os54.homedir(), ".node9", "daemon.pid");
|
|
19509
19534
|
ICONS = {
|
|
19510
19535
|
bash: "\u{1F4BB}",
|
|
19511
19536
|
shell: "\u{1F4BB}",
|
|
@@ -19553,9 +19578,9 @@ __export(hud_exports, {
|
|
|
19553
19578
|
main: () => main,
|
|
19554
19579
|
renderEnvironmentLine: () => renderEnvironmentLine
|
|
19555
19580
|
});
|
|
19556
|
-
import
|
|
19557
|
-
import
|
|
19558
|
-
import
|
|
19581
|
+
import fs65 from "fs";
|
|
19582
|
+
import path62 from "path";
|
|
19583
|
+
import os55 from "os";
|
|
19559
19584
|
import http4 from "http";
|
|
19560
19585
|
async function readStdin() {
|
|
19561
19586
|
const chunks = [];
|
|
@@ -19631,9 +19656,9 @@ function formatTimeLeft(resetsAt) {
|
|
|
19631
19656
|
return ` (${m}m left)`;
|
|
19632
19657
|
}
|
|
19633
19658
|
function safeReadJson(filePath) {
|
|
19634
|
-
if (!
|
|
19659
|
+
if (!fs65.existsSync(filePath)) return null;
|
|
19635
19660
|
try {
|
|
19636
|
-
return JSON.parse(
|
|
19661
|
+
return JSON.parse(fs65.readFileSync(filePath, "utf-8"));
|
|
19637
19662
|
} catch {
|
|
19638
19663
|
return null;
|
|
19639
19664
|
}
|
|
@@ -19654,12 +19679,12 @@ function countHooksInFile(filePath) {
|
|
|
19654
19679
|
return Object.keys(cfg.hooks).length;
|
|
19655
19680
|
}
|
|
19656
19681
|
function countRulesInDir(rulesDir) {
|
|
19657
|
-
if (!
|
|
19682
|
+
if (!fs65.existsSync(rulesDir)) return 0;
|
|
19658
19683
|
let count = 0;
|
|
19659
19684
|
try {
|
|
19660
|
-
for (const entry of
|
|
19685
|
+
for (const entry of fs65.readdirSync(rulesDir, { withFileTypes: true })) {
|
|
19661
19686
|
if (entry.isDirectory()) {
|
|
19662
|
-
count += countRulesInDir(
|
|
19687
|
+
count += countRulesInDir(path62.join(rulesDir, entry.name));
|
|
19663
19688
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
19664
19689
|
count++;
|
|
19665
19690
|
}
|
|
@@ -19670,46 +19695,46 @@ function countRulesInDir(rulesDir) {
|
|
|
19670
19695
|
}
|
|
19671
19696
|
function isSamePath(a, b) {
|
|
19672
19697
|
try {
|
|
19673
|
-
return
|
|
19698
|
+
return path62.resolve(a) === path62.resolve(b);
|
|
19674
19699
|
} catch {
|
|
19675
19700
|
return false;
|
|
19676
19701
|
}
|
|
19677
19702
|
}
|
|
19678
19703
|
function countConfigs(cwd) {
|
|
19679
|
-
const homeDir2 =
|
|
19680
|
-
const claudeDir =
|
|
19704
|
+
const homeDir2 = os55.homedir();
|
|
19705
|
+
const claudeDir = path62.join(homeDir2, ".claude");
|
|
19681
19706
|
let claudeMdCount = 0;
|
|
19682
19707
|
let rulesCount = 0;
|
|
19683
19708
|
let hooksCount = 0;
|
|
19684
19709
|
const userMcpServers = /* @__PURE__ */ new Set();
|
|
19685
19710
|
const projectMcpServers = /* @__PURE__ */ new Set();
|
|
19686
|
-
if (
|
|
19687
|
-
rulesCount += countRulesInDir(
|
|
19688
|
-
const userSettings =
|
|
19711
|
+
if (fs65.existsSync(path62.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
19712
|
+
rulesCount += countRulesInDir(path62.join(claudeDir, "rules"));
|
|
19713
|
+
const userSettings = path62.join(claudeDir, "settings.json");
|
|
19689
19714
|
for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
|
|
19690
19715
|
hooksCount += countHooksInFile(userSettings);
|
|
19691
|
-
const userClaudeJson =
|
|
19716
|
+
const userClaudeJson = path62.join(homeDir2, ".claude.json");
|
|
19692
19717
|
for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
|
|
19693
19718
|
for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
|
|
19694
19719
|
userMcpServers.delete(name);
|
|
19695
19720
|
}
|
|
19696
19721
|
if (cwd) {
|
|
19697
|
-
if (
|
|
19698
|
-
if (
|
|
19699
|
-
const projectClaudeDir =
|
|
19722
|
+
if (fs65.existsSync(path62.join(cwd, "CLAUDE.md"))) claudeMdCount++;
|
|
19723
|
+
if (fs65.existsSync(path62.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
|
|
19724
|
+
const projectClaudeDir = path62.join(cwd, ".claude");
|
|
19700
19725
|
const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
|
|
19701
19726
|
if (!overlapsUserScope) {
|
|
19702
|
-
if (
|
|
19703
|
-
rulesCount += countRulesInDir(
|
|
19704
|
-
const projSettings =
|
|
19727
|
+
if (fs65.existsSync(path62.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
19728
|
+
rulesCount += countRulesInDir(path62.join(projectClaudeDir, "rules"));
|
|
19729
|
+
const projSettings = path62.join(projectClaudeDir, "settings.json");
|
|
19705
19730
|
for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
|
|
19706
19731
|
hooksCount += countHooksInFile(projSettings);
|
|
19707
19732
|
}
|
|
19708
|
-
if (
|
|
19709
|
-
const localSettings =
|
|
19733
|
+
if (fs65.existsSync(path62.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
|
|
19734
|
+
const localSettings = path62.join(projectClaudeDir, "settings.local.json");
|
|
19710
19735
|
for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
|
|
19711
19736
|
hooksCount += countHooksInFile(localSettings);
|
|
19712
|
-
const mcpJsonServers = getMcpServerNames(
|
|
19737
|
+
const mcpJsonServers = getMcpServerNames(path62.join(cwd, ".mcp.json"));
|
|
19713
19738
|
const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
|
|
19714
19739
|
for (const name of disabledMcpJson) mcpJsonServers.delete(name);
|
|
19715
19740
|
for (const name of mcpJsonServers) projectMcpServers.add(name);
|
|
@@ -19742,12 +19767,12 @@ function readActiveShieldsHud() {
|
|
|
19742
19767
|
return shieldsCache.value;
|
|
19743
19768
|
}
|
|
19744
19769
|
try {
|
|
19745
|
-
const shieldsPath =
|
|
19746
|
-
if (!
|
|
19770
|
+
const shieldsPath = path62.join(os55.homedir(), ".node9", "shields.json");
|
|
19771
|
+
if (!fs65.existsSync(shieldsPath)) {
|
|
19747
19772
|
shieldsCache = { value: [], ts: now };
|
|
19748
19773
|
return [];
|
|
19749
19774
|
}
|
|
19750
|
-
const parsed = JSON.parse(
|
|
19775
|
+
const parsed = JSON.parse(fs65.readFileSync(shieldsPath, "utf-8"));
|
|
19751
19776
|
if (!Array.isArray(parsed.active)) {
|
|
19752
19777
|
shieldsCache = { value: [], ts: now };
|
|
19753
19778
|
return [];
|
|
@@ -19849,17 +19874,17 @@ function renderContextLine(stdin) {
|
|
|
19849
19874
|
async function main() {
|
|
19850
19875
|
try {
|
|
19851
19876
|
const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
|
|
19852
|
-
if (
|
|
19877
|
+
if (fs65.existsSync(path62.join(os55.homedir(), ".node9", "hud-debug"))) {
|
|
19853
19878
|
try {
|
|
19854
|
-
const logPath =
|
|
19879
|
+
const logPath = path62.join(os55.homedir(), ".node9", "hud-debug.log");
|
|
19855
19880
|
const MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
19856
19881
|
let size = 0;
|
|
19857
19882
|
try {
|
|
19858
|
-
size =
|
|
19883
|
+
size = fs65.statSync(logPath).size;
|
|
19859
19884
|
} catch {
|
|
19860
19885
|
}
|
|
19861
19886
|
if (size < MAX_LOG_SIZE) {
|
|
19862
|
-
|
|
19887
|
+
fs65.appendFileSync(
|
|
19863
19888
|
logPath,
|
|
19864
19889
|
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
|
|
19865
19890
|
);
|
|
@@ -19879,12 +19904,12 @@ async function main() {
|
|
|
19879
19904
|
const showEnvCounts = (() => {
|
|
19880
19905
|
try {
|
|
19881
19906
|
const cwd = stdin.cwd ?? process.cwd();
|
|
19882
|
-
for (const
|
|
19883
|
-
|
|
19884
|
-
|
|
19907
|
+
for (const configPath of [
|
|
19908
|
+
path62.join(cwd, "node9.config.json"),
|
|
19909
|
+
path62.join(os55.homedir(), ".node9", "config.json")
|
|
19885
19910
|
]) {
|
|
19886
|
-
if (!
|
|
19887
|
-
const cfg = JSON.parse(
|
|
19911
|
+
if (!fs65.existsSync(configPath)) continue;
|
|
19912
|
+
const cfg = JSON.parse(fs65.readFileSync(configPath, "utf-8"));
|
|
19888
19913
|
const hud = cfg.settings?.hud;
|
|
19889
19914
|
if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
|
|
19890
19915
|
}
|
|
@@ -19930,10 +19955,10 @@ init_core();
|
|
|
19930
19955
|
init_setup();
|
|
19931
19956
|
init_daemon2();
|
|
19932
19957
|
import { Command } from "commander";
|
|
19933
|
-
import
|
|
19934
|
-
import
|
|
19935
|
-
import
|
|
19936
|
-
import
|
|
19958
|
+
import chalk36 from "chalk";
|
|
19959
|
+
import fs66 from "fs";
|
|
19960
|
+
import path63 from "path";
|
|
19961
|
+
import os56 from "os";
|
|
19937
19962
|
import { spawn as spawn9 } from "child_process";
|
|
19938
19963
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
19939
19964
|
|
|
@@ -21562,9 +21587,135 @@ function registerLogCommand(program2) {
|
|
|
21562
21587
|
|
|
21563
21588
|
// src/cli/commands/shield.ts
|
|
21564
21589
|
init_shields();
|
|
21590
|
+
import chalk10 from "chalk";
|
|
21591
|
+
import fs46 from "fs";
|
|
21592
|
+
|
|
21593
|
+
// src/shields/build.ts
|
|
21594
|
+
function escapeRegex(s) {
|
|
21595
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
21596
|
+
}
|
|
21597
|
+
function slug(s) {
|
|
21598
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "rule";
|
|
21599
|
+
}
|
|
21600
|
+
var B = "[\\s/\\\\]";
|
|
21601
|
+
var SEP = "[/\\\\]";
|
|
21602
|
+
function pathToRegexFragment(rawPath) {
|
|
21603
|
+
const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
|
|
21604
|
+
const segments = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
|
|
21605
|
+
if (segments.length === 0) return "";
|
|
21606
|
+
return `(^|${B})${segments.join(SEP)}(${B}|$)`;
|
|
21607
|
+
}
|
|
21608
|
+
function toolRule(tool, verdict, reason) {
|
|
21609
|
+
return {
|
|
21610
|
+
name: `${verdict}-${slug(tool)}`,
|
|
21611
|
+
tool,
|
|
21612
|
+
conditions: [],
|
|
21613
|
+
verdict,
|
|
21614
|
+
reason: reason ?? `${tool} is restricted by this shield`
|
|
21615
|
+
};
|
|
21616
|
+
}
|
|
21617
|
+
function pathRules(rawPath, verdict, reason) {
|
|
21618
|
+
const value = pathToRegexFragment(rawPath);
|
|
21619
|
+
if (!value) return [];
|
|
21620
|
+
const why = reason ?? `Accessing ${rawPath} is restricted by this shield`;
|
|
21621
|
+
const s = slug(rawPath);
|
|
21622
|
+
return [
|
|
21623
|
+
{
|
|
21624
|
+
name: `${verdict}-path-${s}-bash`,
|
|
21625
|
+
tool: "bash",
|
|
21626
|
+
conditions: [{ field: "command", op: "matches", value }],
|
|
21627
|
+
verdict,
|
|
21628
|
+
reason: why
|
|
21629
|
+
},
|
|
21630
|
+
{
|
|
21631
|
+
name: `${verdict}-path-${s}-anytool`,
|
|
21632
|
+
tool: "*",
|
|
21633
|
+
conditions: [{ field: "file_path", op: "matches", value }],
|
|
21634
|
+
verdict,
|
|
21635
|
+
reason: why
|
|
21636
|
+
}
|
|
21637
|
+
];
|
|
21638
|
+
}
|
|
21639
|
+
function buildShield(input) {
|
|
21640
|
+
const smartRules = [
|
|
21641
|
+
...(input.blockTools ?? []).map((t) => toolRule(t, "block")),
|
|
21642
|
+
...(input.reviewTools ?? []).map((t) => toolRule(t, "review")),
|
|
21643
|
+
...(input.blockPaths ?? []).flatMap((p) => pathRules(p, "block")),
|
|
21644
|
+
...(input.reviewPaths ?? []).flatMap((p) => pathRules(p, "review"))
|
|
21645
|
+
];
|
|
21646
|
+
return {
|
|
21647
|
+
name: input.name,
|
|
21648
|
+
description: input.description ?? `Custom shield "${input.name}" created with node9 shield create`,
|
|
21649
|
+
aliases: input.aliases ?? [],
|
|
21650
|
+
smartRules,
|
|
21651
|
+
dangerousWords: []
|
|
21652
|
+
};
|
|
21653
|
+
}
|
|
21654
|
+
|
|
21655
|
+
// src/shields/create.ts
|
|
21656
|
+
init_dist();
|
|
21657
|
+
init_shields();
|
|
21658
|
+
init_audit();
|
|
21659
|
+
import fs45 from "fs";
|
|
21660
|
+
import path43 from "path";
|
|
21661
|
+
function builtinNames() {
|
|
21662
|
+
const names = /* @__PURE__ */ new Set();
|
|
21663
|
+
for (const def of Object.values(BUILTIN_SHIELDS)) {
|
|
21664
|
+
names.add(def.name.toLowerCase());
|
|
21665
|
+
for (const a of def.aliases ?? []) names.add(a.toLowerCase());
|
|
21666
|
+
}
|
|
21667
|
+
return names;
|
|
21668
|
+
}
|
|
21669
|
+
function createShield(def, opts = {}) {
|
|
21670
|
+
const name = def.name;
|
|
21671
|
+
if (builtinNames().has(name.toLowerCase())) {
|
|
21672
|
+
return {
|
|
21673
|
+
ok: false,
|
|
21674
|
+
error: `"${name}" is a built-in shield \u2014 choose a different name (a user shield with this name would shadow the built-in).`
|
|
21675
|
+
};
|
|
21676
|
+
}
|
|
21677
|
+
const filePath = path43.join(USER_SHIELDS_DIR_PATH, `${name}.json`);
|
|
21678
|
+
if (!opts.overwrite && fs45.existsSync(filePath)) {
|
|
21679
|
+
return {
|
|
21680
|
+
ok: false,
|
|
21681
|
+
error: `Shield "${name}" already exists at ${filePath}. Pass --overwrite to replace it.`
|
|
21682
|
+
};
|
|
21683
|
+
}
|
|
21684
|
+
if (def.smartRules.length === 0) {
|
|
21685
|
+
return {
|
|
21686
|
+
ok: false,
|
|
21687
|
+
error: "Shield has no rules \u2014 add at least one --block/--review tool or path."
|
|
21688
|
+
};
|
|
21689
|
+
}
|
|
21690
|
+
if (opts.viaMcp && def.smartRules.some((r) => r.verdict === "allow")) {
|
|
21691
|
+
return {
|
|
21692
|
+
ok: false,
|
|
21693
|
+
error: "allow-verdict rules are not permitted over MCP (they would weaken node9). Use the CLI."
|
|
21694
|
+
};
|
|
21695
|
+
}
|
|
21696
|
+
const v = validateShieldDefinition(def);
|
|
21697
|
+
if ("error" in v) {
|
|
21698
|
+
return { ok: false, error: v.error };
|
|
21699
|
+
}
|
|
21700
|
+
installShield(name, def);
|
|
21701
|
+
let enabled = false;
|
|
21702
|
+
if (opts.enable) {
|
|
21703
|
+
const active = readActiveShields();
|
|
21704
|
+
if (!active.includes(name)) writeActiveShields([...active, name]);
|
|
21705
|
+
enabled = true;
|
|
21706
|
+
}
|
|
21707
|
+
appendConfigAudit({
|
|
21708
|
+
event: "shield-create",
|
|
21709
|
+
shield: name,
|
|
21710
|
+
via: opts.viaMcp ? "mcp" : "cli",
|
|
21711
|
+
enabled
|
|
21712
|
+
});
|
|
21713
|
+
return { ok: true, path: filePath, enabled, ruleCount: def.smartRules.length };
|
|
21714
|
+
}
|
|
21715
|
+
|
|
21716
|
+
// src/cli/commands/shield.ts
|
|
21565
21717
|
init_audit();
|
|
21566
21718
|
init_config();
|
|
21567
|
-
import chalk10 from "chalk";
|
|
21568
21719
|
|
|
21569
21720
|
// src/utils/https-fetch.ts
|
|
21570
21721
|
import https4 from "https";
|
|
@@ -21867,6 +22018,73 @@ function registerShieldCommand(program2) {
|
|
|
21867
22018
|
process.exit(1);
|
|
21868
22019
|
});
|
|
21869
22020
|
});
|
|
22021
|
+
const collect = (val, prev) => [...prev, val];
|
|
22022
|
+
shieldCmd.command("create <name>").description("Author a new shield from block/review tools and paths").option("--desc <text>", "Shield description").option("--block-tool <tool>", "Block a tool entirely (repeatable)", collect, []).option("--review-tool <tool>", "Require approval for a tool (repeatable)", collect, []).option("--block-path <path>", "Block access to a path (repeatable)", collect, []).option("--review-path <path>", "Require approval to access a path (repeatable)", collect, []).option("--from-file <path>", "Build from a shield JSON file (overrides inline flags)").option("--enable", "Activate the shield immediately").option("--overwrite", "Replace an existing user shield of the same name").action(
|
|
22023
|
+
(name, opts) => {
|
|
22024
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
22025
|
+
console.error(
|
|
22026
|
+
chalk10.red(
|
|
22027
|
+
`
|
|
22028
|
+
\u274C Invalid shield name: only alphanumeric characters, hyphens, and underscores are allowed
|
|
22029
|
+
`
|
|
22030
|
+
)
|
|
22031
|
+
);
|
|
22032
|
+
process.exit(1);
|
|
22033
|
+
}
|
|
22034
|
+
let def;
|
|
22035
|
+
if (opts.fromFile) {
|
|
22036
|
+
let raw;
|
|
22037
|
+
try {
|
|
22038
|
+
raw = JSON.parse(fs46.readFileSync(opts.fromFile, "utf-8"));
|
|
22039
|
+
} catch (err2) {
|
|
22040
|
+
console.error(
|
|
22041
|
+
chalk10.red(`
|
|
22042
|
+
\u274C Could not read/parse ${opts.fromFile}: ${String(err2)}
|
|
22043
|
+
`)
|
|
22044
|
+
);
|
|
22045
|
+
process.exit(1);
|
|
22046
|
+
return;
|
|
22047
|
+
}
|
|
22048
|
+
def = { ...raw, name };
|
|
22049
|
+
} else {
|
|
22050
|
+
def = buildShield({
|
|
22051
|
+
name,
|
|
22052
|
+
description: opts.desc,
|
|
22053
|
+
blockTools: opts.blockTool,
|
|
22054
|
+
reviewTools: opts.reviewTool,
|
|
22055
|
+
blockPaths: opts.blockPath,
|
|
22056
|
+
reviewPaths: opts.reviewPath
|
|
22057
|
+
});
|
|
22058
|
+
}
|
|
22059
|
+
const res = createShield(def, {
|
|
22060
|
+
enable: opts.enable,
|
|
22061
|
+
overwrite: opts.overwrite,
|
|
22062
|
+
viaMcp: false
|
|
22063
|
+
});
|
|
22064
|
+
if (!res.ok) {
|
|
22065
|
+
console.error(chalk10.red(`
|
|
22066
|
+
\u274C ${res.error}
|
|
22067
|
+
`));
|
|
22068
|
+
process.exit(1);
|
|
22069
|
+
return;
|
|
22070
|
+
}
|
|
22071
|
+
console.log(
|
|
22072
|
+
chalk10.green(`
|
|
22073
|
+
\u2705 Shield "${name}" created`) + chalk10.gray(` \u2014 ${res.ruleCount} rule(s) \u2192 ${res.path}`)
|
|
22074
|
+
);
|
|
22075
|
+
if (res.enabled) {
|
|
22076
|
+
console.log(chalk10.gray(` Active now.`));
|
|
22077
|
+
} else {
|
|
22078
|
+
console.log(
|
|
22079
|
+
chalk10.gray(` Activate it with: ${chalk10.cyan(`node9 shield enable ${name}`)}`)
|
|
22080
|
+
);
|
|
22081
|
+
}
|
|
22082
|
+
console.log(
|
|
22083
|
+
chalk10.gray(` Preview a call: ${chalk10.cyan(`node9 explain bash "<command>"`)}
|
|
22084
|
+
`)
|
|
22085
|
+
);
|
|
22086
|
+
}
|
|
22087
|
+
);
|
|
21870
22088
|
}
|
|
21871
22089
|
function registerConfigShowCommand(program2) {
|
|
21872
22090
|
program2.command("config show").description(
|
|
@@ -21940,8 +22158,8 @@ init_daemon();
|
|
|
21940
22158
|
init_config();
|
|
21941
22159
|
init_agent_wiring();
|
|
21942
22160
|
import chalk11 from "chalk";
|
|
21943
|
-
import
|
|
21944
|
-
import
|
|
22161
|
+
import fs47 from "fs";
|
|
22162
|
+
import path44 from "path";
|
|
21945
22163
|
import os40 from "os";
|
|
21946
22164
|
import { execSync } from "child_process";
|
|
21947
22165
|
function registerDoctorCommand(program2, version2) {
|
|
@@ -21992,10 +22210,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
21992
22210
|
);
|
|
21993
22211
|
}
|
|
21994
22212
|
section("Configuration");
|
|
21995
|
-
const globalConfigPath =
|
|
21996
|
-
if (
|
|
22213
|
+
const globalConfigPath = path44.join(homeDir2, ".node9", "config.json");
|
|
22214
|
+
if (fs47.existsSync(globalConfigPath)) {
|
|
21997
22215
|
try {
|
|
21998
|
-
JSON.parse(
|
|
22216
|
+
JSON.parse(fs47.readFileSync(globalConfigPath, "utf-8"));
|
|
21999
22217
|
pass("~/.node9/config.json found and valid");
|
|
22000
22218
|
} catch {
|
|
22001
22219
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -22003,10 +22221,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
22003
22221
|
} else {
|
|
22004
22222
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
22005
22223
|
}
|
|
22006
|
-
const projectConfigPath =
|
|
22007
|
-
if (
|
|
22224
|
+
const projectConfigPath = path44.join(process.cwd(), "node9.config.json");
|
|
22225
|
+
if (fs47.existsSync(projectConfigPath)) {
|
|
22008
22226
|
try {
|
|
22009
|
-
JSON.parse(
|
|
22227
|
+
JSON.parse(fs47.readFileSync(projectConfigPath, "utf-8"));
|
|
22010
22228
|
pass("node9.config.json found and valid (project)");
|
|
22011
22229
|
} catch {
|
|
22012
22230
|
fail(
|
|
@@ -22015,8 +22233,8 @@ function registerDoctorCommand(program2, version2) {
|
|
|
22015
22233
|
);
|
|
22016
22234
|
}
|
|
22017
22235
|
}
|
|
22018
|
-
const credsPath =
|
|
22019
|
-
if (
|
|
22236
|
+
const credsPath = path44.join(homeDir2, ".node9", "credentials.json");
|
|
22237
|
+
if (fs47.existsSync(credsPath)) {
|
|
22020
22238
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
22021
22239
|
} else {
|
|
22022
22240
|
warn(
|
|
@@ -22060,7 +22278,7 @@ function registerDoctorCommand(program2, version2) {
|
|
|
22060
22278
|
try {
|
|
22061
22279
|
const { shipLagBytes: shipLagBytes2, readWatermark: readWatermark2, AUDIT_SHIP_WATERMARK: AUDIT_SHIP_WATERMARK2 } = await Promise.resolve().then(() => (init_audit_shipper(), audit_shipper_exports));
|
|
22062
22280
|
const cfg = getConfig();
|
|
22063
|
-
const creds =
|
|
22281
|
+
const creds = fs47.existsSync(path44.join(os40.homedir(), ".node9", "credentials.json"));
|
|
22064
22282
|
if (!creds) {
|
|
22065
22283
|
warn("Not logged in \u2014 audit rows stay local", "Run: node9 login <api-key>");
|
|
22066
22284
|
} else if (!cfg.settings.approvers.cloud) {
|
|
@@ -22110,8 +22328,8 @@ function registerDoctorCommand(program2, version2) {
|
|
|
22110
22328
|
|
|
22111
22329
|
// src/cli/commands/audit.ts
|
|
22112
22330
|
import chalk12 from "chalk";
|
|
22113
|
-
import
|
|
22114
|
-
import
|
|
22331
|
+
import fs48 from "fs";
|
|
22332
|
+
import path45 from "path";
|
|
22115
22333
|
import os41 from "os";
|
|
22116
22334
|
function formatRelativeTime(timestamp) {
|
|
22117
22335
|
const diff = Date.now() - new Date(timestamp).getTime();
|
|
@@ -22125,14 +22343,14 @@ function formatRelativeTime(timestamp) {
|
|
|
22125
22343
|
}
|
|
22126
22344
|
function registerAuditCommand(program2) {
|
|
22127
22345
|
program2.command("audit").description("View local execution audit log").option("--tail <n>", "Number of entries to show", "20").option("--tool <pattern>", "Filter by tool name (substring match)").option("--deny", "Show only denied actions").option("--json", "Output raw JSON").action((options) => {
|
|
22128
|
-
const logPath =
|
|
22129
|
-
if (!
|
|
22346
|
+
const logPath = path45.join(os41.homedir(), ".node9", "audit.log");
|
|
22347
|
+
if (!fs48.existsSync(logPath)) {
|
|
22130
22348
|
console.log(
|
|
22131
22349
|
chalk12.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
22132
22350
|
);
|
|
22133
22351
|
return;
|
|
22134
22352
|
}
|
|
22135
|
-
const raw =
|
|
22353
|
+
const raw = fs48.readFileSync(logPath, "utf-8");
|
|
22136
22354
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
22137
22355
|
let entries = lines.flatMap((line) => {
|
|
22138
22356
|
try {
|
|
@@ -22191,9 +22409,9 @@ import chalk13 from "chalk";
|
|
|
22191
22409
|
init_costSync();
|
|
22192
22410
|
init_litellm();
|
|
22193
22411
|
init_cost_codex();
|
|
22194
|
-
import
|
|
22412
|
+
import fs49 from "fs";
|
|
22195
22413
|
import os42 from "os";
|
|
22196
|
-
import
|
|
22414
|
+
import path46 from "path";
|
|
22197
22415
|
var TEST_COMMAND_RE3 = /(?:^|\s)(npm\s+(?:run\s+)?test|npx\s+(?:vitest|jest|mocha)|yarn\s+(?:run\s+)?test|pnpm\s+(?:run\s+)?test|vitest|jest|mocha|pytest|py\.test|cargo\s+test|go\s+test|bundle\s+exec\s+rspec|rspec|phpunit|dotnet\s+test)\b/i;
|
|
22198
22416
|
function buildTestTimestamps(allEntries) {
|
|
22199
22417
|
const testTs = /* @__PURE__ */ new Set();
|
|
@@ -22273,8 +22491,8 @@ function getDateRange(period, now) {
|
|
|
22273
22491
|
}
|
|
22274
22492
|
}
|
|
22275
22493
|
function parseAuditLog(logPath) {
|
|
22276
|
-
if (!
|
|
22277
|
-
const raw =
|
|
22494
|
+
if (!fs49.existsSync(logPath)) return [];
|
|
22495
|
+
const raw = fs49.readFileSync(logPath, "utf-8");
|
|
22278
22496
|
return raw.split("\n").flatMap((line) => {
|
|
22279
22497
|
if (!line.trim()) return [];
|
|
22280
22498
|
try {
|
|
@@ -22321,25 +22539,25 @@ function freezeClaudeCost(acc) {
|
|
|
22321
22539
|
};
|
|
22322
22540
|
}
|
|
22323
22541
|
function processClaudeCostProject(proj, projectsDir, start, end, acc) {
|
|
22324
|
-
const projPath =
|
|
22542
|
+
const projPath = path46.join(projectsDir, proj);
|
|
22325
22543
|
let files;
|
|
22326
22544
|
try {
|
|
22327
|
-
const stat =
|
|
22545
|
+
const stat = fs49.statSync(projPath);
|
|
22328
22546
|
if (!stat.isDirectory()) return;
|
|
22329
|
-
files =
|
|
22547
|
+
files = fs49.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
|
|
22330
22548
|
} catch {
|
|
22331
22549
|
return;
|
|
22332
22550
|
}
|
|
22333
22551
|
const startMs = start.getTime();
|
|
22334
22552
|
for (const file of files) {
|
|
22335
|
-
const filePath =
|
|
22553
|
+
const filePath = path46.join(projPath, file);
|
|
22336
22554
|
try {
|
|
22337
|
-
if (
|
|
22555
|
+
if (fs49.statSync(filePath).mtimeMs < startMs) continue;
|
|
22338
22556
|
} catch {
|
|
22339
22557
|
continue;
|
|
22340
22558
|
}
|
|
22341
22559
|
try {
|
|
22342
|
-
const raw =
|
|
22560
|
+
const raw = fs49.readFileSync(filePath, "utf-8");
|
|
22343
22561
|
for (const line of raw.split("\n")) {
|
|
22344
22562
|
if (!line.trim()) continue;
|
|
22345
22563
|
let entry;
|
|
@@ -22389,10 +22607,10 @@ function processClaudeCostProject(proj, projectsDir, start, end, acc) {
|
|
|
22389
22607
|
}
|
|
22390
22608
|
function loadClaudeCost(start, end, projectsDir) {
|
|
22391
22609
|
const acc = emptyClaudeCostAccumulator();
|
|
22392
|
-
if (!
|
|
22610
|
+
if (!fs49.existsSync(projectsDir)) return freezeClaudeCost(acc);
|
|
22393
22611
|
let dirs;
|
|
22394
22612
|
try {
|
|
22395
|
-
dirs =
|
|
22613
|
+
dirs = fs49.readdirSync(projectsDir);
|
|
22396
22614
|
} catch {
|
|
22397
22615
|
return freezeClaudeCost(acc);
|
|
22398
22616
|
}
|
|
@@ -22404,7 +22622,7 @@ function loadClaudeCost(start, end, projectsDir) {
|
|
|
22404
22622
|
function processCodexCostFile(filePath, start, end, acc) {
|
|
22405
22623
|
let lines;
|
|
22406
22624
|
try {
|
|
22407
|
-
lines =
|
|
22625
|
+
lines = fs49.readFileSync(filePath, "utf-8").split("\n");
|
|
22408
22626
|
} catch {
|
|
22409
22627
|
return;
|
|
22410
22628
|
}
|
|
@@ -22459,31 +22677,31 @@ function processCodexCostFile(filePath, start, end, acc) {
|
|
|
22459
22677
|
}
|
|
22460
22678
|
function listCodexSessionFiles2(sessionsBase) {
|
|
22461
22679
|
const jsonlFiles = [];
|
|
22462
|
-
if (!
|
|
22680
|
+
if (!fs49.existsSync(sessionsBase)) return jsonlFiles;
|
|
22463
22681
|
try {
|
|
22464
|
-
for (const year of
|
|
22465
|
-
const yearPath =
|
|
22682
|
+
for (const year of fs49.readdirSync(sessionsBase)) {
|
|
22683
|
+
const yearPath = path46.join(sessionsBase, year);
|
|
22466
22684
|
try {
|
|
22467
|
-
if (!
|
|
22685
|
+
if (!fs49.statSync(yearPath).isDirectory()) continue;
|
|
22468
22686
|
} catch {
|
|
22469
22687
|
continue;
|
|
22470
22688
|
}
|
|
22471
|
-
for (const month of
|
|
22472
|
-
const monthPath =
|
|
22689
|
+
for (const month of fs49.readdirSync(yearPath)) {
|
|
22690
|
+
const monthPath = path46.join(yearPath, month);
|
|
22473
22691
|
try {
|
|
22474
|
-
if (!
|
|
22692
|
+
if (!fs49.statSync(monthPath).isDirectory()) continue;
|
|
22475
22693
|
} catch {
|
|
22476
22694
|
continue;
|
|
22477
22695
|
}
|
|
22478
|
-
for (const day of
|
|
22479
|
-
const dayPath =
|
|
22696
|
+
for (const day of fs49.readdirSync(monthPath)) {
|
|
22697
|
+
const dayPath = path46.join(monthPath, day);
|
|
22480
22698
|
try {
|
|
22481
|
-
if (!
|
|
22699
|
+
if (!fs49.statSync(dayPath).isDirectory()) continue;
|
|
22482
22700
|
} catch {
|
|
22483
22701
|
continue;
|
|
22484
22702
|
}
|
|
22485
|
-
for (const file of
|
|
22486
|
-
if (file.endsWith(".jsonl")) jsonlFiles.push(
|
|
22703
|
+
for (const file of fs49.readdirSync(dayPath)) {
|
|
22704
|
+
if (file.endsWith(".jsonl")) jsonlFiles.push(path46.join(dayPath, file));
|
|
22487
22705
|
}
|
|
22488
22706
|
}
|
|
22489
22707
|
}
|
|
@@ -22548,13 +22766,13 @@ function freezeGeminiCost(acc) {
|
|
|
22548
22766
|
function processGeminiCostFile(filePath, projectKey, start, end, acc) {
|
|
22549
22767
|
const startMs = start.getTime();
|
|
22550
22768
|
try {
|
|
22551
|
-
if (
|
|
22769
|
+
if (fs49.statSync(filePath).mtimeMs < startMs) return;
|
|
22552
22770
|
} catch {
|
|
22553
22771
|
return;
|
|
22554
22772
|
}
|
|
22555
22773
|
let raw;
|
|
22556
22774
|
try {
|
|
22557
|
-
raw =
|
|
22775
|
+
raw = fs49.readFileSync(filePath, "utf-8");
|
|
22558
22776
|
} catch {
|
|
22559
22777
|
return;
|
|
22560
22778
|
}
|
|
@@ -22603,30 +22821,30 @@ function listGeminiSessionFiles2(geminiTmpDir2) {
|
|
|
22603
22821
|
const out = [];
|
|
22604
22822
|
let dirs;
|
|
22605
22823
|
try {
|
|
22606
|
-
if (!
|
|
22607
|
-
dirs =
|
|
22824
|
+
if (!fs49.statSync(geminiTmpDir2).isDirectory()) return out;
|
|
22825
|
+
dirs = fs49.readdirSync(geminiTmpDir2);
|
|
22608
22826
|
} catch {
|
|
22609
22827
|
return out;
|
|
22610
22828
|
}
|
|
22611
22829
|
for (const proj of dirs) {
|
|
22612
|
-
const chatsDir =
|
|
22830
|
+
const chatsDir = path46.join(geminiTmpDir2, proj, "chats");
|
|
22613
22831
|
let files;
|
|
22614
22832
|
try {
|
|
22615
|
-
if (!
|
|
22616
|
-
files =
|
|
22833
|
+
if (!fs49.statSync(chatsDir).isDirectory()) continue;
|
|
22834
|
+
files = fs49.readdirSync(chatsDir);
|
|
22617
22835
|
} catch {
|
|
22618
22836
|
continue;
|
|
22619
22837
|
}
|
|
22620
22838
|
for (const f of files) {
|
|
22621
22839
|
if (!f.endsWith(".jsonl")) continue;
|
|
22622
|
-
out.push({ projectKey: proj, file:
|
|
22840
|
+
out.push({ projectKey: proj, file: path46.join(chatsDir, f) });
|
|
22623
22841
|
}
|
|
22624
22842
|
}
|
|
22625
22843
|
return out;
|
|
22626
22844
|
}
|
|
22627
22845
|
function loadGeminiCost(start, end, geminiTmpDir2) {
|
|
22628
22846
|
const acc = emptyGeminiAccumulator();
|
|
22629
|
-
if (!
|
|
22847
|
+
if (!fs49.existsSync(geminiTmpDir2)) return freezeGeminiCost(acc);
|
|
22630
22848
|
for (const { projectKey, file } of listGeminiSessionFiles2(geminiTmpDir2)) {
|
|
22631
22849
|
processGeminiCostFile(file, projectKey, start, end, acc);
|
|
22632
22850
|
}
|
|
@@ -22634,11 +22852,11 @@ function loadGeminiCost(start, end, geminiTmpDir2) {
|
|
|
22634
22852
|
}
|
|
22635
22853
|
function aggregateReportFromAudit(period, opts = {}) {
|
|
22636
22854
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
22637
|
-
const auditLogPath = opts.auditLogPath ??
|
|
22638
|
-
const claudeProjectsDir = opts.claudeProjectsDir ??
|
|
22639
|
-
const codexSessionsDir2 = opts.codexSessionsDir ??
|
|
22640
|
-
const geminiTmpDir2 = opts.geminiTmpDir ??
|
|
22641
|
-
const hasAuditFile =
|
|
22855
|
+
const auditLogPath = opts.auditLogPath ?? path46.join(os42.homedir(), ".node9", "audit.log");
|
|
22856
|
+
const claudeProjectsDir = opts.claudeProjectsDir ?? path46.join(os42.homedir(), ".claude", "projects");
|
|
22857
|
+
const codexSessionsDir2 = opts.codexSessionsDir ?? path46.join(os42.homedir(), ".codex", "sessions");
|
|
22858
|
+
const geminiTmpDir2 = opts.geminiTmpDir ?? path46.join(os42.homedir(), ".gemini", "tmp");
|
|
22859
|
+
const hasAuditFile = fs49.existsSync(auditLogPath);
|
|
22642
22860
|
const allEntries = opts.preloadedAuditEntries ?? parseAuditLog(auditLogPath);
|
|
22643
22861
|
const unackedDlp = allEntries.filter((e) => e.source === "response-dlp");
|
|
22644
22862
|
const { start, end } = getDateRange(period, now);
|
|
@@ -23337,8 +23555,8 @@ init_core();
|
|
|
23337
23555
|
init_daemon();
|
|
23338
23556
|
init_agent_wiring();
|
|
23339
23557
|
import chalk15 from "chalk";
|
|
23340
|
-
import
|
|
23341
|
-
import
|
|
23558
|
+
import fs50 from "fs";
|
|
23559
|
+
import path47 from "path";
|
|
23342
23560
|
import os43 from "os";
|
|
23343
23561
|
function printAgentSection(label2, hookPairs, wrapped) {
|
|
23344
23562
|
console.log(chalk15.bold(` ${label2}`));
|
|
@@ -23393,13 +23611,13 @@ function registerStatusCommand(program2) {
|
|
|
23393
23611
|
console.log("");
|
|
23394
23612
|
const modeLabel = settings.mode === "audit" ? chalk15.blue("audit") : settings.mode === "strict" ? chalk15.red("strict") : chalk15.white("standard");
|
|
23395
23613
|
console.log(` Mode: ${modeLabel}`);
|
|
23396
|
-
const projectConfig =
|
|
23397
|
-
const globalConfig =
|
|
23614
|
+
const projectConfig = path47.join(process.cwd(), "node9.config.json");
|
|
23615
|
+
const globalConfig = path47.join(os43.homedir(), ".node9", "config.json");
|
|
23398
23616
|
console.log(
|
|
23399
|
-
` Local: ${
|
|
23617
|
+
` Local: ${fs50.existsSync(projectConfig) ? chalk15.green("Active (node9.config.json)") : chalk15.gray("Not present")}`
|
|
23400
23618
|
);
|
|
23401
23619
|
console.log(
|
|
23402
|
-
` Global: ${
|
|
23620
|
+
` Global: ${fs50.existsSync(globalConfig) ? chalk15.green("Active (~/.node9/config.json)") : chalk15.gray("Not present")}`
|
|
23403
23621
|
);
|
|
23404
23622
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
23405
23623
|
console.log(
|
|
@@ -23445,8 +23663,8 @@ init_setup();
|
|
|
23445
23663
|
init_shields();
|
|
23446
23664
|
init_service();
|
|
23447
23665
|
import chalk16 from "chalk";
|
|
23448
|
-
import
|
|
23449
|
-
import
|
|
23666
|
+
import fs51 from "fs";
|
|
23667
|
+
import path48 from "path";
|
|
23450
23668
|
import os44 from "os";
|
|
23451
23669
|
import https5 from "https";
|
|
23452
23670
|
var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "project-jail"];
|
|
@@ -23533,32 +23751,32 @@ function registerInitCommand(program2) {
|
|
|
23533
23751
|
}
|
|
23534
23752
|
console.log("");
|
|
23535
23753
|
}
|
|
23536
|
-
const
|
|
23537
|
-
const isFirstInstall = !
|
|
23538
|
-
if (
|
|
23754
|
+
const configPath = path48.join(os44.homedir(), ".node9", "config.json");
|
|
23755
|
+
const isFirstInstall = !fs51.existsSync(configPath);
|
|
23756
|
+
if (fs51.existsSync(configPath) && !options.force) {
|
|
23539
23757
|
try {
|
|
23540
|
-
const existing = JSON.parse(
|
|
23758
|
+
const existing = JSON.parse(fs51.readFileSync(configPath, "utf-8"));
|
|
23541
23759
|
const settings = existing.settings ?? {};
|
|
23542
23760
|
if (settings.mode !== chosenMode) {
|
|
23543
23761
|
settings.mode = chosenMode;
|
|
23544
23762
|
existing.settings = settings;
|
|
23545
|
-
|
|
23763
|
+
fs51.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
|
|
23546
23764
|
console.log(chalk16.green(`\u2705 Mode updated: ${chosenMode}`));
|
|
23547
23765
|
} else {
|
|
23548
|
-
console.log(chalk16.blue(`\u2139\uFE0F Config already exists: ${
|
|
23766
|
+
console.log(chalk16.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
23549
23767
|
}
|
|
23550
23768
|
} catch {
|
|
23551
|
-
console.log(chalk16.blue(`\u2139\uFE0F Config already exists: ${
|
|
23769
|
+
console.log(chalk16.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
23552
23770
|
}
|
|
23553
23771
|
} else {
|
|
23554
23772
|
const configToSave = {
|
|
23555
23773
|
...DEFAULT_CONFIG,
|
|
23556
23774
|
settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
|
|
23557
23775
|
};
|
|
23558
|
-
const dir =
|
|
23559
|
-
if (!
|
|
23560
|
-
|
|
23561
|
-
console.log(chalk16.green(`\u2705 Config created: ${
|
|
23776
|
+
const dir = path48.dirname(configPath);
|
|
23777
|
+
if (!fs51.existsSync(dir)) fs51.mkdirSync(dir, { recursive: true });
|
|
23778
|
+
fs51.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
|
|
23779
|
+
console.log(chalk16.green(`\u2705 Config created: ${configPath}`));
|
|
23562
23780
|
console.log(chalk16.gray(` Mode: ${chosenMode}`));
|
|
23563
23781
|
}
|
|
23564
23782
|
if (options.skipSetup) return;
|
|
@@ -23662,7 +23880,7 @@ function registerInitCommand(program2) {
|
|
|
23662
23880
|
}
|
|
23663
23881
|
|
|
23664
23882
|
// src/cli/commands/undo.ts
|
|
23665
|
-
import
|
|
23883
|
+
import path49 from "path";
|
|
23666
23884
|
import chalk18 from "chalk";
|
|
23667
23885
|
|
|
23668
23886
|
// src/tui/undo-navigator.ts
|
|
@@ -23821,7 +24039,7 @@ function findMatchingCwd(startDir, history) {
|
|
|
23821
24039
|
let dir = startDir;
|
|
23822
24040
|
while (true) {
|
|
23823
24041
|
if (cwds.has(dir)) return dir;
|
|
23824
|
-
const parent =
|
|
24042
|
+
const parent = path49.dirname(dir);
|
|
23825
24043
|
if (parent === dir) return null;
|
|
23826
24044
|
dir = parent;
|
|
23827
24045
|
}
|
|
@@ -24456,13 +24674,83 @@ function registerMcpGatewayCommand(program2) {
|
|
|
24456
24674
|
|
|
24457
24675
|
// src/mcp-server/index.ts
|
|
24458
24676
|
import readline5 from "readline";
|
|
24459
|
-
import
|
|
24460
|
-
import
|
|
24461
|
-
import
|
|
24677
|
+
import fs53 from "fs";
|
|
24678
|
+
import os46 from "os";
|
|
24679
|
+
import path51 from "path";
|
|
24462
24680
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
24463
24681
|
init_core();
|
|
24464
24682
|
init_daemon();
|
|
24465
24683
|
init_shields();
|
|
24684
|
+
|
|
24685
|
+
// src/auth/egress-config.ts
|
|
24686
|
+
import fs52 from "fs";
|
|
24687
|
+
import os45 from "os";
|
|
24688
|
+
import path50 from "path";
|
|
24689
|
+
var DEFAULT_EGRESS = {
|
|
24690
|
+
enabled: false,
|
|
24691
|
+
mode: "review",
|
|
24692
|
+
allow: [],
|
|
24693
|
+
deny: [],
|
|
24694
|
+
allowPrivate: true
|
|
24695
|
+
};
|
|
24696
|
+
function egressConfigPath() {
|
|
24697
|
+
return path50.join(os45.homedir(), ".node9", "config.json");
|
|
24698
|
+
}
|
|
24699
|
+
function readEgressRawConfig() {
|
|
24700
|
+
let text;
|
|
24701
|
+
try {
|
|
24702
|
+
text = fs52.readFileSync(egressConfigPath(), "utf8");
|
|
24703
|
+
} catch (err2) {
|
|
24704
|
+
if (err2.code === "ENOENT") return {};
|
|
24705
|
+
throw err2;
|
|
24706
|
+
}
|
|
24707
|
+
try {
|
|
24708
|
+
return JSON.parse(text);
|
|
24709
|
+
} catch {
|
|
24710
|
+
throw new Error(
|
|
24711
|
+
`${egressConfigPath()} is not valid JSON \u2014 fix it before changing egress (refusing to overwrite).`
|
|
24712
|
+
);
|
|
24713
|
+
}
|
|
24714
|
+
}
|
|
24715
|
+
function writeEgressRawConfig(config) {
|
|
24716
|
+
const p = egressConfigPath();
|
|
24717
|
+
fs52.mkdirSync(path50.dirname(p), { recursive: true });
|
|
24718
|
+
fs52.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
|
|
24719
|
+
}
|
|
24720
|
+
function applyEgress(config, change) {
|
|
24721
|
+
const policy = config.policy = config.policy ?? {};
|
|
24722
|
+
const existing = policy.egress ?? {};
|
|
24723
|
+
policy.egress = { ...DEFAULT_EGRESS, ...existing, ...change };
|
|
24724
|
+
return config;
|
|
24725
|
+
}
|
|
24726
|
+
function getEgress() {
|
|
24727
|
+
const raw = readEgressRawConfig();
|
|
24728
|
+
const existing = raw.policy?.egress ?? {};
|
|
24729
|
+
return { ...DEFAULT_EGRESS, ...existing };
|
|
24730
|
+
}
|
|
24731
|
+
function setEgress(change) {
|
|
24732
|
+
const config = readEgressRawConfig();
|
|
24733
|
+
applyEgress(config, change);
|
|
24734
|
+
writeEgressRawConfig(config);
|
|
24735
|
+
}
|
|
24736
|
+
function addEgressHost(list, host) {
|
|
24737
|
+
const config = readEgressRawConfig();
|
|
24738
|
+
const existing = config.policy?.egress ?? {};
|
|
24739
|
+
const current = { ...DEFAULT_EGRESS, ...existing };
|
|
24740
|
+
const updated = current[list].includes(host) ? current[list] : [...current[list], host];
|
|
24741
|
+
applyEgress(config, { [list]: updated });
|
|
24742
|
+
writeEgressRawConfig(config);
|
|
24743
|
+
}
|
|
24744
|
+
function normalizeEgressHost(host) {
|
|
24745
|
+
return host.trim().toLowerCase();
|
|
24746
|
+
}
|
|
24747
|
+
var EGRESS_HOST_RE = /^(\*\.)?[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/;
|
|
24748
|
+
function isValidEgressHost(host) {
|
|
24749
|
+
return EGRESS_HOST_RE.test(host);
|
|
24750
|
+
}
|
|
24751
|
+
|
|
24752
|
+
// src/mcp-server/index.ts
|
|
24753
|
+
init_dist();
|
|
24466
24754
|
function ok(id, result) {
|
|
24467
24755
|
return JSON.stringify({ jsonrpc: "2.0", id: id ?? null, result });
|
|
24468
24756
|
}
|
|
@@ -24473,12 +24761,20 @@ var TOOL_CAPABILITY = {
|
|
|
24473
24761
|
// weaken — gated over MCP
|
|
24474
24762
|
node9_shield_disable: "weaken",
|
|
24475
24763
|
node9_approver_set: "weaken",
|
|
24764
|
+
// NOTE: egress LOOSENING (allow a host / turn egress off) is intentionally NOT
|
|
24765
|
+
// exposed over MCP — it has no legitimate agent use case (it's exactly the
|
|
24766
|
+
// exfil-exit the egress gate exists to prevent) and would be attack surface
|
|
24767
|
+
// even gated. A human loosens egress at the CLI: `node9 egress allow|off`.
|
|
24476
24768
|
// add / restorative — always allowed
|
|
24477
24769
|
node9_shield_enable: "add",
|
|
24478
24770
|
node9_rule_add: "add",
|
|
24479
24771
|
// already block/review-only — handleRuleAdd rejects "allow"
|
|
24480
24772
|
node9_undo_revert: "add",
|
|
24481
24773
|
// restorative
|
|
24774
|
+
node9_egress_protect: "add",
|
|
24775
|
+
// enable/strengthen egress (monotonic — never reduces)
|
|
24776
|
+
node9_egress_deny: "add",
|
|
24777
|
+
// add a deny host (deny always wins)
|
|
24482
24778
|
// readonly
|
|
24483
24779
|
node9_status: "readonly",
|
|
24484
24780
|
node9_config_get: "readonly",
|
|
@@ -24492,7 +24788,8 @@ var TOOL_CAPABILITY = {
|
|
|
24492
24788
|
node9_shield_list: "readonly",
|
|
24493
24789
|
node9_approver_list: "readonly",
|
|
24494
24790
|
node9_undo_list: "readonly",
|
|
24495
|
-
node9_undo_detail: "readonly"
|
|
24791
|
+
node9_undo_detail: "readonly",
|
|
24792
|
+
node9_egress_status: "readonly"
|
|
24496
24793
|
};
|
|
24497
24794
|
function capabilityOf(tool) {
|
|
24498
24795
|
return TOOL_CAPABILITY[tool] ?? "readonly";
|
|
@@ -24748,7 +25045,41 @@ var TOOLS = [
|
|
|
24748
25045
|
},
|
|
24749
25046
|
required: ["name", "tool", "field", "pattern", "verdict", "reason"]
|
|
24750
25047
|
}
|
|
25048
|
+
},
|
|
25049
|
+
{
|
|
25050
|
+
name: "node9_egress_status",
|
|
25051
|
+
description: "Show egress (outbound network) control: whether it is enabled, the mode (off / review / block), and your allow + deny host lists. Common dev/LLM hosts (github, npm, pypi, anthropic, \u2026) are always allowed by a built-in list. Read-only.",
|
|
25052
|
+
inputSchema: { type: "object", properties: {}, required: [] }
|
|
25053
|
+
},
|
|
25054
|
+
{
|
|
25055
|
+
name: "node9_egress_protect",
|
|
25056
|
+
description: 'Turn on egress control (or strengthen it). mode="review" prompts on unknown hosts; mode="block" hard-blocks them. Monotonic \u2014 it only ever ADDS protection and never reduces it (a request for review will not downgrade an existing block). Only adds protection, so it is always allowed over MCP.',
|
|
25057
|
+
inputSchema: {
|
|
25058
|
+
type: "object",
|
|
25059
|
+
properties: {
|
|
25060
|
+
mode: {
|
|
25061
|
+
type: "string",
|
|
25062
|
+
enum: ["review", "block"],
|
|
25063
|
+
description: 'Enforcement to apply. "review" = prompt, "block" = deny. Defaults to review.'
|
|
25064
|
+
}
|
|
25065
|
+
},
|
|
25066
|
+
required: []
|
|
25067
|
+
}
|
|
25068
|
+
},
|
|
25069
|
+
{
|
|
25070
|
+
name: "node9_egress_deny",
|
|
25071
|
+
description: "Add a host to the egress deny list (deny always wins over allow). Only adds protection, so it is always allowed over MCP. Host is an FQDN or wildcard glob (e.g. evil.com or *.evil.com).",
|
|
25072
|
+
inputSchema: {
|
|
25073
|
+
type: "object",
|
|
25074
|
+
properties: {
|
|
25075
|
+
host: { type: "string", description: "Host to deny (FQDN or *.glob)." }
|
|
25076
|
+
},
|
|
25077
|
+
required: ["host"]
|
|
25078
|
+
}
|
|
24751
25079
|
}
|
|
25080
|
+
// Egress LOOSENING (allow a host / turn egress off) is deliberately CLI-only —
|
|
25081
|
+
// see the note in TOOL_CAPABILITY. The agent can see and tighten egress over
|
|
25082
|
+
// MCP, but never loosen it.
|
|
24752
25083
|
];
|
|
24753
25084
|
function handleStatus() {
|
|
24754
25085
|
const config = getConfig();
|
|
@@ -24769,13 +25100,13 @@ function handleStatus() {
|
|
|
24769
25100
|
lines.push(`Active shields: ${activeShields.length > 0 ? activeShields.join(", ") : "none"}`);
|
|
24770
25101
|
lines.push(`Smart rules: ${config.policy.smartRules.length} loaded`);
|
|
24771
25102
|
lines.push(`DLP: ${config.policy.dlp?.enabled !== false ? "enabled" : "disabled"}`);
|
|
24772
|
-
const projectConfig =
|
|
24773
|
-
const globalConfig =
|
|
25103
|
+
const projectConfig = path51.join(process.cwd(), "node9.config.json");
|
|
25104
|
+
const globalConfig = path51.join(os46.homedir(), ".node9", "config.json");
|
|
24774
25105
|
lines.push(
|
|
24775
|
-
`Project config (node9.config.json): ${
|
|
25106
|
+
`Project config (node9.config.json): ${fs53.existsSync(projectConfig) ? "present" : "not found"}`
|
|
24776
25107
|
);
|
|
24777
25108
|
lines.push(
|
|
24778
|
-
`Global config (~/.node9/config.json): ${
|
|
25109
|
+
`Global config (~/.node9/config.json): ${fs53.existsSync(globalConfig) ? "present" : "not found"}`
|
|
24779
25110
|
);
|
|
24780
25111
|
return lines.join("\n");
|
|
24781
25112
|
}
|
|
@@ -24849,21 +25180,53 @@ function handleShieldDisable(args) {
|
|
|
24849
25180
|
writeActiveShields(active.filter((s) => s !== name));
|
|
24850
25181
|
return `Shield "${name}" disabled.`;
|
|
24851
25182
|
}
|
|
24852
|
-
|
|
25183
|
+
function handleEgressStatus() {
|
|
25184
|
+
const e = getConfig().policy.egress;
|
|
25185
|
+
const state = !e.enabled ? "OFF \u2014 the agent can reach any host" : e.mode === "block" ? "LOCKED (block) \u2014 unknown hosts are denied" : "WATCHING (review) \u2014 unknown hosts prompt for approval";
|
|
25186
|
+
const lines = [
|
|
25187
|
+
`Egress control: ${state}`,
|
|
25188
|
+
`${DEFAULT_EGRESS_ALLOWLIST.length} common dev/LLM hosts are always allowed (github, npm, pypi, anthropic, \u2026).`,
|
|
25189
|
+
`Your allow list: ${e.allow.length ? e.allow.join(", ") : "(none)"}`,
|
|
25190
|
+
`Your deny list: ${e.deny.length ? e.deny.join(", ") : "(none)"}`
|
|
25191
|
+
];
|
|
25192
|
+
return lines.join("\n");
|
|
25193
|
+
}
|
|
25194
|
+
function handleEgressProtect(args) {
|
|
25195
|
+
const rawMode = args.mode;
|
|
25196
|
+
if (rawMode !== void 0 && rawMode !== "review" && rawMode !== "block") {
|
|
25197
|
+
throw new Error('mode must be "review" or "block".');
|
|
25198
|
+
}
|
|
25199
|
+
const requested = rawMode ?? "review";
|
|
25200
|
+
const current = getEgress();
|
|
25201
|
+
const mode = current.enabled && current.mode === "block" ? "block" : requested;
|
|
25202
|
+
setEgress({ enabled: true, mode });
|
|
25203
|
+
return mode === "block" ? "Egress is now LOCKED (block) \u2014 unknown hosts are denied. Routine hosts stay allowed." : "Egress is now WATCHED (review) \u2014 unknown hosts prompt for approval.";
|
|
25204
|
+
}
|
|
25205
|
+
function handleEgressDeny(args) {
|
|
25206
|
+
const host = typeof args.host === "string" ? normalizeEgressHost(args.host) : "";
|
|
25207
|
+
if (!isValidEgressHost(host)) {
|
|
25208
|
+
throw new Error(
|
|
25209
|
+
`Invalid host: "${String(args.host)}". Use an FQDN or *.glob (e.g. *.evil.com).`
|
|
25210
|
+
);
|
|
25211
|
+
}
|
|
25212
|
+
addEgressHost("deny", host);
|
|
25213
|
+
return `Denied egress to ${host} (deny always wins over allow).`;
|
|
25214
|
+
}
|
|
25215
|
+
var GLOBAL_CONFIG_PATH = path51.join(os46.homedir(), ".node9", "config.json");
|
|
24853
25216
|
var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
|
|
24854
25217
|
function readGlobalConfigRaw() {
|
|
24855
25218
|
try {
|
|
24856
|
-
if (
|
|
24857
|
-
return JSON.parse(
|
|
25219
|
+
if (fs53.existsSync(GLOBAL_CONFIG_PATH)) {
|
|
25220
|
+
return JSON.parse(fs53.readFileSync(GLOBAL_CONFIG_PATH, "utf-8"));
|
|
24858
25221
|
}
|
|
24859
25222
|
} catch {
|
|
24860
25223
|
}
|
|
24861
25224
|
return {};
|
|
24862
25225
|
}
|
|
24863
25226
|
function writeGlobalConfigRaw(data) {
|
|
24864
|
-
const dir =
|
|
24865
|
-
if (!
|
|
24866
|
-
|
|
25227
|
+
const dir = path51.dirname(GLOBAL_CONFIG_PATH);
|
|
25228
|
+
if (!fs53.existsSync(dir)) fs53.mkdirSync(dir, { recursive: true });
|
|
25229
|
+
fs53.writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(data, null, 2) + "\n");
|
|
24867
25230
|
}
|
|
24868
25231
|
function handleApproverList() {
|
|
24869
25232
|
const config = getConfig();
|
|
@@ -24907,9 +25270,9 @@ function handleApproverSet(args) {
|
|
|
24907
25270
|
function handleAuditGet(args) {
|
|
24908
25271
|
const limit = Math.min(typeof args.limit === "number" ? args.limit : 20, 100);
|
|
24909
25272
|
const filter = typeof args.filter === "string" && args.filter !== "all" ? args.filter : null;
|
|
24910
|
-
const auditPath =
|
|
24911
|
-
if (!
|
|
24912
|
-
const rawLines =
|
|
25273
|
+
const auditPath = path51.join(os46.homedir(), ".node9", "audit.log");
|
|
25274
|
+
if (!fs53.existsSync(auditPath)) return "No audit log found.";
|
|
25275
|
+
const rawLines = fs53.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
|
|
24913
25276
|
const parsed = [];
|
|
24914
25277
|
for (const line of rawLines) {
|
|
24915
25278
|
try {
|
|
@@ -25187,6 +25550,12 @@ function runMcpServer() {
|
|
|
25187
25550
|
text = handlePostureMcp(toolArgs);
|
|
25188
25551
|
} else if (toolName === "node9_explain") {
|
|
25189
25552
|
text = handleExplainMcp(toolArgs);
|
|
25553
|
+
} else if (toolName === "node9_egress_status") {
|
|
25554
|
+
text = handleEgressStatus();
|
|
25555
|
+
} else if (toolName === "node9_egress_protect") {
|
|
25556
|
+
text = handleEgressProtect(toolArgs);
|
|
25557
|
+
} else if (toolName === "node9_egress_deny") {
|
|
25558
|
+
text = handleEgressDeny(toolArgs);
|
|
25190
25559
|
} else {
|
|
25191
25560
|
process.stdout.write(err(id, -32601, `Unknown tool: ${toolName}`) + "\n");
|
|
25192
25561
|
return;
|
|
@@ -25277,7 +25646,7 @@ function registerTrustCommand(program2) {
|
|
|
25277
25646
|
// src/cli/commands/mcp-pin.ts
|
|
25278
25647
|
init_mcp_pin();
|
|
25279
25648
|
import chalk21 from "chalk";
|
|
25280
|
-
import
|
|
25649
|
+
import fs54 from "fs";
|
|
25281
25650
|
function registerMcpPinCommand(program2) {
|
|
25282
25651
|
const pinCmd = program2.command("mcp").description("Manage MCP server tool definition pinning (rug pull defense)");
|
|
25283
25652
|
const pinSubCmd = pinCmd.command("pin").description("Manage pinned MCP server tool definitions");
|
|
@@ -25288,7 +25657,7 @@ function registerMcpPinCommand(program2) {
|
|
|
25288
25657
|
let repoCorrupt = false;
|
|
25289
25658
|
if (found.source === "repo") {
|
|
25290
25659
|
try {
|
|
25291
|
-
const raw =
|
|
25660
|
+
const raw = fs54.readFileSync(found.path, "utf-8");
|
|
25292
25661
|
const parsed = JSON.parse(raw);
|
|
25293
25662
|
repoEntries = parsed.servers ?? {};
|
|
25294
25663
|
} catch {
|
|
@@ -25777,50 +26146,10 @@ function registerPostureCommand(program2) {
|
|
|
25777
26146
|
init_config();
|
|
25778
26147
|
init_dist();
|
|
25779
26148
|
import chalk26 from "chalk";
|
|
25780
|
-
|
|
25781
|
-
import os46 from "os";
|
|
25782
|
-
import path50 from "path";
|
|
25783
|
-
var DEFAULT_EGRESS = {
|
|
25784
|
-
enabled: false,
|
|
25785
|
-
mode: "review",
|
|
25786
|
-
allow: [],
|
|
25787
|
-
deny: [],
|
|
25788
|
-
allowPrivate: true
|
|
25789
|
-
};
|
|
25790
|
-
function configPath() {
|
|
25791
|
-
return path50.join(os46.homedir(), ".node9", "config.json");
|
|
25792
|
-
}
|
|
25793
|
-
function readRawConfig() {
|
|
25794
|
-
let text;
|
|
25795
|
-
try {
|
|
25796
|
-
text = fs52.readFileSync(configPath(), "utf8");
|
|
25797
|
-
} catch (err2) {
|
|
25798
|
-
if (err2.code === "ENOENT") return {};
|
|
25799
|
-
throw err2;
|
|
25800
|
-
}
|
|
25801
|
-
try {
|
|
25802
|
-
return JSON.parse(text);
|
|
25803
|
-
} catch {
|
|
25804
|
-
throw new Error(
|
|
25805
|
-
`${configPath()} is not valid JSON \u2014 fix it before changing egress (refusing to overwrite).`
|
|
25806
|
-
);
|
|
25807
|
-
}
|
|
25808
|
-
}
|
|
25809
|
-
function writeRawConfig(config) {
|
|
25810
|
-
const p = configPath();
|
|
25811
|
-
fs52.mkdirSync(path50.dirname(p), { recursive: true });
|
|
25812
|
-
fs52.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
|
|
25813
|
-
}
|
|
25814
|
-
function applyEgress(config, change) {
|
|
25815
|
-
const policy = config.policy = config.policy ?? {};
|
|
25816
|
-
const existing = policy.egress ?? {};
|
|
25817
|
-
policy.egress = { ...DEFAULT_EGRESS, ...existing, ...change };
|
|
25818
|
-
return config;
|
|
25819
|
-
}
|
|
25820
|
-
function withConfig(fn) {
|
|
25821
|
-
let config;
|
|
26149
|
+
function guard(fn) {
|
|
25822
26150
|
try {
|
|
25823
|
-
|
|
26151
|
+
fn();
|
|
26152
|
+
return true;
|
|
25824
26153
|
} catch (err2) {
|
|
25825
26154
|
console.error(chalk26.red(`
|
|
25826
26155
|
\u2717 ${err2.message}
|
|
@@ -25828,20 +26157,12 @@ function withConfig(fn) {
|
|
|
25828
26157
|
process.exitCode = 1;
|
|
25829
26158
|
return false;
|
|
25830
26159
|
}
|
|
25831
|
-
fn(config);
|
|
25832
|
-
writeRawConfig(config);
|
|
25833
|
-
return true;
|
|
25834
26160
|
}
|
|
25835
26161
|
function mutate(change) {
|
|
25836
|
-
return
|
|
26162
|
+
return guard(() => setEgress(change));
|
|
25837
26163
|
}
|
|
25838
26164
|
function addHost(list, host) {
|
|
25839
|
-
return
|
|
25840
|
-
const existing = config.policy?.egress ?? {};
|
|
25841
|
-
const current = { ...DEFAULT_EGRESS, ...existing };
|
|
25842
|
-
const updated = current[list].includes(host) ? current[list] : [...current[list], host];
|
|
25843
|
-
applyEgress(config, { [list]: updated });
|
|
25844
|
-
});
|
|
26165
|
+
return guard(() => addEgressHost(list, host));
|
|
25845
26166
|
}
|
|
25846
26167
|
function showStatus() {
|
|
25847
26168
|
const e = getConfig().policy.egress;
|
|
@@ -25900,16 +26221,191 @@ function registerEgressCommand(program2) {
|
|
|
25900
26221
|
egress.action(showStatus);
|
|
25901
26222
|
}
|
|
25902
26223
|
|
|
25903
|
-
// src/cli/commands/
|
|
25904
|
-
init_config();
|
|
26224
|
+
// src/cli/commands/jail.ts
|
|
25905
26225
|
import chalk27 from "chalk";
|
|
26226
|
+
|
|
26227
|
+
// src/shields/jail.ts
|
|
25906
26228
|
import fs55 from "fs";
|
|
25907
|
-
import
|
|
26229
|
+
import os47 from "os";
|
|
26230
|
+
import path52 from "path";
|
|
26231
|
+
init_shields();
|
|
26232
|
+
var USER_JAIL_SHIELD = "user-jail";
|
|
26233
|
+
function jailStorePath() {
|
|
26234
|
+
return path52.join(os47.homedir(), ".node9", "jail-paths.json");
|
|
26235
|
+
}
|
|
26236
|
+
function readJailPaths() {
|
|
26237
|
+
let text;
|
|
26238
|
+
try {
|
|
26239
|
+
text = fs55.readFileSync(jailStorePath(), "utf8");
|
|
26240
|
+
} catch (err2) {
|
|
26241
|
+
if (err2.code === "ENOENT") return [];
|
|
26242
|
+
throw err2;
|
|
26243
|
+
}
|
|
26244
|
+
let parsed;
|
|
26245
|
+
try {
|
|
26246
|
+
parsed = JSON.parse(text);
|
|
26247
|
+
} catch {
|
|
26248
|
+
throw new Error(`${jailStorePath()} is not valid JSON \u2014 fix it before changing the jail.`);
|
|
26249
|
+
}
|
|
26250
|
+
if (!Array.isArray(parsed.paths)) return [];
|
|
26251
|
+
return parsed.paths.filter(
|
|
26252
|
+
(p) => !!p && typeof p.path === "string" && (p.verdict === "block" || p.verdict === "review")
|
|
26253
|
+
);
|
|
26254
|
+
}
|
|
26255
|
+
function writeJailPaths(paths) {
|
|
26256
|
+
const p = jailStorePath();
|
|
26257
|
+
fs55.mkdirSync(path52.dirname(p), { recursive: true });
|
|
26258
|
+
fs55.writeFileSync(p, JSON.stringify({ paths }, null, 2) + "\n", { mode: 384 });
|
|
26259
|
+
}
|
|
26260
|
+
function addJailPath(rawPath, verdict) {
|
|
26261
|
+
const norm = rawPath.trim();
|
|
26262
|
+
if (!pathToRegexFragment(norm)) {
|
|
26263
|
+
throw new Error(
|
|
26264
|
+
`"${rawPath}" is too broad to jail \u2014 give a specific path (e.g. ~/.gmail-mcp), not a home or root directory.`
|
|
26265
|
+
);
|
|
26266
|
+
}
|
|
26267
|
+
const next = [...readJailPaths().filter((p) => p.path !== norm), { path: norm, verdict }];
|
|
26268
|
+
writeJailPaths(next);
|
|
26269
|
+
return next;
|
|
26270
|
+
}
|
|
26271
|
+
function removeJailPath(rawPath) {
|
|
26272
|
+
const norm = rawPath.trim();
|
|
26273
|
+
const before = readJailPaths();
|
|
26274
|
+
const after = before.filter((p) => p.path !== norm);
|
|
26275
|
+
const removed = after.length !== before.length;
|
|
26276
|
+
if (removed) writeJailPaths(after);
|
|
26277
|
+
return { removed, paths: after };
|
|
26278
|
+
}
|
|
26279
|
+
function regenerateUserJail(paths) {
|
|
26280
|
+
const file = path52.join(USER_SHIELDS_DIR_PATH, `${USER_JAIL_SHIELD}.json`);
|
|
26281
|
+
if (paths.length === 0) {
|
|
26282
|
+
const active2 = readActiveShields();
|
|
26283
|
+
if (active2.includes(USER_JAIL_SHIELD)) {
|
|
26284
|
+
writeActiveShields(active2.filter((s) => s !== USER_JAIL_SHIELD));
|
|
26285
|
+
}
|
|
26286
|
+
try {
|
|
26287
|
+
fs55.rmSync(file, { force: true });
|
|
26288
|
+
} catch {
|
|
26289
|
+
}
|
|
26290
|
+
return;
|
|
26291
|
+
}
|
|
26292
|
+
const def = buildShield({
|
|
26293
|
+
name: USER_JAIL_SHIELD,
|
|
26294
|
+
description: "User-added credential jail paths (node9 jail add)",
|
|
26295
|
+
blockPaths: paths.filter((p) => p.verdict === "block").map((p) => p.path),
|
|
26296
|
+
reviewPaths: paths.filter((p) => p.verdict === "review").map((p) => p.path)
|
|
26297
|
+
});
|
|
26298
|
+
installShield(USER_JAIL_SHIELD, def);
|
|
26299
|
+
const active = readActiveShields();
|
|
26300
|
+
if (!active.includes(USER_JAIL_SHIELD)) writeActiveShields([...active, USER_JAIL_SHIELD]);
|
|
26301
|
+
}
|
|
26302
|
+
|
|
26303
|
+
// src/cli/commands/jail.ts
|
|
26304
|
+
init_audit();
|
|
26305
|
+
var BUILTIN_JAIL = [
|
|
26306
|
+
"~/.ssh \u2014 SSH private keys",
|
|
26307
|
+
"~/.aws \u2014 AWS credentials",
|
|
26308
|
+
".env files",
|
|
26309
|
+
"credential files \u2014 credentials.json, .netrc, .npmrc, .docker, .kube, gcloud"
|
|
26310
|
+
];
|
|
26311
|
+
function registerJailCommand(program2) {
|
|
26312
|
+
const jail = program2.command("jail").description("Grow the credential jail \u2014 block/review reads of your sensitive paths");
|
|
26313
|
+
jail.command("add <path>").description("Add a path to the jail (default: block reads; --review to soften)").option("--review", "Require human approval instead of hard-blocking reads").action((p, opts) => {
|
|
26314
|
+
const verdict = opts.review ? "review" : "block";
|
|
26315
|
+
try {
|
|
26316
|
+
const paths = addJailPath(p, verdict);
|
|
26317
|
+
regenerateUserJail(paths);
|
|
26318
|
+
appendConfigAudit({ event: "jail-add", path: p.trim(), verdict });
|
|
26319
|
+
} catch (err2) {
|
|
26320
|
+
console.error(chalk27.red(`
|
|
26321
|
+
\u274C ${err2.message}
|
|
26322
|
+
`));
|
|
26323
|
+
process.exit(1);
|
|
26324
|
+
return;
|
|
26325
|
+
}
|
|
26326
|
+
console.log(chalk27.green(`
|
|
26327
|
+
\u2705 Jailed ${p} (${verdict}).`));
|
|
26328
|
+
console.log(
|
|
26329
|
+
chalk27.gray(
|
|
26330
|
+
` AI reads of this path now ${verdict === "block" ? "BLOCK" : "require approval"}.`
|
|
26331
|
+
)
|
|
26332
|
+
);
|
|
26333
|
+
console.log(chalk27.gray(` Preview: ${chalk27.cyan(`node9 explain bash "cat ${p}"`)}
|
|
26334
|
+
`));
|
|
26335
|
+
});
|
|
26336
|
+
jail.command("remove <path>").description("Remove a user-added jail path (built-in paths are not removable)").action((p) => {
|
|
26337
|
+
let result;
|
|
26338
|
+
try {
|
|
26339
|
+
result = removeJailPath(p);
|
|
26340
|
+
} catch (err2) {
|
|
26341
|
+
console.error(chalk27.red(`
|
|
26342
|
+
\u274C ${err2.message}
|
|
26343
|
+
`));
|
|
26344
|
+
process.exit(1);
|
|
26345
|
+
return;
|
|
26346
|
+
}
|
|
26347
|
+
if (!result.removed) {
|
|
26348
|
+
console.error(chalk27.yellow(`
|
|
26349
|
+
\u2139\uFE0F "${p}" is not a user-added jail path.
|
|
26350
|
+
`));
|
|
26351
|
+
console.error(chalk27.gray(` Run ${chalk27.cyan("node9 jail list")} to see your paths.
|
|
26352
|
+
`));
|
|
26353
|
+
process.exit(1);
|
|
26354
|
+
return;
|
|
26355
|
+
}
|
|
26356
|
+
try {
|
|
26357
|
+
regenerateUserJail(result.paths);
|
|
26358
|
+
appendConfigAudit({ event: "jail-remove", path: p.trim() });
|
|
26359
|
+
} catch (err2) {
|
|
26360
|
+
console.error(chalk27.red(`
|
|
26361
|
+
\u274C ${err2.message}
|
|
26362
|
+
`));
|
|
26363
|
+
process.exit(1);
|
|
26364
|
+
return;
|
|
26365
|
+
}
|
|
26366
|
+
console.log(chalk27.green(`
|
|
26367
|
+
\u2705 Removed ${p} from the jail.
|
|
26368
|
+
`));
|
|
26369
|
+
});
|
|
26370
|
+
jail.command("list").description("Show built-in + user-added jail paths").action(() => {
|
|
26371
|
+
console.log(chalk27.bold("\n\u{1F512} Credential Jail\n"));
|
|
26372
|
+
console.log(chalk27.gray(" Built-in (always on, not removable):"));
|
|
26373
|
+
for (const b of BUILTIN_JAIL) console.log(` ${chalk27.gray("\u2022")} ${b}`);
|
|
26374
|
+
console.log("");
|
|
26375
|
+
let user;
|
|
26376
|
+
try {
|
|
26377
|
+
user = readJailPaths();
|
|
26378
|
+
} catch (err2) {
|
|
26379
|
+
console.error(chalk27.red(` \u2717 ${err2.message}
|
|
26380
|
+
`));
|
|
26381
|
+
process.exit(1);
|
|
26382
|
+
return;
|
|
26383
|
+
}
|
|
26384
|
+
if (user.length === 0) {
|
|
26385
|
+
console.log(
|
|
26386
|
+
chalk27.gray(" Your paths: (none) \u2014 add one with ") + chalk27.cyan("node9 jail add <path>")
|
|
26387
|
+
);
|
|
26388
|
+
} else {
|
|
26389
|
+
console.log(chalk27.gray(" Your paths (removable):"));
|
|
26390
|
+
for (const u of user) {
|
|
26391
|
+
const v = u.verdict === "block" ? chalk27.red("block ") : chalk27.yellow("review");
|
|
26392
|
+
console.log(` ${v} ${chalk27.cyan(u.path)}`);
|
|
26393
|
+
}
|
|
26394
|
+
}
|
|
26395
|
+
console.log("");
|
|
26396
|
+
});
|
|
26397
|
+
}
|
|
26398
|
+
|
|
26399
|
+
// src/cli/commands/sandbox.ts
|
|
26400
|
+
init_config();
|
|
26401
|
+
import chalk28 from "chalk";
|
|
26402
|
+
import fs58 from "fs";
|
|
26403
|
+
import path55 from "path";
|
|
25908
26404
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
25909
26405
|
|
|
25910
26406
|
// src/sandbox/config.ts
|
|
25911
|
-
import
|
|
25912
|
-
import
|
|
26407
|
+
import fs56 from "fs";
|
|
26408
|
+
import path53 from "path";
|
|
25913
26409
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
25914
26410
|
var SANDBOX_CONFIG_FILE = "node9.sandbox.yaml";
|
|
25915
26411
|
var FORBIDDEN_ENV = /* @__PURE__ */ new Set(["NODE9_API_KEY", "NODE9_API_URL"]);
|
|
@@ -25982,16 +26478,16 @@ function scaffoldSandboxYaml(agent) {
|
|
|
25982
26478
|
return header + stringifyYaml(defaultSandboxConfig(agent));
|
|
25983
26479
|
}
|
|
25984
26480
|
function sandboxConfigPath(cwd = process.cwd()) {
|
|
25985
|
-
return
|
|
26481
|
+
return path53.join(cwd, SANDBOX_CONFIG_FILE);
|
|
25986
26482
|
}
|
|
25987
26483
|
function loadSandboxConfig(cwd = process.cwd(), fallbackAgent = "claude") {
|
|
25988
26484
|
const p = sandboxConfigPath(cwd);
|
|
25989
|
-
if (!
|
|
26485
|
+
if (!fs56.existsSync(p)) {
|
|
25990
26486
|
throw new Error(`sandbox: ${SANDBOX_CONFIG_FILE} not found \u2014 run \`node9 sandbox new\` first.`);
|
|
25991
26487
|
}
|
|
25992
26488
|
let raw;
|
|
25993
26489
|
try {
|
|
25994
|
-
raw = parseYaml(
|
|
26490
|
+
raw = parseYaml(fs56.readFileSync(p, "utf-8"));
|
|
25995
26491
|
} catch (err2) {
|
|
25996
26492
|
throw new Error(
|
|
25997
26493
|
`sandbox: ${SANDBOX_CONFIG_FILE} is not valid YAML \u2014 ${err2.message}`
|
|
@@ -26050,13 +26546,13 @@ init_templates();
|
|
|
26050
26546
|
|
|
26051
26547
|
// src/sandbox/runtime.ts
|
|
26052
26548
|
init_templates();
|
|
26053
|
-
import
|
|
26054
|
-
import
|
|
26055
|
-
import
|
|
26549
|
+
import fs57 from "fs";
|
|
26550
|
+
import os48 from "os";
|
|
26551
|
+
import path54 from "path";
|
|
26056
26552
|
import crypto8 from "crypto";
|
|
26057
26553
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
26058
26554
|
function sandboxDataDir(cwd = process.cwd()) {
|
|
26059
|
-
return
|
|
26555
|
+
return path54.join(cwd, ".node9", "sandbox", "data");
|
|
26060
26556
|
}
|
|
26061
26557
|
function detectEngine(engine) {
|
|
26062
26558
|
const r = spawnSync5(engine, ["--version"], { encoding: "utf-8" });
|
|
@@ -26067,7 +26563,7 @@ function detectEngine(engine) {
|
|
|
26067
26563
|
}
|
|
26068
26564
|
function agentCredentialsMount(agent) {
|
|
26069
26565
|
const rel = agent === "codex" ? ".codex/auth.json" : ".claude/.credentials.json";
|
|
26070
|
-
return { hostPath:
|
|
26566
|
+
return { hostPath: path54.join(os48.homedir(), rel), target: `/home/${RUN_AS_USER}/${rel}` };
|
|
26071
26567
|
}
|
|
26072
26568
|
function buildRunArgs(opts) {
|
|
26073
26569
|
const { config, workspaceHostPath, dataHostPath, allowlistHostPath, agentArgs } = opts;
|
|
@@ -26077,7 +26573,7 @@ function buildRunArgs(opts) {
|
|
|
26077
26573
|
args.push("-v", `${allowlistHostPath}:${ALLOWED_DOMAINS_PATH}:ro`);
|
|
26078
26574
|
if (config.node9.mountAgentCredentials) {
|
|
26079
26575
|
const creds = agentCredentialsMount(config.agent);
|
|
26080
|
-
if (
|
|
26576
|
+
if (fs57.existsSync(creds.hostPath)) {
|
|
26081
26577
|
args.push("-v", `${creds.hostPath}:${creds.target}`);
|
|
26082
26578
|
}
|
|
26083
26579
|
}
|
|
@@ -26095,30 +26591,30 @@ function imageContentHash(dockerfile, entrypoint) {
|
|
|
26095
26591
|
return crypto8.createHash("sha256").update(dockerfile).update("\0").update(entrypoint).digest("hex").slice(0, 16);
|
|
26096
26592
|
}
|
|
26097
26593
|
function sandboxBuildDir(cwd = process.cwd()) {
|
|
26098
|
-
return
|
|
26594
|
+
return path54.join(cwd, ".node9", "sandbox", "build");
|
|
26099
26595
|
}
|
|
26100
26596
|
function writeBuildContext(cwd, dockerfile, entrypoint) {
|
|
26101
26597
|
const dir = sandboxBuildDir(cwd);
|
|
26102
|
-
|
|
26103
|
-
|
|
26104
|
-
|
|
26598
|
+
fs57.mkdirSync(dir, { recursive: true });
|
|
26599
|
+
fs57.writeFileSync(path54.join(dir, "Dockerfile"), dockerfile);
|
|
26600
|
+
fs57.writeFileSync(path54.join(dir, "entrypoint.sh"), entrypoint);
|
|
26105
26601
|
return dir;
|
|
26106
26602
|
}
|
|
26107
26603
|
function writeAllowlist(cwd, hosts) {
|
|
26108
|
-
const dir =
|
|
26109
|
-
|
|
26110
|
-
const p =
|
|
26111
|
-
|
|
26604
|
+
const dir = path54.join(cwd, ".node9", "sandbox");
|
|
26605
|
+
fs57.mkdirSync(dir, { recursive: true });
|
|
26606
|
+
const p = path54.join(dir, "allowed-domains.txt");
|
|
26607
|
+
fs57.writeFileSync(p, hosts.join("\n") + "\n");
|
|
26112
26608
|
return p;
|
|
26113
26609
|
}
|
|
26114
26610
|
function resolveHomePath(p) {
|
|
26115
|
-
return p.startsWith("~") ?
|
|
26611
|
+
return p.startsWith("~") ? path54.join(os48.homedir(), p.slice(1)) : path54.resolve(p);
|
|
26116
26612
|
}
|
|
26117
26613
|
|
|
26118
26614
|
// src/cli/commands/sandbox.ts
|
|
26119
26615
|
function seedDataDirConfig(dataDir, sandbox) {
|
|
26120
|
-
|
|
26121
|
-
const
|
|
26616
|
+
fs58.mkdirSync(dataDir, { recursive: true });
|
|
26617
|
+
const configPath = path55.join(dataDir, "config.json");
|
|
26122
26618
|
const seed = {
|
|
26123
26619
|
settings: {
|
|
26124
26620
|
approvers: {
|
|
@@ -26129,7 +26625,7 @@ function seedDataDirConfig(dataDir, sandbox) {
|
|
|
26129
26625
|
}
|
|
26130
26626
|
}
|
|
26131
26627
|
};
|
|
26132
|
-
|
|
26628
|
+
fs58.writeFileSync(configPath, JSON.stringify(seed, null, 2), { mode: 384 });
|
|
26133
26629
|
}
|
|
26134
26630
|
function registerSandboxCommand(program2, version2) {
|
|
26135
26631
|
const node9Version2 = pinnedNode9Version(version2);
|
|
@@ -26137,18 +26633,18 @@ function registerSandboxCommand(program2, version2) {
|
|
|
26137
26633
|
cmd.command("new").description(`Scaffold ${SANDBOX_CONFIG_FILE} in this project`).option("--agent <agent>", "claude (default) or codex", "claude").action((opts) => {
|
|
26138
26634
|
const agent = opts.agent === "codex" ? "codex" : "claude";
|
|
26139
26635
|
const p = sandboxConfigPath();
|
|
26140
|
-
if (
|
|
26636
|
+
if (fs58.existsSync(p)) {
|
|
26141
26637
|
console.log(
|
|
26142
|
-
|
|
26638
|
+
chalk28.yellow(` ${SANDBOX_CONFIG_FILE} already exists \u2014 leaving it untouched.`)
|
|
26143
26639
|
);
|
|
26144
26640
|
return;
|
|
26145
26641
|
}
|
|
26146
|
-
|
|
26642
|
+
fs58.writeFileSync(p, scaffoldSandboxYaml(agent));
|
|
26147
26643
|
console.log(
|
|
26148
|
-
|
|
26644
|
+
chalk28.green(` \u2713 wrote ${SANDBOX_CONFIG_FILE}`) + chalk28.dim(` (agent: ${agent})`)
|
|
26149
26645
|
);
|
|
26150
26646
|
console.log(
|
|
26151
|
-
|
|
26647
|
+
chalk28.dim(" Edit it (mounts / allow / expose), then: ") + chalk28.cyan("node9 sandbox run")
|
|
26152
26648
|
);
|
|
26153
26649
|
});
|
|
26154
26650
|
cmd.command("run [agent]").description("Build (if needed) + run the agent jailed. Extra args after -- go to the agent.").allowUnknownOption(true).allowExcessArguments(true).action((agentArg, _opts, command) => {
|
|
@@ -26158,7 +26654,7 @@ function registerSandboxCommand(program2, version2) {
|
|
|
26158
26654
|
const engine = detectEngine(sandbox.runtime.engine);
|
|
26159
26655
|
if (!engine.available) {
|
|
26160
26656
|
console.error(
|
|
26161
|
-
|
|
26657
|
+
chalk28.red(` ${sandbox.runtime.engine} not found.`) + chalk28.dim(` Install it first \u2014 node9 sandbox needs a container runtime.`)
|
|
26162
26658
|
);
|
|
26163
26659
|
process.exit(1);
|
|
26164
26660
|
}
|
|
@@ -26171,11 +26667,11 @@ function registerSandboxCommand(program2, version2) {
|
|
|
26171
26667
|
});
|
|
26172
26668
|
if (compiled.rejected.length) {
|
|
26173
26669
|
console.log(
|
|
26174
|
-
|
|
26670
|
+
chalk28.yellow(` \u26A0 ignoring invalid allow hosts: ${compiled.rejected.join(", ")}`)
|
|
26175
26671
|
);
|
|
26176
26672
|
}
|
|
26177
26673
|
if (compiled.denied.length) {
|
|
26178
|
-
console.log(
|
|
26674
|
+
console.log(chalk28.dim(` (denied: ${compiled.denied.join(", ")})`));
|
|
26179
26675
|
}
|
|
26180
26676
|
const allowlistPath = writeAllowlist(cwd, compiled.allow);
|
|
26181
26677
|
const dockerfile = renderDockerfile(sandbox, node9Version2);
|
|
@@ -26183,20 +26679,20 @@ function registerSandboxCommand(program2, version2) {
|
|
|
26183
26679
|
const buildDir = writeBuildContext(cwd, dockerfile, entrypoint);
|
|
26184
26680
|
const hash = imageContentHash(dockerfile, entrypoint);
|
|
26185
26681
|
const image = sandbox.runtime.image;
|
|
26186
|
-
const hashFile =
|
|
26187
|
-
const lastHash =
|
|
26682
|
+
const hashFile = path55.join(sandboxBuildDir(cwd), ".image-hash");
|
|
26683
|
+
const lastHash = fs58.existsSync(hashFile) ? fs58.readFileSync(hashFile, "utf-8").trim() : "";
|
|
26188
26684
|
const imageExists = spawnSync6(sandbox.runtime.engine, ["image", "inspect", image], { stdio: "ignore" }).status === 0;
|
|
26189
26685
|
const needBuild = sandbox.runtime.rebuild === "always" || !imageExists || sandbox.runtime.rebuild !== "never" && lastHash !== hash;
|
|
26190
26686
|
if (needBuild) {
|
|
26191
|
-
console.log(
|
|
26687
|
+
console.log(chalk28.dim(` building ${image} \u2026`));
|
|
26192
26688
|
const b = spawnSync6(sandbox.runtime.engine, ["build", "-t", image, buildDir], {
|
|
26193
26689
|
stdio: "inherit"
|
|
26194
26690
|
});
|
|
26195
26691
|
if (b.status !== 0) {
|
|
26196
|
-
console.error(
|
|
26692
|
+
console.error(chalk28.red(" build failed."));
|
|
26197
26693
|
process.exit(b.status ?? 1);
|
|
26198
26694
|
}
|
|
26199
|
-
|
|
26695
|
+
fs58.writeFileSync(hashFile, hash);
|
|
26200
26696
|
}
|
|
26201
26697
|
const dataDir = sandboxDataDir(cwd);
|
|
26202
26698
|
seedDataDirConfig(dataDir, sandbox);
|
|
@@ -26210,36 +26706,36 @@ function registerSandboxCommand(program2, version2) {
|
|
|
26210
26706
|
});
|
|
26211
26707
|
if (sandbox.node9.mountAgentCredentials) {
|
|
26212
26708
|
const creds = agentCredentialsMount(sandbox.agent);
|
|
26213
|
-
if (
|
|
26214
|
-
console.log(
|
|
26709
|
+
if (fs58.existsSync(creds.hostPath)) {
|
|
26710
|
+
console.log(chalk28.dim(` mounting ${creds.hostPath} (agent credentials, rw)`));
|
|
26215
26711
|
} else {
|
|
26216
26712
|
console.log(
|
|
26217
|
-
|
|
26713
|
+
chalk28.yellow(` \u26A0 ${creds.hostPath} not found \u2014 `) + chalk28.dim(`the agent must auth via an env key in env.pass.`)
|
|
26218
26714
|
);
|
|
26219
26715
|
}
|
|
26220
26716
|
}
|
|
26221
26717
|
console.log(
|
|
26222
|
-
|
|
26718
|
+
chalk28.green(` \u{1F6E1}\uFE0F ${sandbox.agent} jailed \u2014 ${compiled.allow.length} hosts allowed
|
|
26223
26719
|
`)
|
|
26224
26720
|
);
|
|
26225
26721
|
const r = spawnSync6(sandbox.runtime.engine, runArgs, { stdio: "inherit" });
|
|
26226
26722
|
process.exit(r.status ?? 0);
|
|
26227
26723
|
});
|
|
26228
26724
|
cmd.command("tail").description("Stream the sandbox's audit log (host-side)").action(() => {
|
|
26229
|
-
const auditPath =
|
|
26230
|
-
if (!
|
|
26231
|
-
console.log(
|
|
26725
|
+
const auditPath = path55.join(sandboxDataDir(), "audit.log");
|
|
26726
|
+
if (!fs58.existsSync(auditPath)) {
|
|
26727
|
+
console.log(chalk28.dim(" no sandbox audit yet."));
|
|
26232
26728
|
return;
|
|
26233
26729
|
}
|
|
26234
26730
|
spawnSync6("tail", ["-f", auditPath], { stdio: "inherit" });
|
|
26235
26731
|
});
|
|
26236
26732
|
cmd.command("logs").description("Dump the sandbox's audit log").action(() => {
|
|
26237
|
-
const auditPath =
|
|
26238
|
-
if (!
|
|
26239
|
-
console.log(
|
|
26733
|
+
const auditPath = path55.join(sandboxDataDir(), "audit.log");
|
|
26734
|
+
if (!fs58.existsSync(auditPath)) {
|
|
26735
|
+
console.log(chalk28.dim(" no sandbox audit yet."));
|
|
26240
26736
|
return;
|
|
26241
26737
|
}
|
|
26242
|
-
process.stdout.write(
|
|
26738
|
+
process.stdout.write(fs58.readFileSync(auditPath, "utf-8"));
|
|
26243
26739
|
});
|
|
26244
26740
|
cmd.command("clean").description("Remove the sandbox image, build context, and data").action(() => {
|
|
26245
26741
|
const cwd = process.cwd();
|
|
@@ -26253,8 +26749,8 @@ function registerSandboxCommand(program2, version2) {
|
|
|
26253
26749
|
stdio: "ignore"
|
|
26254
26750
|
});
|
|
26255
26751
|
}
|
|
26256
|
-
|
|
26257
|
-
console.log(
|
|
26752
|
+
fs58.rmSync(path55.join(cwd, ".node9", "sandbox"), { recursive: true, force: true });
|
|
26753
|
+
console.log(chalk28.green(" \u2713 sandbox image + build + data removed."));
|
|
26258
26754
|
});
|
|
26259
26755
|
}
|
|
26260
26756
|
|
|
@@ -26263,10 +26759,10 @@ init_scan_summary();
|
|
|
26263
26759
|
init_litellm();
|
|
26264
26760
|
init_cost_gemini();
|
|
26265
26761
|
init_cost_codex();
|
|
26266
|
-
import
|
|
26267
|
-
import
|
|
26268
|
-
import
|
|
26269
|
-
import
|
|
26762
|
+
import chalk29 from "chalk";
|
|
26763
|
+
import fs59 from "fs";
|
|
26764
|
+
import path56 from "path";
|
|
26765
|
+
import os49 from "os";
|
|
26270
26766
|
function modelPrice(model) {
|
|
26271
26767
|
const t = pricingFor(model);
|
|
26272
26768
|
if (!t) return null;
|
|
@@ -26283,10 +26779,10 @@ function encodeProjectPath(projectPath) {
|
|
|
26283
26779
|
}
|
|
26284
26780
|
function sessionJsonlPath(projectPath, sessionId) {
|
|
26285
26781
|
const encoded = encodeProjectPath(projectPath);
|
|
26286
|
-
return
|
|
26782
|
+
return path56.join(os49.homedir(), ".claude", "projects", encoded, `${sessionId}.jsonl`);
|
|
26287
26783
|
}
|
|
26288
26784
|
function projectLabel(projectPath) {
|
|
26289
|
-
return projectPath.replace(
|
|
26785
|
+
return projectPath.replace(os49.homedir(), "~");
|
|
26290
26786
|
}
|
|
26291
26787
|
function parseHistoryLines(lines) {
|
|
26292
26788
|
const entries = [];
|
|
@@ -26355,10 +26851,10 @@ function parseSessionLines(lines) {
|
|
|
26355
26851
|
return { toolCalls, costUSD, hasSnapshot, modifiedFiles };
|
|
26356
26852
|
}
|
|
26357
26853
|
function loadAuditEntries(auditPath) {
|
|
26358
|
-
const aPath = auditPath ??
|
|
26854
|
+
const aPath = auditPath ?? path56.join(os49.homedir(), ".node9", "audit.log");
|
|
26359
26855
|
let raw;
|
|
26360
26856
|
try {
|
|
26361
|
-
raw =
|
|
26857
|
+
raw = fs59.readFileSync(aPath, "utf-8");
|
|
26362
26858
|
} catch {
|
|
26363
26859
|
return [];
|
|
26364
26860
|
}
|
|
@@ -26394,8 +26890,8 @@ function auditEntriesInWindow(entries, windowStart, windowEnd) {
|
|
|
26394
26890
|
return result;
|
|
26395
26891
|
}
|
|
26396
26892
|
function buildGeminiSessions(days, allAuditEntries) {
|
|
26397
|
-
const tmpDir =
|
|
26398
|
-
if (!
|
|
26893
|
+
const tmpDir = path56.join(os49.homedir(), ".gemini", "tmp");
|
|
26894
|
+
if (!fs59.existsSync(tmpDir)) return [];
|
|
26399
26895
|
const cutoff = days !== null ? (() => {
|
|
26400
26896
|
const d = /* @__PURE__ */ new Date();
|
|
26401
26897
|
d.setDate(d.getDate() - days);
|
|
@@ -26404,35 +26900,35 @@ function buildGeminiSessions(days, allAuditEntries) {
|
|
|
26404
26900
|
})() : null;
|
|
26405
26901
|
let slugDirs;
|
|
26406
26902
|
try {
|
|
26407
|
-
slugDirs =
|
|
26903
|
+
slugDirs = fs59.readdirSync(tmpDir);
|
|
26408
26904
|
} catch {
|
|
26409
26905
|
return [];
|
|
26410
26906
|
}
|
|
26411
26907
|
const summaries = [];
|
|
26412
|
-
for (const
|
|
26413
|
-
const slugPath =
|
|
26908
|
+
for (const slug2 of slugDirs) {
|
|
26909
|
+
const slugPath = path56.join(tmpDir, slug2);
|
|
26414
26910
|
try {
|
|
26415
|
-
if (!
|
|
26911
|
+
if (!fs59.statSync(slugPath).isDirectory()) continue;
|
|
26416
26912
|
} catch {
|
|
26417
26913
|
continue;
|
|
26418
26914
|
}
|
|
26419
|
-
let projectRoot =
|
|
26915
|
+
let projectRoot = path56.join(os49.homedir(), slug2);
|
|
26420
26916
|
try {
|
|
26421
|
-
projectRoot =
|
|
26917
|
+
projectRoot = fs59.readFileSync(path56.join(slugPath, ".project_root"), "utf-8").trim();
|
|
26422
26918
|
} catch {
|
|
26423
26919
|
}
|
|
26424
|
-
const chatsDir =
|
|
26425
|
-
if (!
|
|
26920
|
+
const chatsDir = path56.join(slugPath, "chats");
|
|
26921
|
+
if (!fs59.existsSync(chatsDir)) continue;
|
|
26426
26922
|
let chatFiles;
|
|
26427
26923
|
try {
|
|
26428
|
-
chatFiles =
|
|
26924
|
+
chatFiles = fs59.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
|
|
26429
26925
|
} catch {
|
|
26430
26926
|
continue;
|
|
26431
26927
|
}
|
|
26432
26928
|
for (const chatFile of chatFiles) {
|
|
26433
26929
|
let raw;
|
|
26434
26930
|
try {
|
|
26435
|
-
raw =
|
|
26931
|
+
raw = fs59.readFileSync(path56.join(chatsDir, chatFile), "utf-8");
|
|
26436
26932
|
} catch {
|
|
26437
26933
|
continue;
|
|
26438
26934
|
}
|
|
@@ -26512,8 +27008,8 @@ function buildGeminiSessions(days, allAuditEntries) {
|
|
|
26512
27008
|
return summaries;
|
|
26513
27009
|
}
|
|
26514
27010
|
function buildCodexSessions(days, allAuditEntries) {
|
|
26515
|
-
const sessionsBase =
|
|
26516
|
-
if (!
|
|
27011
|
+
const sessionsBase = path56.join(os49.homedir(), ".codex", "sessions");
|
|
27012
|
+
if (!fs59.existsSync(sessionsBase)) return [];
|
|
26517
27013
|
const cutoff = days !== null ? (() => {
|
|
26518
27014
|
const d = /* @__PURE__ */ new Date();
|
|
26519
27015
|
d.setDate(d.getDate() - days);
|
|
@@ -26522,29 +27018,29 @@ function buildCodexSessions(days, allAuditEntries) {
|
|
|
26522
27018
|
})() : null;
|
|
26523
27019
|
const jsonlFiles = [];
|
|
26524
27020
|
try {
|
|
26525
|
-
for (const year of
|
|
26526
|
-
const yearPath =
|
|
27021
|
+
for (const year of fs59.readdirSync(sessionsBase)) {
|
|
27022
|
+
const yearPath = path56.join(sessionsBase, year);
|
|
26527
27023
|
try {
|
|
26528
|
-
if (!
|
|
27024
|
+
if (!fs59.statSync(yearPath).isDirectory()) continue;
|
|
26529
27025
|
} catch {
|
|
26530
27026
|
continue;
|
|
26531
27027
|
}
|
|
26532
|
-
for (const month of
|
|
26533
|
-
const monthPath =
|
|
27028
|
+
for (const month of fs59.readdirSync(yearPath)) {
|
|
27029
|
+
const monthPath = path56.join(yearPath, month);
|
|
26534
27030
|
try {
|
|
26535
|
-
if (!
|
|
27031
|
+
if (!fs59.statSync(monthPath).isDirectory()) continue;
|
|
26536
27032
|
} catch {
|
|
26537
27033
|
continue;
|
|
26538
27034
|
}
|
|
26539
|
-
for (const day of
|
|
26540
|
-
const dayPath =
|
|
27035
|
+
for (const day of fs59.readdirSync(monthPath)) {
|
|
27036
|
+
const dayPath = path56.join(monthPath, day);
|
|
26541
27037
|
try {
|
|
26542
|
-
if (!
|
|
27038
|
+
if (!fs59.statSync(dayPath).isDirectory()) continue;
|
|
26543
27039
|
} catch {
|
|
26544
27040
|
continue;
|
|
26545
27041
|
}
|
|
26546
|
-
for (const file of
|
|
26547
|
-
if (file.endsWith(".jsonl")) jsonlFiles.push(
|
|
27042
|
+
for (const file of fs59.readdirSync(dayPath)) {
|
|
27043
|
+
if (file.endsWith(".jsonl")) jsonlFiles.push(path56.join(dayPath, file));
|
|
26548
27044
|
}
|
|
26549
27045
|
}
|
|
26550
27046
|
}
|
|
@@ -26556,7 +27052,7 @@ function buildCodexSessions(days, allAuditEntries) {
|
|
|
26556
27052
|
for (const filePath of jsonlFiles) {
|
|
26557
27053
|
let lines;
|
|
26558
27054
|
try {
|
|
26559
|
-
lines =
|
|
27055
|
+
lines = fs59.readFileSync(filePath, "utf-8").split("\n");
|
|
26560
27056
|
} catch {
|
|
26561
27057
|
continue;
|
|
26562
27058
|
}
|
|
@@ -26642,10 +27138,10 @@ function buildCodexSessions(days, allAuditEntries) {
|
|
|
26642
27138
|
return summaries;
|
|
26643
27139
|
}
|
|
26644
27140
|
function buildSessions(days, historyPath) {
|
|
26645
|
-
const hPath = historyPath ??
|
|
27141
|
+
const hPath = historyPath ?? path56.join(os49.homedir(), ".claude", "history.jsonl");
|
|
26646
27142
|
let historyRaw = "";
|
|
26647
27143
|
try {
|
|
26648
|
-
historyRaw =
|
|
27144
|
+
historyRaw = fs59.readFileSync(hPath, "utf-8");
|
|
26649
27145
|
} catch {
|
|
26650
27146
|
}
|
|
26651
27147
|
const cutoff = days !== null ? (() => {
|
|
@@ -26669,7 +27165,7 @@ function buildSessions(days, historyPath) {
|
|
|
26669
27165
|
const jsonlFile = sessionJsonlPath(entry.project, entry.sessionId);
|
|
26670
27166
|
let sessionLines = [];
|
|
26671
27167
|
try {
|
|
26672
|
-
sessionLines =
|
|
27168
|
+
sessionLines = fs59.readFileSync(jsonlFile, "utf-8").split("\n");
|
|
26673
27169
|
} catch {
|
|
26674
27170
|
}
|
|
26675
27171
|
const { toolCalls, costUSD, hasSnapshot, modifiedFiles } = parseSessionLines(sessionLines);
|
|
@@ -26755,11 +27251,11 @@ function toolInputSummary(tool, input) {
|
|
|
26755
27251
|
}
|
|
26756
27252
|
function toolColor(tool) {
|
|
26757
27253
|
const t = tool.toLowerCase();
|
|
26758
|
-
if (t === "bash" || t === "execute_bash") return
|
|
26759
|
-
if (t === "write") return
|
|
26760
|
-
if (t === "edit" || t === "notebookedit") return
|
|
26761
|
-
if (t === "read") return
|
|
26762
|
-
return
|
|
27254
|
+
if (t === "bash" || t === "execute_bash") return chalk29.red;
|
|
27255
|
+
if (t === "write") return chalk29.green;
|
|
27256
|
+
if (t === "edit" || t === "notebookedit") return chalk29.yellow;
|
|
27257
|
+
if (t === "read") return chalk29.cyan;
|
|
27258
|
+
return chalk29.gray;
|
|
26763
27259
|
}
|
|
26764
27260
|
function barStr2(value, max, width) {
|
|
26765
27261
|
if (max === 0 || width <= 0) return "\u2591".repeat(width);
|
|
@@ -26769,7 +27265,7 @@ function barStr2(value, max, width) {
|
|
|
26769
27265
|
function colorBar2(value, max, width) {
|
|
26770
27266
|
const s = barStr2(value, max, width);
|
|
26771
27267
|
const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
|
|
26772
|
-
return
|
|
27268
|
+
return chalk29.cyan(s.slice(0, filled)) + chalk29.dim(s.slice(filled));
|
|
26773
27269
|
}
|
|
26774
27270
|
function renderSummary(summaries) {
|
|
26775
27271
|
const totalTools = summaries.reduce((n, s) => n + s.toolCalls.length, 0);
|
|
@@ -26799,45 +27295,45 @@ function renderSummary(summaries) {
|
|
|
26799
27295
|
}
|
|
26800
27296
|
const topProjects = [...projCosts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
26801
27297
|
const W = 20;
|
|
26802
|
-
console.log(
|
|
27298
|
+
console.log(chalk29.dim(" " + "\u2500".repeat(70)));
|
|
26803
27299
|
console.log(
|
|
26804
|
-
" " +
|
|
27300
|
+
" " + chalk29.bold.white(String(summaries.length).padEnd(4)) + chalk29.dim("sessions ") + chalk29.bold.yellow(fmtCost3(totalCost).padEnd(10)) + chalk29.dim("total ") + chalk29.bold.white(String(totalTools).padEnd(6)) + chalk29.dim("tool calls ") + chalk29.bold.white(String(totalFiles)) + chalk29.dim(" files modified") + (totalBlocked > 0 ? chalk29.dim(" ") + chalk29.red.bold(String(totalBlocked)) + chalk29.dim(" blocked by node9") : "")
|
|
26805
27301
|
);
|
|
26806
27302
|
console.log(
|
|
26807
|
-
" " +
|
|
27303
|
+
" " + chalk29.dim("avg ") + chalk29.white(fmtCost3(avgCost).padEnd(10)) + chalk29.dim("/session ") + chalk29.green(String(snapshots)) + chalk29.dim(` of ${summaries.length} sessions had snapshots`)
|
|
26808
27304
|
);
|
|
26809
27305
|
console.log("");
|
|
26810
|
-
console.log(" " +
|
|
27306
|
+
console.log(" " + chalk29.dim("Tool breakdown:"));
|
|
26811
27307
|
const maxGroup = Math.max(...Object.values(groups));
|
|
26812
27308
|
for (const [label2, count] of Object.entries(groups)) {
|
|
26813
27309
|
if (count === 0) continue;
|
|
26814
27310
|
const pct = totalTools > 0 ? Math.round(count / totalTools * 100) : 0;
|
|
26815
27311
|
console.log(
|
|
26816
|
-
" " + label2.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " +
|
|
27312
|
+
" " + label2.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " + chalk29.white(String(count).padStart(4)) + chalk29.dim(` (${String(pct)}%)`)
|
|
26817
27313
|
);
|
|
26818
27314
|
}
|
|
26819
27315
|
console.log("");
|
|
26820
27316
|
if (topProjects.length > 1) {
|
|
26821
|
-
console.log(" " +
|
|
27317
|
+
console.log(" " + chalk29.dim("Cost by project:"));
|
|
26822
27318
|
const maxProjCost = topProjects[0][1];
|
|
26823
27319
|
for (const [proj, cost] of topProjects) {
|
|
26824
27320
|
console.log(
|
|
26825
|
-
" " + proj.slice(0, 28).padEnd(28) + " " + colorBar2(cost, maxProjCost, W) + " " +
|
|
27321
|
+
" " + proj.slice(0, 28).padEnd(28) + " " + colorBar2(cost, maxProjCost, W) + " " + chalk29.yellow(fmtCost3(cost))
|
|
26826
27322
|
);
|
|
26827
27323
|
}
|
|
26828
27324
|
console.log("");
|
|
26829
27325
|
}
|
|
26830
|
-
console.log(
|
|
27326
|
+
console.log(chalk29.dim(" " + "\u2500".repeat(70)));
|
|
26831
27327
|
console.log("");
|
|
26832
27328
|
}
|
|
26833
27329
|
function renderList(summaries, totalCost) {
|
|
26834
27330
|
if (summaries.length === 0) {
|
|
26835
|
-
console.log(
|
|
27331
|
+
console.log(chalk29.yellow(" No sessions found in the requested range.\n"));
|
|
26836
27332
|
return;
|
|
26837
27333
|
}
|
|
26838
|
-
const totalLabel = totalCost > 0 ?
|
|
27334
|
+
const totalLabel = totalCost > 0 ? chalk29.dim(" ~" + fmtCost3(totalCost) + " total") : "";
|
|
26839
27335
|
console.log(
|
|
26840
|
-
" " +
|
|
27336
|
+
" " + chalk29.white(String(summaries.length)) + chalk29.dim(` session${summaries.length !== 1 ? "s" : ""}`) + totalLabel
|
|
26841
27337
|
);
|
|
26842
27338
|
console.log("");
|
|
26843
27339
|
let lastGroup = "";
|
|
@@ -26845,51 +27341,51 @@ function renderList(summaries, totalCost) {
|
|
|
26845
27341
|
const activeDate = fmtDate2(s.lastActiveTime);
|
|
26846
27342
|
const group = activeDate + " " + s.projectLabel;
|
|
26847
27343
|
if (group !== lastGroup) {
|
|
26848
|
-
console.log(
|
|
27344
|
+
console.log(chalk29.dim(" \u2500\u2500\u2500 ") + chalk29.bold(activeDate) + chalk29.dim(" " + s.projectLabel));
|
|
26849
27345
|
lastGroup = group;
|
|
26850
27346
|
}
|
|
26851
27347
|
const startDate = fmtDate2(s.startTime);
|
|
26852
|
-
const dateRange = startDate !== activeDate ?
|
|
26853
|
-
const timeStr =
|
|
26854
|
-
const prompt =
|
|
26855
|
-
const tools = s.toolCalls.length > 0 ?
|
|
26856
|
-
const cost = s.costUSD > 0 ?
|
|
26857
|
-
const blocked = s.blockedCalls.length > 0 ?
|
|
26858
|
-
const snap = s.hasSnapshot ?
|
|
26859
|
-
const agentBadge =
|
|
27348
|
+
const dateRange = startDate !== activeDate ? chalk29.dim(" (" + startDate + " \u2192 " + activeDate + ")") : "";
|
|
27349
|
+
const timeStr = chalk29.dim(fmtTime(s.startTime));
|
|
27350
|
+
const prompt = chalk29.white(truncate(s.firstPrompt.replace(/\n/g, " "), 50).padEnd(50));
|
|
27351
|
+
const tools = s.toolCalls.length > 0 ? chalk29.dim(String(s.toolCalls.length).padStart(3) + " tools") : chalk29.dim(" 0 tools");
|
|
27352
|
+
const cost = s.costUSD > 0 ? chalk29.dim(" " + fmtCost3(s.costUSD).padEnd(8)) : " ";
|
|
27353
|
+
const blocked = s.blockedCalls.length > 0 ? chalk29.red(" \u{1F6D1} " + String(s.blockedCalls.length)) : "";
|
|
27354
|
+
const snap = s.hasSnapshot ? chalk29.green(" \u{1F4F8}") : "";
|
|
27355
|
+
const agentBadge = chalk29[agentColorName(s.agent ?? "claude")](
|
|
26860
27356
|
" " + agentBadgeText(s.agent ?? "claude", 0)
|
|
26861
27357
|
);
|
|
26862
|
-
const sid =
|
|
27358
|
+
const sid = chalk29.dim(" " + s.sessionId.slice(0, 8));
|
|
26863
27359
|
console.log(
|
|
26864
27360
|
` ${timeStr} ${prompt} ${tools}${cost}${blocked}${snap}${agentBadge}${sid}${dateRange}`
|
|
26865
27361
|
);
|
|
26866
27362
|
}
|
|
26867
27363
|
console.log("");
|
|
26868
27364
|
console.log(
|
|
26869
|
-
|
|
27365
|
+
chalk29.dim(" Run") + " " + chalk29.cyan("node9 sessions --detail <session-id>") + chalk29.dim(" for full tool trace.")
|
|
26870
27366
|
);
|
|
26871
27367
|
console.log("");
|
|
26872
27368
|
}
|
|
26873
27369
|
function renderDetail(s) {
|
|
26874
27370
|
console.log("");
|
|
26875
|
-
console.log(
|
|
27371
|
+
console.log(chalk29.bold(" Session ") + chalk29.dim(s.sessionId));
|
|
26876
27372
|
console.log(
|
|
26877
|
-
|
|
27373
|
+
chalk29.bold(" Prompt ") + chalk29.white(s.firstPrompt.replace(/\n/g, " ").slice(0, 120))
|
|
26878
27374
|
);
|
|
26879
|
-
console.log(
|
|
27375
|
+
console.log(chalk29.bold(" Project ") + chalk29.white(s.projectLabel));
|
|
26880
27376
|
if (s.agent) {
|
|
26881
|
-
const agentLabel2 =
|
|
26882
|
-
console.log(
|
|
27377
|
+
const agentLabel2 = chalk29[agentColorName(s.agent)](agentDisplayName(s.agent));
|
|
27378
|
+
console.log(chalk29.bold(" Agent ") + agentLabel2);
|
|
26883
27379
|
}
|
|
26884
|
-
console.log(
|
|
27380
|
+
console.log(chalk29.bold(" When ") + chalk29.white(fmtDateTime(s.startTime)));
|
|
26885
27381
|
if (s.costUSD > 0)
|
|
26886
|
-
console.log(
|
|
27382
|
+
console.log(chalk29.bold(" Cost ") + chalk29.yellow("~" + fmtCost3(s.costUSD)));
|
|
26887
27383
|
console.log(
|
|
26888
|
-
|
|
27384
|
+
chalk29.bold(" Snapshot ") + (s.hasSnapshot ? chalk29.green("\u2713 taken") : chalk29.dim("none"))
|
|
26889
27385
|
);
|
|
26890
27386
|
console.log("");
|
|
26891
27387
|
if (s.toolCalls.length === 0 && s.blockedCalls.length === 0) {
|
|
26892
|
-
console.log(
|
|
27388
|
+
console.log(chalk29.dim(" No tool calls recorded.\n"));
|
|
26893
27389
|
return;
|
|
26894
27390
|
}
|
|
26895
27391
|
const timeline = [
|
|
@@ -26902,32 +27398,32 @@ function renderDetail(s) {
|
|
|
26902
27398
|
});
|
|
26903
27399
|
const headerParts = [`Tool calls (${s.toolCalls.length})`];
|
|
26904
27400
|
if (s.blockedCalls.length > 0)
|
|
26905
|
-
headerParts.push(
|
|
26906
|
-
console.log(
|
|
27401
|
+
headerParts.push(chalk29.red(`${s.blockedCalls.length} blocked by node9`));
|
|
27402
|
+
console.log(chalk29.bold(" " + headerParts.join(" \xB7 ")));
|
|
26907
27403
|
console.log("");
|
|
26908
27404
|
for (const entry of timeline) {
|
|
26909
27405
|
if (entry.kind === "tool") {
|
|
26910
27406
|
const tc = entry.tc;
|
|
26911
27407
|
const colorFn = toolColor(tc.tool);
|
|
26912
27408
|
const toolPad = colorFn(tc.tool.padEnd(16));
|
|
26913
|
-
const detail =
|
|
26914
|
-
const ts = tc.timestamp ?
|
|
27409
|
+
const detail = chalk29.gray(truncate(toolInputSummary(tc.tool, tc.input), 70));
|
|
27410
|
+
const ts = tc.timestamp ? chalk29.dim(fmtTime(tc.timestamp) + " ") : " ";
|
|
26915
27411
|
console.log(` ${ts}${toolPad} ${detail}`);
|
|
26916
27412
|
} else {
|
|
26917
27413
|
const bc = entry.bc;
|
|
26918
|
-
const ts = bc.timestamp ?
|
|
26919
|
-
const label2 =
|
|
26920
|
-
const toolName =
|
|
26921
|
-
const argsSummary = bc.args ?
|
|
26922
|
-
const reason = bc.checkedBy ?
|
|
27414
|
+
const ts = bc.timestamp ? chalk29.dim(fmtTime(bc.timestamp) + " ") : " ";
|
|
27415
|
+
const label2 = chalk29.red("\u{1F6D1} BLOCKED".padEnd(16));
|
|
27416
|
+
const toolName = chalk29.red(bc.tool.padEnd(10));
|
|
27417
|
+
const argsSummary = bc.args ? chalk29.gray(truncate(toolInputSummary(bc.tool, bc.args), 40)) : chalk29.dim("[args not logged]");
|
|
27418
|
+
const reason = bc.checkedBy ? chalk29.dim(" \u2190 " + bc.checkedBy) : "";
|
|
26923
27419
|
console.log(` ${ts}${label2} ${toolName} ${argsSummary}${reason}`);
|
|
26924
27420
|
}
|
|
26925
27421
|
}
|
|
26926
27422
|
console.log("");
|
|
26927
27423
|
if (s.modifiedFiles.length > 0) {
|
|
26928
|
-
console.log(
|
|
27424
|
+
console.log(chalk29.bold(` Files modified (${s.modifiedFiles.length}):`));
|
|
26929
27425
|
for (const f of s.modifiedFiles) {
|
|
26930
|
-
console.log(" " +
|
|
27426
|
+
console.log(" " + chalk29.yellow(f));
|
|
26931
27427
|
}
|
|
26932
27428
|
console.log("");
|
|
26933
27429
|
}
|
|
@@ -26935,13 +27431,13 @@ function renderDetail(s) {
|
|
|
26935
27431
|
function registerSessionsCommand(program2) {
|
|
26936
27432
|
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) => {
|
|
26937
27433
|
console.log("");
|
|
26938
|
-
console.log(
|
|
27434
|
+
console.log(chalk29.cyan.bold("\u{1F4CB} node9 sessions") + chalk29.dim(" \u2014 what your AI agent did"));
|
|
26939
27435
|
console.log("");
|
|
26940
27436
|
const days = options.detail || options.all ? null : Math.max(1, parseInt(options.days, 10) || 7);
|
|
26941
27437
|
const rangeLabel = options.detail ? "all time" : options.all ? "all time" : `last ${String(days)} days`;
|
|
26942
|
-
console.log(
|
|
27438
|
+
console.log(chalk29.dim(" " + rangeLabel));
|
|
26943
27439
|
console.log("");
|
|
26944
|
-
process.stdout.write(
|
|
27440
|
+
process.stdout.write(chalk29.dim(" Loading\u2026"));
|
|
26945
27441
|
const summaries = buildSessions(days);
|
|
26946
27442
|
if (process.stdout.isTTY) {
|
|
26947
27443
|
process.stdout.clearLine(0);
|
|
@@ -26954,8 +27450,8 @@ function registerSessionsCommand(program2) {
|
|
|
26954
27450
|
(s) => s.sessionId === options.detail || s.sessionId.startsWith(options.detail)
|
|
26955
27451
|
);
|
|
26956
27452
|
if (!target) {
|
|
26957
|
-
console.log(
|
|
26958
|
-
console.log(
|
|
27453
|
+
console.log(chalk29.red(` Session not found: ${options.detail}`));
|
|
27454
|
+
console.log(chalk29.dim(" Run `node9 sessions` to list recent sessions.\n"));
|
|
26959
27455
|
return;
|
|
26960
27456
|
}
|
|
26961
27457
|
renderDetail(target);
|
|
@@ -26969,7 +27465,7 @@ function registerSessionsCommand(program2) {
|
|
|
26969
27465
|
|
|
26970
27466
|
// src/cli/commands/session-taint.ts
|
|
26971
27467
|
init_daemon();
|
|
26972
|
-
import
|
|
27468
|
+
import chalk30 from "chalk";
|
|
26973
27469
|
function resolveSessionId(records, query) {
|
|
26974
27470
|
const exact = records.find((r) => r.sessionId === query);
|
|
26975
27471
|
if (exact) return { record: exact };
|
|
@@ -26995,22 +27491,22 @@ function registerSessionTaintCommand(program2) {
|
|
|
26995
27491
|
const records = await listSessionTaints();
|
|
26996
27492
|
console.log("");
|
|
26997
27493
|
if (records.length === 0) {
|
|
26998
|
-
console.log(
|
|
26999
|
-
console.log(
|
|
27494
|
+
console.log(chalk30.dim(" No tainted sessions."));
|
|
27495
|
+
console.log(chalk30.dim(" (Taint lives in the daemon \u2014 a stopped daemon has none.)") + "\n");
|
|
27000
27496
|
return;
|
|
27001
27497
|
}
|
|
27002
27498
|
console.log(
|
|
27003
|
-
" " +
|
|
27499
|
+
" " + chalk30.bold(String(records.length)) + chalk30.dim(` tainted session${records.length !== 1 ? "s" : ""}`)
|
|
27004
27500
|
);
|
|
27005
27501
|
console.log("");
|
|
27006
27502
|
for (const r of records) {
|
|
27007
27503
|
console.log(
|
|
27008
|
-
" " +
|
|
27504
|
+
" " + chalk30.yellow(r.sessionId.slice(0, 8).padEnd(10)) + chalk30.red(r.source) + sourceGap(r.source) + chalk30.dim("clears in " + fmtRemaining(r.expiresAt))
|
|
27009
27505
|
);
|
|
27010
27506
|
}
|
|
27011
27507
|
console.log("");
|
|
27012
27508
|
console.log(
|
|
27013
|
-
|
|
27509
|
+
chalk30.dim(" Run ") + chalk30.cyan("node9 session-taint clear <id>") + chalk30.dim(" to release one, or ") + chalk30.cyan("--all") + chalk30.dim(" for every session.") + "\n"
|
|
27014
27510
|
);
|
|
27015
27511
|
});
|
|
27016
27512
|
cmd.command("clear").description("Clear a session's taint so its next network/write action isn't held for review").argument("[sessionId]", "Session id to clear (the 8-char prefix from `list` is accepted)").option("--all", "Clear every session taint").action(async (sessionId, opts) => {
|
|
@@ -27018,32 +27514,32 @@ function registerSessionTaintCommand(program2) {
|
|
|
27018
27514
|
if (opts.all) {
|
|
27019
27515
|
const res2 = await clearSessionTaint({ all: true });
|
|
27020
27516
|
if (res2.daemonUnavailable) {
|
|
27021
|
-
console.log(
|
|
27517
|
+
console.log(chalk30.dim(" node9 daemon not running \u2014 no active taints to clear.") + "\n");
|
|
27022
27518
|
return;
|
|
27023
27519
|
}
|
|
27024
27520
|
console.log(
|
|
27025
|
-
|
|
27521
|
+
chalk30.green(" \u2713 ") + `Cleared ${chalk30.bold(String(res2.cleared))} session taint${res2.cleared !== 1 ? "s" : ""}.
|
|
27026
27522
|
`
|
|
27027
27523
|
);
|
|
27028
27524
|
return;
|
|
27029
27525
|
}
|
|
27030
27526
|
if (!sessionId) {
|
|
27031
|
-
console.log(
|
|
27032
|
-
console.log(
|
|
27527
|
+
console.log(chalk30.red(" Provide a session id or --all."));
|
|
27528
|
+
console.log(chalk30.dim(" Run `node9 session-taint list` to see tainted sessions.") + "\n");
|
|
27033
27529
|
return;
|
|
27034
27530
|
}
|
|
27035
27531
|
const records = await listSessionTaints();
|
|
27036
27532
|
if (records.length === 0) {
|
|
27037
|
-
console.log(
|
|
27533
|
+
console.log(chalk30.dim(" No tainted sessions \u2014 nothing to clear.") + "\n");
|
|
27038
27534
|
return;
|
|
27039
27535
|
}
|
|
27040
27536
|
const resolved = resolveSessionId(records, sessionId);
|
|
27041
27537
|
if ("error" in resolved) {
|
|
27042
27538
|
if (resolved.error === "not-found") {
|
|
27043
|
-
console.log(
|
|
27539
|
+
console.log(chalk30.red(` No tainted session matches "${sessionId}".`));
|
|
27044
27540
|
} else {
|
|
27045
|
-
console.log(
|
|
27046
|
-
for (const m of resolved.matches) console.log(
|
|
27541
|
+
console.log(chalk30.red(` "${sessionId}" is ambiguous \u2014 matches:`));
|
|
27542
|
+
for (const m of resolved.matches) console.log(chalk30.dim(" " + m));
|
|
27047
27543
|
}
|
|
27048
27544
|
console.log("");
|
|
27049
27545
|
return;
|
|
@@ -27051,24 +27547,24 @@ function registerSessionTaintCommand(program2) {
|
|
|
27051
27547
|
const res = await clearSessionTaint({ sessionId: resolved.record.sessionId });
|
|
27052
27548
|
if (res.cleared > 0) {
|
|
27053
27549
|
console.log(
|
|
27054
|
-
|
|
27550
|
+
chalk30.green(" \u2713 ") + `Cleared taint for ${chalk30.yellow(resolved.record.sessionId.slice(0, 8))} ` + chalk30.dim(`(was flagged by ${resolved.record.source}).`) + "\n"
|
|
27055
27551
|
);
|
|
27056
27552
|
} else {
|
|
27057
27553
|
console.log(
|
|
27058
|
-
|
|
27554
|
+
chalk30.dim(` Session ${resolved.record.sessionId.slice(0, 8)} was already clear.`) + "\n"
|
|
27059
27555
|
);
|
|
27060
27556
|
}
|
|
27061
27557
|
});
|
|
27062
27558
|
}
|
|
27063
27559
|
|
|
27064
27560
|
// src/cli/commands/skill-pin.ts
|
|
27065
|
-
import
|
|
27066
|
-
import
|
|
27067
|
-
import
|
|
27068
|
-
import
|
|
27561
|
+
import chalk31 from "chalk";
|
|
27562
|
+
import fs60 from "fs";
|
|
27563
|
+
import os50 from "os";
|
|
27564
|
+
import path57 from "path";
|
|
27069
27565
|
function wipeSkillSessions() {
|
|
27070
27566
|
try {
|
|
27071
|
-
|
|
27567
|
+
fs60.rmSync(path57.join(os50.homedir(), ".node9", "skill-sessions"), {
|
|
27072
27568
|
recursive: true,
|
|
27073
27569
|
force: true
|
|
27074
27570
|
});
|
|
@@ -27082,29 +27578,29 @@ function registerSkillPinCommand(program2) {
|
|
|
27082
27578
|
const result = readSkillPinsSafe();
|
|
27083
27579
|
if (!result.ok) {
|
|
27084
27580
|
if (result.reason === "missing") {
|
|
27085
|
-
console.log(
|
|
27581
|
+
console.log(chalk31.gray("\nNo skill roots are pinned yet."));
|
|
27086
27582
|
console.log(
|
|
27087
|
-
|
|
27583
|
+
chalk31.gray("Pins are created automatically on the first tool call of each session.\n")
|
|
27088
27584
|
);
|
|
27089
27585
|
return;
|
|
27090
27586
|
}
|
|
27091
|
-
console.error(
|
|
27587
|
+
console.error(chalk31.red(`
|
|
27092
27588
|
\u274C Pin file is corrupt: ${result.detail}`));
|
|
27093
|
-
console.error(
|
|
27589
|
+
console.error(chalk31.yellow(" Run: node9 skill pin reset\n"));
|
|
27094
27590
|
process.exit(1);
|
|
27095
27591
|
}
|
|
27096
27592
|
const entries = Object.entries(result.pins.roots);
|
|
27097
27593
|
if (entries.length === 0) {
|
|
27098
|
-
console.log(
|
|
27594
|
+
console.log(chalk31.gray("\nNo skill roots are pinned yet.\n"));
|
|
27099
27595
|
return;
|
|
27100
27596
|
}
|
|
27101
|
-
console.log(
|
|
27597
|
+
console.log(chalk31.bold("\n\u{1F512} Pinned Skill Roots\n"));
|
|
27102
27598
|
for (const [key, entry] of entries) {
|
|
27103
|
-
const missing = entry.exists ? "" :
|
|
27104
|
-
console.log(` ${
|
|
27599
|
+
const missing = entry.exists ? "" : chalk31.yellow(" (not present at pin time)");
|
|
27600
|
+
console.log(` ${chalk31.cyan(key)} ${chalk31.gray(entry.rootPath)}${missing}`);
|
|
27105
27601
|
console.log(` Files (${entry.fileCount})`);
|
|
27106
|
-
console.log(` Hash: ${
|
|
27107
|
-
console.log(` Pinned: ${
|
|
27602
|
+
console.log(` Hash: ${chalk31.gray(entry.contentHash.slice(0, 16))}...`);
|
|
27603
|
+
console.log(` Pinned: ${chalk31.gray(entry.pinnedAt)}
|
|
27108
27604
|
`);
|
|
27109
27605
|
}
|
|
27110
27606
|
});
|
|
@@ -27113,52 +27609,52 @@ function registerSkillPinCommand(program2) {
|
|
|
27113
27609
|
try {
|
|
27114
27610
|
pins = readSkillPins();
|
|
27115
27611
|
} catch {
|
|
27116
|
-
console.error(
|
|
27117
|
-
console.error(
|
|
27612
|
+
console.error(chalk31.red("\n\u274C Pin file is corrupt."));
|
|
27613
|
+
console.error(chalk31.yellow(" Run: node9 skill pin reset\n"));
|
|
27118
27614
|
process.exit(1);
|
|
27119
27615
|
}
|
|
27120
27616
|
if (!pins.roots[rootKey]) {
|
|
27121
|
-
console.error(
|
|
27617
|
+
console.error(chalk31.red(`
|
|
27122
27618
|
\u274C No pin found for root key "${rootKey}"
|
|
27123
27619
|
`));
|
|
27124
|
-
console.error(`Run ${
|
|
27620
|
+
console.error(`Run ${chalk31.cyan("node9 skill pin list")} to see pinned roots.
|
|
27125
27621
|
`);
|
|
27126
27622
|
process.exit(1);
|
|
27127
27623
|
}
|
|
27128
27624
|
const rootPath = pins.roots[rootKey].rootPath;
|
|
27129
27625
|
removePin2(rootKey);
|
|
27130
27626
|
wipeSkillSessions();
|
|
27131
|
-
console.log(
|
|
27132
|
-
\u{1F513} Pin removed for ${
|
|
27133
|
-
console.log(
|
|
27134
|
-
console.log(
|
|
27627
|
+
console.log(chalk31.green(`
|
|
27628
|
+
\u{1F513} Pin removed for ${chalk31.cyan(rootKey)}`));
|
|
27629
|
+
console.log(chalk31.gray(` ${rootPath}`));
|
|
27630
|
+
console.log(chalk31.gray(" Next session will re-pin with current state.\n"));
|
|
27135
27631
|
});
|
|
27136
27632
|
pinSubCmd.command("reset").description("Clear all skill pins and wipe session verification flags").action(() => {
|
|
27137
27633
|
const result = readSkillPinsSafe();
|
|
27138
27634
|
if (!result.ok && result.reason === "missing") {
|
|
27139
27635
|
wipeSkillSessions();
|
|
27140
|
-
console.log(
|
|
27636
|
+
console.log(chalk31.gray("\nNo pins to clear.\n"));
|
|
27141
27637
|
return;
|
|
27142
27638
|
}
|
|
27143
27639
|
const count = result.ok ? Object.keys(result.pins.roots).length : "?";
|
|
27144
27640
|
clearAllPins2();
|
|
27145
27641
|
wipeSkillSessions();
|
|
27146
|
-
console.log(
|
|
27642
|
+
console.log(chalk31.green(`
|
|
27147
27643
|
\u{1F513} Cleared ${count} skill pin(s).`));
|
|
27148
|
-
console.log(
|
|
27644
|
+
console.log(chalk31.gray(" Next session will re-pin with current state.\n"));
|
|
27149
27645
|
});
|
|
27150
27646
|
}
|
|
27151
27647
|
|
|
27152
27648
|
// src/cli/commands/decisions.ts
|
|
27153
|
-
import
|
|
27154
|
-
import
|
|
27155
|
-
import
|
|
27156
|
-
import
|
|
27157
|
-
var DECISIONS_FILE2 =
|
|
27649
|
+
import fs61 from "fs";
|
|
27650
|
+
import os51 from "os";
|
|
27651
|
+
import path58 from "path";
|
|
27652
|
+
import chalk32 from "chalk";
|
|
27653
|
+
var DECISIONS_FILE2 = path58.join(os51.homedir(), ".node9", "decisions.json");
|
|
27158
27654
|
function readDecisions() {
|
|
27159
27655
|
try {
|
|
27160
|
-
if (!
|
|
27161
|
-
const raw =
|
|
27656
|
+
if (!fs61.existsSync(DECISIONS_FILE2)) return {};
|
|
27657
|
+
const raw = fs61.readFileSync(DECISIONS_FILE2, "utf-8");
|
|
27162
27658
|
const parsed = JSON.parse(raw);
|
|
27163
27659
|
const out = {};
|
|
27164
27660
|
for (const [k, v] of Object.entries(parsed)) {
|
|
@@ -27170,11 +27666,11 @@ function readDecisions() {
|
|
|
27170
27666
|
}
|
|
27171
27667
|
}
|
|
27172
27668
|
function writeDecisions(d) {
|
|
27173
|
-
const dir =
|
|
27174
|
-
if (!
|
|
27669
|
+
const dir = path58.dirname(DECISIONS_FILE2);
|
|
27670
|
+
if (!fs61.existsSync(dir)) fs61.mkdirSync(dir, { recursive: true });
|
|
27175
27671
|
const tmp = `${DECISIONS_FILE2}.${process.pid}.tmp`;
|
|
27176
|
-
|
|
27177
|
-
|
|
27672
|
+
fs61.writeFileSync(tmp, JSON.stringify(d, null, 2));
|
|
27673
|
+
fs61.renameSync(tmp, DECISIONS_FILE2);
|
|
27178
27674
|
}
|
|
27179
27675
|
function registerDecisionsCommand(program2) {
|
|
27180
27676
|
const cmd = program2.command("decisions").description('Manage persistent "Always Allow" / "Always Deny" tool decisions');
|
|
@@ -27182,67 +27678,67 @@ function registerDecisionsCommand(program2) {
|
|
|
27182
27678
|
const decisions = readDecisions();
|
|
27183
27679
|
const entries = Object.entries(decisions);
|
|
27184
27680
|
if (entries.length === 0) {
|
|
27185
|
-
console.log(
|
|
27681
|
+
console.log(chalk32.gray(" No persistent decisions stored."));
|
|
27186
27682
|
console.log(
|
|
27187
|
-
|
|
27188
|
-
`) +
|
|
27683
|
+
chalk32.gray(` File: ${DECISIONS_FILE2}
|
|
27684
|
+
`) + chalk32.gray(' Decisions are written when you click "Always Allow" or')
|
|
27189
27685
|
);
|
|
27190
|
-
console.log(
|
|
27686
|
+
console.log(chalk32.gray(' "Always Deny" in node9 tail or the native popup.'));
|
|
27191
27687
|
return;
|
|
27192
27688
|
}
|
|
27193
|
-
console.log(
|
|
27689
|
+
console.log(chalk32.bold(`
|
|
27194
27690
|
Persistent decisions (${entries.length})
|
|
27195
27691
|
`));
|
|
27196
27692
|
const w = Math.max(...entries.map(([k]) => k.length));
|
|
27197
27693
|
for (const [tool, verdict] of entries.sort()) {
|
|
27198
|
-
const colored = verdict === "allow" ?
|
|
27694
|
+
const colored = verdict === "allow" ? chalk32.green(verdict) : chalk32.red(verdict);
|
|
27199
27695
|
console.log(` ${tool.padEnd(w)} ${colored}`);
|
|
27200
27696
|
}
|
|
27201
27697
|
console.log(
|
|
27202
|
-
|
|
27698
|
+
chalk32.gray(`
|
|
27203
27699
|
Stored in ${DECISIONS_FILE2}
|
|
27204
|
-
`) +
|
|
27700
|
+
`) + chalk32.gray(" Run `node9 decisions clear <tool>` to remove an entry.")
|
|
27205
27701
|
);
|
|
27206
27702
|
});
|
|
27207
27703
|
cmd.command("clear <toolName>").description("Remove a persistent decision for one tool").action((toolName) => {
|
|
27208
27704
|
const decisions = readDecisions();
|
|
27209
27705
|
if (!(toolName in decisions)) {
|
|
27210
|
-
console.log(
|
|
27706
|
+
console.log(chalk32.yellow(` No persistent decision for "${toolName}". Nothing to clear.`));
|
|
27211
27707
|
process.exitCode = 1;
|
|
27212
27708
|
return;
|
|
27213
27709
|
}
|
|
27214
27710
|
delete decisions[toolName];
|
|
27215
27711
|
writeDecisions(decisions);
|
|
27216
|
-
console.log(
|
|
27712
|
+
console.log(chalk32.green(` \u2713 Cleared persistent decision for "${toolName}".`));
|
|
27217
27713
|
});
|
|
27218
27714
|
cmd.command("clear-all").description("Remove every persistent decision (irreversible)").action(() => {
|
|
27219
27715
|
const decisions = readDecisions();
|
|
27220
27716
|
const count = Object.keys(decisions).length;
|
|
27221
27717
|
if (count === 0) {
|
|
27222
|
-
console.log(
|
|
27718
|
+
console.log(chalk32.gray(" Nothing to clear \u2014 no persistent decisions stored."));
|
|
27223
27719
|
return;
|
|
27224
27720
|
}
|
|
27225
27721
|
writeDecisions({});
|
|
27226
27722
|
console.log(
|
|
27227
|
-
|
|
27723
|
+
chalk32.green(` \u2713 Cleared ${count} persistent decision${count === 1 ? "" : "s"}.`)
|
|
27228
27724
|
);
|
|
27229
27725
|
});
|
|
27230
27726
|
}
|
|
27231
27727
|
|
|
27232
27728
|
// src/cli/commands/dlp.ts
|
|
27233
|
-
import
|
|
27234
|
-
import
|
|
27235
|
-
import
|
|
27236
|
-
import
|
|
27237
|
-
var AUDIT_LOG =
|
|
27238
|
-
var RESOLVED_FILE =
|
|
27729
|
+
import chalk33 from "chalk";
|
|
27730
|
+
import fs62 from "fs";
|
|
27731
|
+
import path59 from "path";
|
|
27732
|
+
import os52 from "os";
|
|
27733
|
+
var AUDIT_LOG = path59.join(os52.homedir(), ".node9", "audit.log");
|
|
27734
|
+
var RESOLVED_FILE = path59.join(os52.homedir(), ".node9", "dlp-resolved.json");
|
|
27239
27735
|
var ANSI_RE = /\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g;
|
|
27240
27736
|
function stripAnsi(s) {
|
|
27241
27737
|
return s.replace(ANSI_RE, "");
|
|
27242
27738
|
}
|
|
27243
27739
|
function loadResolved() {
|
|
27244
27740
|
try {
|
|
27245
|
-
const raw = JSON.parse(
|
|
27741
|
+
const raw = JSON.parse(fs62.readFileSync(RESOLVED_FILE, "utf-8"));
|
|
27246
27742
|
return new Set(raw);
|
|
27247
27743
|
} catch {
|
|
27248
27744
|
return /* @__PURE__ */ new Set();
|
|
@@ -27250,13 +27746,13 @@ function loadResolved() {
|
|
|
27250
27746
|
}
|
|
27251
27747
|
function saveResolved(resolved) {
|
|
27252
27748
|
try {
|
|
27253
|
-
|
|
27749
|
+
fs62.writeFileSync(RESOLVED_FILE, JSON.stringify([...resolved], null, 2), { mode: 384 });
|
|
27254
27750
|
} catch {
|
|
27255
27751
|
}
|
|
27256
27752
|
}
|
|
27257
27753
|
function loadDlpFindings() {
|
|
27258
|
-
if (!
|
|
27259
|
-
return
|
|
27754
|
+
if (!fs62.existsSync(AUDIT_LOG)) return [];
|
|
27755
|
+
return fs62.readFileSync(AUDIT_LOG, "utf-8").split("\n").flatMap((line) => {
|
|
27260
27756
|
if (!line.trim()) return [];
|
|
27261
27757
|
try {
|
|
27262
27758
|
const e = JSON.parse(line);
|
|
@@ -27285,14 +27781,14 @@ function registerDlpCommand(program2) {
|
|
|
27285
27781
|
cmd.command("resolve").description("Mark all current DLP findings as resolved").action(() => {
|
|
27286
27782
|
const findings = loadDlpFindings();
|
|
27287
27783
|
if (findings.length === 0) {
|
|
27288
|
-
console.log(
|
|
27784
|
+
console.log(chalk33.green("\n \u2705 No response-DLP findings to resolve.\n"));
|
|
27289
27785
|
return;
|
|
27290
27786
|
}
|
|
27291
27787
|
const resolved = loadResolved();
|
|
27292
27788
|
for (const e of findings) resolved.add(entryKey2(e));
|
|
27293
27789
|
saveResolved(resolved);
|
|
27294
27790
|
console.log(
|
|
27295
|
-
|
|
27791
|
+
chalk33.green(
|
|
27296
27792
|
`
|
|
27297
27793
|
\u2705 ${findings.length} finding${findings.length !== 1 ? "s" : ""} marked as resolved.
|
|
27298
27794
|
`
|
|
@@ -27306,47 +27802,47 @@ function registerDlpCommand(program2) {
|
|
|
27306
27802
|
const resolvedCount = findings.length - open.length;
|
|
27307
27803
|
console.log("");
|
|
27308
27804
|
console.log(
|
|
27309
|
-
|
|
27805
|
+
chalk33.bold.cyan("\u{1F510} node9 dlp") + chalk33.dim(" \u2014 secrets found in Claude response text")
|
|
27310
27806
|
);
|
|
27311
27807
|
console.log("");
|
|
27312
27808
|
if (open.length === 0) {
|
|
27313
27809
|
if (resolvedCount > 0) {
|
|
27314
|
-
console.log(
|
|
27810
|
+
console.log(chalk33.green(` \u2705 No open findings \xB7 ${resolvedCount} previously resolved`));
|
|
27315
27811
|
} else {
|
|
27316
27812
|
console.log(
|
|
27317
|
-
|
|
27813
|
+
chalk33.green(" \u2705 No findings \u2014 Claude has not leaked secrets in response text")
|
|
27318
27814
|
);
|
|
27319
27815
|
}
|
|
27320
27816
|
console.log("");
|
|
27321
27817
|
return;
|
|
27322
27818
|
}
|
|
27323
27819
|
console.log(
|
|
27324
|
-
|
|
27820
|
+
chalk33.bgRed.white.bold(` \u26A0\uFE0F ${open.length} open finding${open.length !== 1 ? "s" : ""} `) + chalk33.dim(resolvedCount > 0 ? ` (${resolvedCount} resolved)` : "")
|
|
27325
27821
|
);
|
|
27326
27822
|
console.log("");
|
|
27327
27823
|
console.log(
|
|
27328
|
-
|
|
27824
|
+
chalk33.dim(" These secrets were included in Claude's response text \u2014 NOT blocked.")
|
|
27329
27825
|
);
|
|
27330
|
-
console.log(
|
|
27826
|
+
console.log(chalk33.dim(" Rotate each affected key immediately.\n"));
|
|
27331
27827
|
for (const e of open) {
|
|
27332
27828
|
console.log(
|
|
27333
|
-
" " +
|
|
27829
|
+
" " + chalk33.red("\u25CF") + " " + chalk33.white(e.dlpPattern ?? "Secret") + chalk33.dim(" " + fmtDate3(e.ts))
|
|
27334
27830
|
);
|
|
27335
27831
|
if (e.dlpSample) {
|
|
27336
|
-
console.log(" " +
|
|
27832
|
+
console.log(" " + chalk33.dim("Sample: ") + chalk33.yellow(stripAnsi(e.dlpSample)));
|
|
27337
27833
|
}
|
|
27338
27834
|
if (e.project) {
|
|
27339
|
-
console.log(" " +
|
|
27835
|
+
console.log(" " + chalk33.dim("Project: ") + chalk33.dim(stripAnsi(e.project)));
|
|
27340
27836
|
}
|
|
27341
27837
|
console.log("");
|
|
27342
27838
|
}
|
|
27343
|
-
console.log(" " +
|
|
27344
|
-
console.log(" " +
|
|
27839
|
+
console.log(" " + chalk33.bold("Next steps:"));
|
|
27840
|
+
console.log(" " + chalk33.cyan("1.") + " Rotate any exposed keys shown above");
|
|
27345
27841
|
console.log(
|
|
27346
|
-
" " +
|
|
27842
|
+
" " + chalk33.cyan("2.") + " Run " + chalk33.white("node9 dlp resolve") + " to acknowledge"
|
|
27347
27843
|
);
|
|
27348
27844
|
console.log(
|
|
27349
|
-
" " +
|
|
27845
|
+
" " + chalk33.cyan("3.") + " Run " + chalk33.white("node9 report") + " for full audit history"
|
|
27350
27846
|
);
|
|
27351
27847
|
console.log("");
|
|
27352
27848
|
});
|
|
@@ -27354,15 +27850,15 @@ function registerDlpCommand(program2) {
|
|
|
27354
27850
|
|
|
27355
27851
|
// src/cli/commands/mask.ts
|
|
27356
27852
|
init_dlp();
|
|
27357
|
-
import
|
|
27358
|
-
import
|
|
27359
|
-
import
|
|
27360
|
-
import
|
|
27853
|
+
import chalk34 from "chalk";
|
|
27854
|
+
import fs63 from "fs";
|
|
27855
|
+
import path60 from "path";
|
|
27856
|
+
import os53 from "os";
|
|
27361
27857
|
function findJsonlFiles(dir) {
|
|
27362
27858
|
const results = [];
|
|
27363
|
-
if (!
|
|
27364
|
-
for (const entry of
|
|
27365
|
-
const full =
|
|
27859
|
+
if (!fs63.existsSync(dir)) return results;
|
|
27860
|
+
for (const entry of fs63.readdirSync(dir, { withFileTypes: true })) {
|
|
27861
|
+
const full = path60.join(dir, entry.name);
|
|
27366
27862
|
if (entry.isDirectory()) results.push(...findJsonlFiles(full));
|
|
27367
27863
|
else if (entry.isFile() && entry.name.endsWith(".jsonl")) results.push(full);
|
|
27368
27864
|
}
|
|
@@ -27405,7 +27901,7 @@ function redactJson(obj) {
|
|
|
27405
27901
|
function processFile(filePath, dryRun) {
|
|
27406
27902
|
let raw;
|
|
27407
27903
|
try {
|
|
27408
|
-
raw =
|
|
27904
|
+
raw = fs63.readFileSync(filePath, "utf-8");
|
|
27409
27905
|
} catch {
|
|
27410
27906
|
return { redactedLines: 0, patterns: [] };
|
|
27411
27907
|
}
|
|
@@ -27437,14 +27933,14 @@ function processFile(filePath, dryRun) {
|
|
|
27437
27933
|
}
|
|
27438
27934
|
}
|
|
27439
27935
|
if (!dryRun && redactedLines > 0) {
|
|
27440
|
-
|
|
27936
|
+
fs63.writeFileSync(filePath, newLines.join("\n"), "utf-8");
|
|
27441
27937
|
}
|
|
27442
27938
|
return { redactedLines, patterns };
|
|
27443
27939
|
}
|
|
27444
27940
|
function processJsonFile(filePath, dryRun) {
|
|
27445
27941
|
let raw;
|
|
27446
27942
|
try {
|
|
27447
|
-
raw =
|
|
27943
|
+
raw = fs63.readFileSync(filePath, "utf-8");
|
|
27448
27944
|
} catch {
|
|
27449
27945
|
return { redactedLines: 0, patterns: [] };
|
|
27450
27946
|
}
|
|
@@ -27457,15 +27953,15 @@ function processJsonFile(filePath, dryRun) {
|
|
|
27457
27953
|
const { value, modified, found } = redactJson(parsed);
|
|
27458
27954
|
if (!modified) return { redactedLines: 0, patterns: [] };
|
|
27459
27955
|
if (!dryRun) {
|
|
27460
|
-
|
|
27956
|
+
fs63.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
|
|
27461
27957
|
}
|
|
27462
27958
|
return { redactedLines: 1, patterns: found };
|
|
27463
27959
|
}
|
|
27464
27960
|
function findJsonFiles(dir) {
|
|
27465
27961
|
const results = [];
|
|
27466
|
-
if (!
|
|
27467
|
-
for (const entry of
|
|
27468
|
-
const full =
|
|
27962
|
+
if (!fs63.existsSync(dir)) return results;
|
|
27963
|
+
for (const entry of fs63.readdirSync(dir, { withFileTypes: true })) {
|
|
27964
|
+
const full = path60.join(dir, entry.name);
|
|
27469
27965
|
if (entry.isDirectory()) results.push(...findJsonFiles(full));
|
|
27470
27966
|
else if (entry.isFile() && entry.name.endsWith(".json")) results.push(full);
|
|
27471
27967
|
}
|
|
@@ -27474,9 +27970,9 @@ function findJsonFiles(dir) {
|
|
|
27474
27970
|
function registerMaskCommand(program2) {
|
|
27475
27971
|
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) => {
|
|
27476
27972
|
const dryRun = !!options.dryRun;
|
|
27477
|
-
const home =
|
|
27478
|
-
const claudeDir =
|
|
27479
|
-
const geminiDir =
|
|
27973
|
+
const home = os53.homedir();
|
|
27974
|
+
const claudeDir = path60.join(home, ".claude", "projects");
|
|
27975
|
+
const geminiDir = path60.join(home, ".gemini", "tmp");
|
|
27480
27976
|
const allFiles = [
|
|
27481
27977
|
...findJsonlFiles(claudeDir).map((p) => ({ path: p, type: "jsonl" })),
|
|
27482
27978
|
...findJsonFiles(geminiDir).map((p) => ({ path: p, type: "json" }))
|
|
@@ -27484,18 +27980,18 @@ function registerMaskCommand(program2) {
|
|
|
27484
27980
|
const cutoff = options.all ? null : new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3);
|
|
27485
27981
|
const filtered = cutoff ? allFiles.filter((f) => {
|
|
27486
27982
|
try {
|
|
27487
|
-
return
|
|
27983
|
+
return fs63.statSync(f.path).mtime >= cutoff;
|
|
27488
27984
|
} catch {
|
|
27489
27985
|
return false;
|
|
27490
27986
|
}
|
|
27491
27987
|
}) : allFiles;
|
|
27492
27988
|
if (filtered.length === 0) {
|
|
27493
|
-
console.log(
|
|
27989
|
+
console.log(chalk34.yellow(" No session files found."));
|
|
27494
27990
|
return;
|
|
27495
27991
|
}
|
|
27496
27992
|
console.log("");
|
|
27497
27993
|
if (dryRun) {
|
|
27498
|
-
console.log(
|
|
27994
|
+
console.log(chalk34.dim(" Dry run \u2014 no files will be modified.\n"));
|
|
27499
27995
|
}
|
|
27500
27996
|
let totalFiles = 0;
|
|
27501
27997
|
let totalLines = 0;
|
|
@@ -27511,23 +28007,23 @@ function registerMaskCommand(program2) {
|
|
|
27511
28007
|
});
|
|
27512
28008
|
const verb = dryRun ? "Would redact" : "Redacted";
|
|
27513
28009
|
console.log(
|
|
27514
|
-
" " +
|
|
28010
|
+
" " + chalk34.dim(shortPath.slice(0, 60).padEnd(62)) + chalk34.red(`${verb}: `) + chalk34.yellow(patterns.join(", ")) + chalk34.dim(` (${redactedLines} line${redactedLines !== 1 ? "s" : ""})`)
|
|
27515
28011
|
);
|
|
27516
28012
|
}
|
|
27517
28013
|
}
|
|
27518
28014
|
console.log("");
|
|
27519
28015
|
if (totalFiles === 0) {
|
|
27520
|
-
console.log(
|
|
28016
|
+
console.log(chalk34.green(" No secrets found in session history."));
|
|
27521
28017
|
} else {
|
|
27522
28018
|
const verb = dryRun ? "would be modified" : "modified";
|
|
27523
28019
|
console.log(
|
|
27524
|
-
|
|
28020
|
+
chalk34.bold(` ${totalFiles} file${totalFiles !== 1 ? "s" : ""} ${verb}`) + chalk34.dim(`, ${totalLines} line${totalLines !== 1 ? "s" : ""} redacted`)
|
|
27525
28021
|
);
|
|
27526
|
-
console.log(" Patterns: " +
|
|
28022
|
+
console.log(" Patterns: " + chalk34.yellow(totalPatterns.join(", ")));
|
|
27527
28023
|
if (!dryRun) {
|
|
27528
28024
|
console.log("");
|
|
27529
28025
|
console.log(
|
|
27530
|
-
|
|
28026
|
+
chalk34.dim(
|
|
27531
28027
|
" Note: secrets were already sent to the AI provider during the active session.\n This cleans your local disk only. Rotate any exposed keys."
|
|
27532
28028
|
)
|
|
27533
28029
|
);
|
|
@@ -27540,20 +28036,20 @@ function registerMaskCommand(program2) {
|
|
|
27540
28036
|
// src/cli.ts
|
|
27541
28037
|
init_blast();
|
|
27542
28038
|
var { version } = JSON.parse(
|
|
27543
|
-
|
|
28039
|
+
fs66.readFileSync(path63.join(__dirname, "../package.json"), "utf-8")
|
|
27544
28040
|
);
|
|
27545
28041
|
var program = new Command();
|
|
27546
28042
|
program.name("node9").description("The Sudo Command for AI Agents").version(version);
|
|
27547
28043
|
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) => {
|
|
27548
28044
|
const DEFAULT_API_URL2 = "https://api.node9.ai/api/v1/intercept";
|
|
27549
|
-
const credPath =
|
|
27550
|
-
if (!
|
|
27551
|
-
|
|
28045
|
+
const credPath = path63.join(os56.homedir(), ".node9", "credentials.json");
|
|
28046
|
+
if (!fs66.existsSync(path63.dirname(credPath)))
|
|
28047
|
+
fs66.mkdirSync(path63.dirname(credPath), { recursive: true });
|
|
27552
28048
|
const profileName = options.profile || "default";
|
|
27553
28049
|
let existingCreds = {};
|
|
27554
28050
|
try {
|
|
27555
|
-
if (
|
|
27556
|
-
const raw = JSON.parse(
|
|
28051
|
+
if (fs66.existsSync(credPath)) {
|
|
28052
|
+
const raw = JSON.parse(fs66.readFileSync(credPath, "utf-8"));
|
|
27557
28053
|
if (raw.apiKey) {
|
|
27558
28054
|
existingCreds = {
|
|
27559
28055
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL2 }
|
|
@@ -27565,14 +28061,14 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
27565
28061
|
} catch {
|
|
27566
28062
|
}
|
|
27567
28063
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL2 };
|
|
27568
|
-
|
|
28064
|
+
fs66.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
27569
28065
|
let effectiveCloud = null;
|
|
27570
28066
|
if (profileName === "default") {
|
|
27571
|
-
const
|
|
28067
|
+
const configPath = path63.join(os56.homedir(), ".node9", "config.json");
|
|
27572
28068
|
let config = {};
|
|
27573
28069
|
try {
|
|
27574
|
-
if (
|
|
27575
|
-
config = JSON.parse(
|
|
28070
|
+
if (fs66.existsSync(configPath))
|
|
28071
|
+
config = JSON.parse(fs66.readFileSync(configPath, "utf-8"));
|
|
27576
28072
|
} catch {
|
|
27577
28073
|
}
|
|
27578
28074
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -27587,35 +28083,35 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
27587
28083
|
approvers.cloud = false;
|
|
27588
28084
|
}
|
|
27589
28085
|
s.approvers = approvers;
|
|
27590
|
-
if (!
|
|
27591
|
-
|
|
27592
|
-
|
|
28086
|
+
if (!fs66.existsSync(path63.dirname(configPath)))
|
|
28087
|
+
fs66.mkdirSync(path63.dirname(configPath), { recursive: true });
|
|
28088
|
+
fs66.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
27593
28089
|
effectiveCloud = approvers.cloud === true;
|
|
27594
28090
|
}
|
|
27595
28091
|
if (options.profile && profileName !== "default") {
|
|
27596
|
-
console.log(
|
|
27597
|
-
console.log(
|
|
28092
|
+
console.log(chalk36.green(`\u2705 Profile "${profileName}" saved`));
|
|
28093
|
+
console.log(chalk36.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
27598
28094
|
} else if (options.local || effectiveCloud === false) {
|
|
27599
|
-
console.log(
|
|
27600
|
-
console.log(
|
|
28095
|
+
console.log(chalk36.green(`\u2705 Key saved \u2014 Privacy mode \u{1F6E1}\uFE0F`));
|
|
28096
|
+
console.log(chalk36.gray(` All decisions stay on this machine. Nothing syncs to the cloud.`));
|
|
27601
28097
|
if (!options.local) {
|
|
27602
28098
|
console.log(
|
|
27603
|
-
|
|
28099
|
+
chalk36.yellow(` Your config has cloud approvals OFF (settings.approvers.cloud).`)
|
|
27604
28100
|
);
|
|
27605
28101
|
console.log(
|
|
27606
|
-
|
|
28102
|
+
chalk36.gray(` To enable team policy + dashboard sync: set it to true, or re-init.`)
|
|
27607
28103
|
);
|
|
27608
28104
|
}
|
|
27609
28105
|
} else {
|
|
27610
|
-
console.log(
|
|
27611
|
-
console.log(
|
|
28106
|
+
console.log(chalk36.green(`\u2705 Logged in \u2014 agent mode`));
|
|
28107
|
+
console.log(chalk36.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
27612
28108
|
}
|
|
27613
28109
|
});
|
|
27614
28110
|
program.command("signup").description("Create your node9 account / open the dashboard in your browser").option("--login", "Open the login page instead of signup").action((options) => {
|
|
27615
28111
|
const route = options.login ? "auth/login" : "auth/signup";
|
|
27616
28112
|
const url = `https://node9.ai/${route}?ref=cli_cmd`;
|
|
27617
28113
|
console.log("");
|
|
27618
|
-
console.log(" " +
|
|
28114
|
+
console.log(" " + chalk36.dim("Opening ") + chalk36.cyan.underline(url));
|
|
27619
28115
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
27620
28116
|
try {
|
|
27621
28117
|
const child = spawn9(opener, [url], {
|
|
@@ -27648,7 +28144,7 @@ program.command("addto", { hidden: true }).description("Integrate Node9 with an
|
|
|
27648
28144
|
if (target === "hermes") return setupHermes();
|
|
27649
28145
|
if (target === "hud") return setupHud();
|
|
27650
28146
|
console.error(
|
|
27651
|
-
|
|
28147
|
+
chalk36.red(
|
|
27652
28148
|
`Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
|
|
27653
28149
|
)
|
|
27654
28150
|
);
|
|
@@ -27662,20 +28158,20 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
|
|
|
27662
28158
|
"The agent to protect: claude | antigravity | copilot | gemini | cursor | codex | windsurf | vscode | hud"
|
|
27663
28159
|
).action(async (target) => {
|
|
27664
28160
|
if (!target) {
|
|
27665
|
-
console.log(
|
|
27666
|
-
console.log(" Usage: " +
|
|
28161
|
+
console.log(chalk36.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
28162
|
+
console.log(" Usage: " + chalk36.white("node9 setup <target>") + "\n");
|
|
27667
28163
|
console.log(" Targets:");
|
|
27668
|
-
console.log(" " +
|
|
27669
|
-
console.log(" " +
|
|
27670
|
-
console.log(" " +
|
|
27671
|
-
console.log(" " +
|
|
27672
|
-
console.log(" " +
|
|
27673
|
-
console.log(" " +
|
|
27674
|
-
console.log(" " +
|
|
27675
|
-
console.log(" " +
|
|
27676
|
-
console.log(" " +
|
|
28164
|
+
console.log(" " + chalk36.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
28165
|
+
console.log(" " + chalk36.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
28166
|
+
console.log(" " + chalk36.green("antigravity") + " \u2014 Antigravity / agy (hook mode)");
|
|
28167
|
+
console.log(" " + chalk36.green("copilot") + " \u2014 GitHub Copilot CLI (hook mode)");
|
|
28168
|
+
console.log(" " + chalk36.green("cursor") + " \u2014 Cursor (MCP proxy)");
|
|
28169
|
+
console.log(" " + chalk36.green("codex") + " \u2014 OpenAI Codex CLI (MCP proxy)");
|
|
28170
|
+
console.log(" " + chalk36.green("windsurf") + " \u2014 Windsurf (MCP proxy)");
|
|
28171
|
+
console.log(" " + chalk36.green("vscode") + " \u2014 VSCode / Copilot (MCP proxy)");
|
|
28172
|
+
console.log(" " + chalk36.green("hermes") + " \u2014 Hermes Agent (hook mode)");
|
|
27677
28173
|
process.stdout.write(
|
|
27678
|
-
" " +
|
|
28174
|
+
" " + chalk36.green("hud") + " \u2014 Claude Code security statusline\n"
|
|
27679
28175
|
);
|
|
27680
28176
|
console.log("");
|
|
27681
28177
|
return;
|
|
@@ -27692,7 +28188,7 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
|
|
|
27692
28188
|
if (t === "hermes") return setupHermes();
|
|
27693
28189
|
if (t === "hud") return setupHud();
|
|
27694
28190
|
console.error(
|
|
27695
|
-
|
|
28191
|
+
chalk36.red(
|
|
27696
28192
|
`Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
|
|
27697
28193
|
)
|
|
27698
28194
|
);
|
|
@@ -27718,33 +28214,33 @@ program.command("removefrom", { hidden: true }).description("Remove Node9 hooks
|
|
|
27718
28214
|
else if (target === "hud") fn = teardownHud;
|
|
27719
28215
|
else {
|
|
27720
28216
|
console.error(
|
|
27721
|
-
|
|
28217
|
+
chalk36.red(
|
|
27722
28218
|
`Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
|
|
27723
28219
|
)
|
|
27724
28220
|
);
|
|
27725
28221
|
process.exit(1);
|
|
27726
28222
|
}
|
|
27727
|
-
console.log(
|
|
28223
|
+
console.log(chalk36.cyan(`
|
|
27728
28224
|
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
27729
28225
|
`));
|
|
27730
28226
|
try {
|
|
27731
28227
|
fn();
|
|
27732
28228
|
} catch (err2) {
|
|
27733
|
-
console.error(
|
|
28229
|
+
console.error(chalk36.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
27734
28230
|
process.exit(1);
|
|
27735
28231
|
}
|
|
27736
|
-
console.log(
|
|
28232
|
+
console.log(chalk36.gray("\n Restart the agent for changes to take effect."));
|
|
27737
28233
|
});
|
|
27738
28234
|
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) => {
|
|
27739
|
-
console.log(
|
|
27740
|
-
console.log(
|
|
28235
|
+
console.log(chalk36.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
28236
|
+
console.log(chalk36.bold("Stopping daemon..."));
|
|
27741
28237
|
try {
|
|
27742
28238
|
stopDaemon();
|
|
27743
|
-
console.log(
|
|
28239
|
+
console.log(chalk36.green(" \u2705 Daemon stopped"));
|
|
27744
28240
|
} catch {
|
|
27745
|
-
console.log(
|
|
28241
|
+
console.log(chalk36.blue(" \u2139\uFE0F Daemon was not running"));
|
|
27746
28242
|
}
|
|
27747
|
-
console.log(
|
|
28243
|
+
console.log(chalk36.bold("\nRemoving hooks..."));
|
|
27748
28244
|
let teardownFailed = false;
|
|
27749
28245
|
for (const [label2, fn] of [
|
|
27750
28246
|
["Claude", teardownClaude],
|
|
@@ -27760,45 +28256,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
27760
28256
|
} catch (err2) {
|
|
27761
28257
|
teardownFailed = true;
|
|
27762
28258
|
console.error(
|
|
27763
|
-
|
|
28259
|
+
chalk36.red(
|
|
27764
28260
|
` \u26A0\uFE0F Failed to remove ${label2} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
27765
28261
|
)
|
|
27766
28262
|
);
|
|
27767
28263
|
}
|
|
27768
28264
|
}
|
|
27769
28265
|
if (options.purge) {
|
|
27770
|
-
const node9Dir =
|
|
27771
|
-
if (
|
|
28266
|
+
const node9Dir = path63.join(os56.homedir(), ".node9");
|
|
28267
|
+
if (fs66.existsSync(node9Dir)) {
|
|
27772
28268
|
const confirmed = await confirm2({
|
|
27773
28269
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
27774
28270
|
default: false
|
|
27775
28271
|
});
|
|
27776
28272
|
if (confirmed) {
|
|
27777
|
-
|
|
27778
|
-
if (
|
|
28273
|
+
fs66.rmSync(node9Dir, { recursive: true });
|
|
28274
|
+
if (fs66.existsSync(node9Dir)) {
|
|
27779
28275
|
console.error(
|
|
27780
|
-
|
|
28276
|
+
chalk36.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
27781
28277
|
);
|
|
27782
28278
|
} else {
|
|
27783
|
-
console.log(
|
|
28279
|
+
console.log(chalk36.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
27784
28280
|
}
|
|
27785
28281
|
} else {
|
|
27786
|
-
console.log(
|
|
28282
|
+
console.log(chalk36.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
27787
28283
|
}
|
|
27788
28284
|
} else {
|
|
27789
|
-
console.log(
|
|
28285
|
+
console.log(chalk36.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
27790
28286
|
}
|
|
27791
28287
|
} else {
|
|
27792
28288
|
console.log(
|
|
27793
|
-
|
|
28289
|
+
chalk36.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
27794
28290
|
);
|
|
27795
28291
|
}
|
|
27796
28292
|
if (teardownFailed) {
|
|
27797
|
-
console.error(
|
|
28293
|
+
console.error(chalk36.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
27798
28294
|
process.exit(1);
|
|
27799
28295
|
}
|
|
27800
|
-
console.log(
|
|
27801
|
-
console.log(
|
|
28296
|
+
console.log(chalk36.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g node9-ai"));
|
|
28297
|
+
console.log(chalk36.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
|
|
27802
28298
|
});
|
|
27803
28299
|
registerDoctorCommand(program, version);
|
|
27804
28300
|
program.command("explain").description(
|
|
@@ -27811,7 +28307,7 @@ program.command("explain").description(
|
|
|
27811
28307
|
try {
|
|
27812
28308
|
args = JSON.parse(trimmed);
|
|
27813
28309
|
} catch {
|
|
27814
|
-
console.error(
|
|
28310
|
+
console.error(chalk36.red(`
|
|
27815
28311
|
\u274C Invalid JSON: ${trimmed}
|
|
27816
28312
|
`));
|
|
27817
28313
|
process.exit(1);
|
|
@@ -27822,54 +28318,62 @@ program.command("explain").description(
|
|
|
27822
28318
|
}
|
|
27823
28319
|
const result = await explainPolicy(tool, args);
|
|
27824
28320
|
console.log("");
|
|
27825
|
-
console.log(
|
|
28321
|
+
console.log(chalk36.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
27826
28322
|
console.log("");
|
|
27827
|
-
console.log(` ${
|
|
28323
|
+
console.log(` ${chalk36.bold("Tool:")} ${chalk36.white(result.tool)}`);
|
|
27828
28324
|
if (argsRaw) {
|
|
27829
28325
|
const preview2 = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
27830
|
-
console.log(` ${
|
|
28326
|
+
console.log(` ${chalk36.bold("Input:")} ${chalk36.gray(preview2)}`);
|
|
27831
28327
|
}
|
|
27832
28328
|
console.log("");
|
|
27833
|
-
console.log(
|
|
28329
|
+
console.log(chalk36.bold("Config Sources (Waterfall):"));
|
|
27834
28330
|
for (const tier of result.waterfall) {
|
|
27835
|
-
const num3 =
|
|
28331
|
+
const num3 = chalk36.gray(` ${tier.tier}.`);
|
|
27836
28332
|
const label2 = tier.label.padEnd(16);
|
|
27837
28333
|
let statusStr;
|
|
27838
28334
|
if (tier.tier === 1) {
|
|
27839
|
-
statusStr =
|
|
28335
|
+
statusStr = chalk36.gray(tier.note ?? "");
|
|
27840
28336
|
} else if (tier.status === "active") {
|
|
27841
|
-
const loc = tier.path ?
|
|
27842
|
-
const note = tier.note ?
|
|
27843
|
-
statusStr =
|
|
28337
|
+
const loc = tier.path ? chalk36.gray(tier.path) : "";
|
|
28338
|
+
const note = tier.note ? chalk36.gray(`(${tier.note})`) : "";
|
|
28339
|
+
statusStr = chalk36.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
|
|
27844
28340
|
} else {
|
|
27845
|
-
statusStr =
|
|
28341
|
+
statusStr = chalk36.gray("\u25CB " + (tier.note ?? "not found"));
|
|
27846
28342
|
}
|
|
27847
|
-
console.log(`${num3} ${
|
|
28343
|
+
console.log(`${num3} ${chalk36.white(label2)} ${statusStr}`);
|
|
27848
28344
|
}
|
|
27849
28345
|
console.log("");
|
|
27850
|
-
console.log(
|
|
28346
|
+
console.log(chalk36.bold("Policy Evaluation:"));
|
|
27851
28347
|
for (const step of result.steps) {
|
|
27852
28348
|
const isFinal = step.isFinal;
|
|
27853
28349
|
let icon;
|
|
27854
|
-
if (step.outcome === "allow") icon =
|
|
27855
|
-
else if (step.outcome === "
|
|
27856
|
-
else if (step.outcome === "
|
|
27857
|
-
else icon =
|
|
28350
|
+
if (step.outcome === "allow") icon = chalk36.green(" \u2705");
|
|
28351
|
+
else if (step.outcome === "block") icon = chalk36.red(" \u{1F6D1}");
|
|
28352
|
+
else if (step.outcome === "review") icon = chalk36.red(" \u{1F534}");
|
|
28353
|
+
else if (step.outcome === "skip") icon = chalk36.gray(" \u2500 ");
|
|
28354
|
+
else icon = chalk36.gray(" \u25CB ");
|
|
27858
28355
|
const name = step.name.padEnd(18);
|
|
27859
|
-
const nameStr = isFinal ?
|
|
27860
|
-
const detail = isFinal ?
|
|
27861
|
-
const arrow = isFinal ?
|
|
28356
|
+
const nameStr = isFinal ? chalk36.white.bold(name) : chalk36.white(name);
|
|
28357
|
+
const detail = isFinal ? chalk36.white(step.detail) : chalk36.gray(step.detail);
|
|
28358
|
+
const arrow = isFinal ? chalk36.yellow(" \u2190 STOP") : "";
|
|
27862
28359
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
27863
28360
|
}
|
|
27864
28361
|
console.log("");
|
|
27865
28362
|
if (result.decision === "allow") {
|
|
27866
|
-
console.log(
|
|
28363
|
+
console.log(chalk36.green.bold(" Decision: \u2705 ALLOW") + chalk36.gray(" \u2014 no approval needed"));
|
|
28364
|
+
} else if (result.decision === "block") {
|
|
28365
|
+
console.log(
|
|
28366
|
+
chalk36.red.bold(" Decision: \u{1F6D1} BLOCK") + chalk36.gray(" \u2014 this action is blocked")
|
|
28367
|
+
);
|
|
28368
|
+
if (result.blockedByLabel) {
|
|
28369
|
+
console.log(chalk36.gray(` Reason: ${result.blockedByLabel}`));
|
|
28370
|
+
}
|
|
27867
28371
|
} else {
|
|
27868
28372
|
console.log(
|
|
27869
|
-
|
|
28373
|
+
chalk36.red.bold(" Decision: \u{1F534} REVIEW") + chalk36.gray(" \u2014 human approval required")
|
|
27870
28374
|
);
|
|
27871
28375
|
if (result.blockedByLabel) {
|
|
27872
|
-
console.log(
|
|
28376
|
+
console.log(chalk36.gray(` Reason: ${result.blockedByLabel}`));
|
|
27873
28377
|
}
|
|
27874
28378
|
}
|
|
27875
28379
|
console.log("");
|
|
@@ -27884,18 +28388,18 @@ program.command("tail").description("Stream live agent activity to the terminal"
|
|
|
27884
28388
|
try {
|
|
27885
28389
|
await startTail2(options);
|
|
27886
28390
|
} catch (err2) {
|
|
27887
|
-
console.error(
|
|
28391
|
+
console.error(chalk36.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
27888
28392
|
process.exit(1);
|
|
27889
28393
|
}
|
|
27890
28394
|
});
|
|
27891
28395
|
program.command("monitor").description("Live interactive dashboard \u2014 activity feed, approvals, security signals").action(async () => {
|
|
27892
28396
|
try {
|
|
27893
|
-
const dashboardPath =
|
|
28397
|
+
const dashboardPath = path63.join(__dirname, "dashboard.mjs");
|
|
27894
28398
|
const dynamicImport = new Function("id", "return import(id)");
|
|
27895
28399
|
const mod = await dynamicImport(`file://${dashboardPath}`);
|
|
27896
28400
|
await mod.startMonitor();
|
|
27897
28401
|
} catch (err2) {
|
|
27898
|
-
console.error(
|
|
28402
|
+
console.error(chalk36.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
27899
28403
|
process.exit(1);
|
|
27900
28404
|
}
|
|
27901
28405
|
});
|
|
@@ -27928,14 +28432,14 @@ Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
|
|
|
27928
28432
|
Run "node9 addto claude" to register it as the statusLine.`
|
|
27929
28433
|
).argument("[subcommand]", 'Optional: "debug on" / "debug off" to toggle stdin logging').argument("[state]", 'on|off \u2014 used with "debug" subcommand').action(async (subcommand, state) => {
|
|
27930
28434
|
if (subcommand === "debug") {
|
|
27931
|
-
const flagFile =
|
|
28435
|
+
const flagFile = path63.join(os56.homedir(), ".node9", "hud-debug");
|
|
27932
28436
|
if (state === "on") {
|
|
27933
|
-
|
|
27934
|
-
|
|
28437
|
+
fs66.mkdirSync(path63.dirname(flagFile), { recursive: true });
|
|
28438
|
+
fs66.writeFileSync(flagFile, "");
|
|
27935
28439
|
console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
|
|
27936
28440
|
console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
|
|
27937
28441
|
} else if (state === "off") {
|
|
27938
|
-
if (
|
|
28442
|
+
if (fs66.existsSync(flagFile)) fs66.unlinkSync(flagFile);
|
|
27939
28443
|
console.log("HUD debug logging disabled.");
|
|
27940
28444
|
} else {
|
|
27941
28445
|
console.error("Usage: node9 hud debug on|off");
|
|
@@ -27950,7 +28454,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
27950
28454
|
const ms = parseDuration(options.duration);
|
|
27951
28455
|
if (ms === null) {
|
|
27952
28456
|
console.error(
|
|
27953
|
-
|
|
28457
|
+
chalk36.red(`
|
|
27954
28458
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
27955
28459
|
`)
|
|
27956
28460
|
);
|
|
@@ -27958,20 +28462,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
27958
28462
|
}
|
|
27959
28463
|
pauseNode9(ms, options.duration);
|
|
27960
28464
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
27961
|
-
console.log(
|
|
28465
|
+
console.log(chalk36.yellow(`
|
|
27962
28466
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
27963
|
-
console.log(
|
|
27964
|
-
console.log(
|
|
28467
|
+
console.log(chalk36.gray(` All tool calls will be allowed without review.`));
|
|
28468
|
+
console.log(chalk36.gray(` Run "node9 resume" to re-enable early.
|
|
27965
28469
|
`));
|
|
27966
28470
|
});
|
|
27967
28471
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
27968
28472
|
const { paused } = checkPause();
|
|
27969
28473
|
if (!paused) {
|
|
27970
|
-
console.log(
|
|
28474
|
+
console.log(chalk36.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
27971
28475
|
return;
|
|
27972
28476
|
}
|
|
27973
28477
|
resumeNode9();
|
|
27974
|
-
console.log(
|
|
28478
|
+
console.log(chalk36.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
27975
28479
|
});
|
|
27976
28480
|
var HOOK_BASED_AGENTS = {
|
|
27977
28481
|
claude: "claude",
|
|
@@ -27987,15 +28491,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
27987
28491
|
if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
|
|
27988
28492
|
const target = HOOK_BASED_AGENTS[firstArg2];
|
|
27989
28493
|
console.error(
|
|
27990
|
-
|
|
28494
|
+
chalk36.yellow(`
|
|
27991
28495
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
27992
28496
|
);
|
|
27993
|
-
console.error(
|
|
28497
|
+
console.error(chalk36.white(`
|
|
27994
28498
|
"${target}" uses its own hook system. Use:`));
|
|
27995
28499
|
console.error(
|
|
27996
|
-
|
|
28500
|
+
chalk36.green(` node9 addto ${target} `) + chalk36.gray("# one-time setup")
|
|
27997
28501
|
);
|
|
27998
|
-
console.error(
|
|
28502
|
+
console.error(chalk36.green(` ${target} `) + chalk36.gray("# run normally"));
|
|
27999
28503
|
process.exit(1);
|
|
28000
28504
|
}
|
|
28001
28505
|
const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
|
|
@@ -28012,7 +28516,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
28012
28516
|
}
|
|
28013
28517
|
);
|
|
28014
28518
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
28015
|
-
console.error(
|
|
28519
|
+
console.error(chalk36.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
28016
28520
|
const daemonReady = await autoStartDaemonAndWait();
|
|
28017
28521
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
28018
28522
|
}
|
|
@@ -28025,12 +28529,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
28025
28529
|
}
|
|
28026
28530
|
if (!result.approved) {
|
|
28027
28531
|
console.error(
|
|
28028
|
-
|
|
28532
|
+
chalk36.red(`
|
|
28029
28533
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
28030
28534
|
);
|
|
28031
28535
|
process.exit(1);
|
|
28032
28536
|
}
|
|
28033
|
-
console.error(
|
|
28537
|
+
console.error(chalk36.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
28034
28538
|
await runProxy(fullCommand);
|
|
28035
28539
|
} else {
|
|
28036
28540
|
program.help();
|
|
@@ -28045,6 +28549,7 @@ registerAgentsCommand(program);
|
|
|
28045
28549
|
registerScanCommand(program);
|
|
28046
28550
|
registerPostureCommand(program);
|
|
28047
28551
|
registerEgressCommand(program);
|
|
28552
|
+
registerJailCommand(program);
|
|
28048
28553
|
registerSandboxCommand(program, version);
|
|
28049
28554
|
registerSessionsCommand(program);
|
|
28050
28555
|
registerSessionTaintCommand(program);
|
|
@@ -28056,9 +28561,9 @@ if (process.argv[2] !== "daemon") {
|
|
|
28056
28561
|
const isCheckHook = process.argv[2] === "check";
|
|
28057
28562
|
if (isCheckHook) {
|
|
28058
28563
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
28059
|
-
const logPath =
|
|
28564
|
+
const logPath = path63.join(os56.homedir(), ".node9", "hook-debug.log");
|
|
28060
28565
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
28061
|
-
|
|
28566
|
+
fs66.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
28062
28567
|
`);
|
|
28063
28568
|
}
|
|
28064
28569
|
process.exit(0);
|