@node9/proxy 1.39.0 → 1.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -0
- package/dist/cli.js +993 -461
- package/dist/cli.mjs +989 -457
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -206,8 +206,8 @@ function sanitizeConfig(raw) {
|
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
208
|
const lines = result.error.issues.map((issue) => {
|
|
209
|
-
const
|
|
210
|
-
return ` \u2022 ${
|
|
209
|
+
const path61 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
210
|
+
return ` \u2022 ${path61}: ${issue.message}`;
|
|
211
211
|
});
|
|
212
212
|
return {
|
|
213
213
|
sanitized,
|
|
@@ -1274,9 +1274,9 @@ function matchesPattern(text, patterns) {
|
|
|
1274
1274
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1275
1275
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1276
1276
|
}
|
|
1277
|
-
function getNestedValue(obj,
|
|
1277
|
+
function getNestedValue(obj, path61) {
|
|
1278
1278
|
if (!obj || typeof obj !== "object") return null;
|
|
1279
|
-
const segments =
|
|
1279
|
+
const segments = path61.split(".");
|
|
1280
1280
|
for (const seg of segments) {
|
|
1281
1281
|
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
1282
1282
|
}
|
|
@@ -17246,20 +17246,20 @@ function getModelContextLimit(model) {
|
|
|
17246
17246
|
return 2e5;
|
|
17247
17247
|
}
|
|
17248
17248
|
function readSessionUsage() {
|
|
17249
|
-
const projectsDir =
|
|
17250
|
-
if (!
|
|
17249
|
+
const projectsDir = import_path58.default.join(import_os52.default.homedir(), ".claude", "projects");
|
|
17250
|
+
if (!import_fs60.default.existsSync(projectsDir)) return null;
|
|
17251
17251
|
let latestFile = null;
|
|
17252
17252
|
let latestMtime = 0;
|
|
17253
17253
|
try {
|
|
17254
|
-
for (const dir of
|
|
17255
|
-
const dirPath =
|
|
17254
|
+
for (const dir of import_fs60.default.readdirSync(projectsDir)) {
|
|
17255
|
+
const dirPath = import_path58.default.join(projectsDir, dir);
|
|
17256
17256
|
try {
|
|
17257
|
-
if (!
|
|
17258
|
-
for (const file of
|
|
17257
|
+
if (!import_fs60.default.statSync(dirPath).isDirectory()) continue;
|
|
17258
|
+
for (const file of import_fs60.default.readdirSync(dirPath)) {
|
|
17259
17259
|
if (!file.endsWith(".jsonl") || file.startsWith("agent-")) continue;
|
|
17260
|
-
const filePath =
|
|
17260
|
+
const filePath = import_path58.default.join(dirPath, file);
|
|
17261
17261
|
try {
|
|
17262
|
-
const mtime =
|
|
17262
|
+
const mtime = import_fs60.default.statSync(filePath).mtimeMs;
|
|
17263
17263
|
if (mtime > latestMtime) {
|
|
17264
17264
|
latestMtime = mtime;
|
|
17265
17265
|
latestFile = filePath;
|
|
@@ -17274,7 +17274,7 @@ function readSessionUsage() {
|
|
|
17274
17274
|
}
|
|
17275
17275
|
if (!latestFile) return null;
|
|
17276
17276
|
try {
|
|
17277
|
-
const lines =
|
|
17277
|
+
const lines = import_fs60.default.readFileSync(latestFile, "utf-8").split("\n");
|
|
17278
17278
|
let lastModel = "";
|
|
17279
17279
|
let lastInput = 0;
|
|
17280
17280
|
let lastOutput = 0;
|
|
@@ -17299,10 +17299,10 @@ function readSessionUsage() {
|
|
|
17299
17299
|
}
|
|
17300
17300
|
}
|
|
17301
17301
|
function formatContextStat(stat) {
|
|
17302
|
-
const pctColor = stat.fillPct >= 80 ?
|
|
17302
|
+
const pctColor = stat.fillPct >= 80 ? import_chalk34.default.red : stat.fillPct >= 50 ? import_chalk34.default.yellow : import_chalk34.default.cyan;
|
|
17303
17303
|
const k = (n) => `${Math.round(n / 1e3)}k`;
|
|
17304
17304
|
const modelShort = stat.model.replace(/@.*$/, "").replace(/-\d{8}$/, "").replace(/^claude-/, "");
|
|
17305
|
-
return
|
|
17305
|
+
return import_chalk34.default.dim("ctx: ") + pctColor(`${stat.fillPct}%`) + import_chalk34.default.dim(
|
|
17306
17306
|
` (${k(stat.inputTokens)}/${k(getModelContextLimit(stat.model))} out ${k(stat.outputTokens)} \xB7 ${modelShort})`
|
|
17307
17307
|
);
|
|
17308
17308
|
}
|
|
@@ -17325,32 +17325,32 @@ function agentLabel(agent, mcpServer, sessionId) {
|
|
|
17325
17325
|
const tag = sessionTag(sessionId);
|
|
17326
17326
|
const tagSuffix = tag ? `\xB7${tag}` : "";
|
|
17327
17327
|
if (!agent || agent === "Terminal") {
|
|
17328
|
-
return mcpServer ?
|
|
17328
|
+
return mcpServer ? import_chalk34.default.dim(`[\u2192 ${mcpServer}] `) : "";
|
|
17329
17329
|
}
|
|
17330
17330
|
const short = agent === "Claude Code" ? "Claude" : agent === "Gemini CLI" ? "Gemini" : agent === "Unknown Agent" ? "" : agent.split(" ")[0];
|
|
17331
|
-
if (!short) return mcpServer ?
|
|
17332
|
-
return mcpServer ?
|
|
17331
|
+
if (!short) return mcpServer ? import_chalk34.default.dim(`[\u2192 ${mcpServer}] `) : "";
|
|
17332
|
+
return mcpServer ? import_chalk34.default.dim(`[${short}${tagSuffix} \u2192 ${mcpServer}] `) : import_chalk34.default.dim(`[${short}${tagSuffix}] `);
|
|
17333
17333
|
}
|
|
17334
17334
|
function formatBase(activity) {
|
|
17335
17335
|
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
17336
17336
|
const icon = getIcon(activity.tool);
|
|
17337
17337
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
17338
|
-
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(
|
|
17338
|
+
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(import_os52.default.homedir(), "~");
|
|
17339
17339
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
17340
|
-
return `${
|
|
17340
|
+
return `${import_chalk34.default.gray(time)} ${icon} ${agentLabel(activity.agent, activity.mcpServer, activity.sessionId)}${import_chalk34.default.white.bold(toolName)} ${import_chalk34.default.dim(argsPreview)}`;
|
|
17341
17341
|
}
|
|
17342
17342
|
function renderResult(activity, result) {
|
|
17343
17343
|
const base = formatBase(activity);
|
|
17344
17344
|
let status;
|
|
17345
17345
|
if (result.status === "allow") {
|
|
17346
|
-
status =
|
|
17346
|
+
status = import_chalk34.default.green("\u2713 ALLOW");
|
|
17347
17347
|
} else if (result.status === "dlp") {
|
|
17348
|
-
status =
|
|
17348
|
+
status = import_chalk34.default.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
17349
17349
|
} else {
|
|
17350
|
-
status =
|
|
17350
|
+
status = import_chalk34.default.red("\u2717 BLOCK");
|
|
17351
17351
|
}
|
|
17352
17352
|
const cost = result.costEstimate ?? activity.costEstimate;
|
|
17353
|
-
const costSuffix = cost == null ? "" :
|
|
17353
|
+
const costSuffix = cost == null ? "" : import_chalk34.default.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
|
|
17354
17354
|
if (process.stdout.isTTY) {
|
|
17355
17355
|
if (pendingShownForId === activity.id && pendingWrappedLines > 1) {
|
|
17356
17356
|
import_readline6.default.moveCursor(process.stdout, 0, -(pendingWrappedLines - 1));
|
|
@@ -17367,19 +17367,19 @@ function renderResult(activity, result) {
|
|
|
17367
17367
|
}
|
|
17368
17368
|
function renderPending(activity) {
|
|
17369
17369
|
if (!process.stdout.isTTY) return;
|
|
17370
|
-
const line = `${formatBase(activity)} ${
|
|
17370
|
+
const line = `${formatBase(activity)} ${import_chalk34.default.yellow("\u25CF \u2026")}`;
|
|
17371
17371
|
pendingShownForId = activity.id;
|
|
17372
17372
|
pendingWrappedLines = wrappedLineCount(line);
|
|
17373
17373
|
process.stdout.write(`${line}\r`);
|
|
17374
17374
|
}
|
|
17375
17375
|
async function ensureDaemon() {
|
|
17376
17376
|
let pidPort = null;
|
|
17377
|
-
if (
|
|
17377
|
+
if (import_fs60.default.existsSync(PID_FILE)) {
|
|
17378
17378
|
try {
|
|
17379
|
-
const { port } = JSON.parse(
|
|
17379
|
+
const { port } = JSON.parse(import_fs60.default.readFileSync(PID_FILE, "utf-8"));
|
|
17380
17380
|
pidPort = port;
|
|
17381
17381
|
} catch {
|
|
17382
|
-
console.error(
|
|
17382
|
+
console.error(import_chalk34.default.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
17383
17383
|
}
|
|
17384
17384
|
}
|
|
17385
17385
|
const checkPort = pidPort ?? DAEMON_PORT;
|
|
@@ -17390,8 +17390,8 @@ async function ensureDaemon() {
|
|
|
17390
17390
|
if (res.ok) return checkPort;
|
|
17391
17391
|
} catch {
|
|
17392
17392
|
}
|
|
17393
|
-
console.log(
|
|
17394
|
-
const child = (0,
|
|
17393
|
+
console.log(import_chalk34.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
17394
|
+
const child = (0, import_child_process14.spawn)(process.execPath, [process.argv[1], "daemon"], {
|
|
17395
17395
|
detached: true,
|
|
17396
17396
|
stdio: "ignore",
|
|
17397
17397
|
env: { ...process.env, NODE9_AUTO_STARTED: "1" }
|
|
@@ -17407,7 +17407,7 @@ async function ensureDaemon() {
|
|
|
17407
17407
|
} catch {
|
|
17408
17408
|
}
|
|
17409
17409
|
}
|
|
17410
|
-
console.error(
|
|
17410
|
+
console.error(import_chalk34.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
17411
17411
|
process.exit(1);
|
|
17412
17412
|
}
|
|
17413
17413
|
function postDecisionHttp(id, decision, authToken, port, opts) {
|
|
@@ -17476,7 +17476,7 @@ function buildCardLines(req, localCount = 0) {
|
|
|
17476
17476
|
const severityIcon = isBlock ? `${RED}\u{1F6D1}` : `${YELLOW}\u26A0 `;
|
|
17477
17477
|
const rawDesc = req.riskMetadata?.ruleDescription ?? "";
|
|
17478
17478
|
const description = rawDesc ? cleanReason(rawDesc) : "";
|
|
17479
|
-
const agentSuffix = req.agent && req.agent !== "Terminal" ? ` ${RESET2}${
|
|
17479
|
+
const agentSuffix = req.agent && req.agent !== "Terminal" ? ` ${RESET2}${import_chalk34.default.dim(`(${req.agent})`)}` : "";
|
|
17480
17480
|
const lines = [
|
|
17481
17481
|
``,
|
|
17482
17482
|
`${BOLD2}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET2}`,
|
|
@@ -17532,9 +17532,9 @@ function buildRecoveryCardLines(req) {
|
|
|
17532
17532
|
];
|
|
17533
17533
|
}
|
|
17534
17534
|
function readApproversFromDisk() {
|
|
17535
|
-
const configPath2 =
|
|
17535
|
+
const configPath2 = import_path58.default.join(import_os52.default.homedir(), ".node9", "config.json");
|
|
17536
17536
|
try {
|
|
17537
|
-
const raw = JSON.parse(
|
|
17537
|
+
const raw = JSON.parse(import_fs60.default.readFileSync(configPath2, "utf-8"));
|
|
17538
17538
|
const settings = raw.settings ?? {};
|
|
17539
17539
|
return settings.approvers ?? {};
|
|
17540
17540
|
} catch {
|
|
@@ -17545,20 +17545,20 @@ function approverStatusLine() {
|
|
|
17545
17545
|
const a = readApproversFromDisk();
|
|
17546
17546
|
const fmt = (label2, key) => {
|
|
17547
17547
|
const on = a[key] !== false;
|
|
17548
|
-
return `[${key[0]}]${label2.slice(1)} ${on ?
|
|
17548
|
+
return `[${key[0]}]${label2.slice(1)} ${on ? import_chalk34.default.green("\u2713") : import_chalk34.default.dim("\u2717")}`;
|
|
17549
17549
|
};
|
|
17550
17550
|
return `${fmt("native", "native")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
|
|
17551
17551
|
}
|
|
17552
17552
|
function toggleApprover(channel) {
|
|
17553
|
-
const configPath2 =
|
|
17553
|
+
const configPath2 = import_path58.default.join(import_os52.default.homedir(), ".node9", "config.json");
|
|
17554
17554
|
try {
|
|
17555
|
-
const raw = JSON.parse(
|
|
17555
|
+
const raw = JSON.parse(import_fs60.default.readFileSync(configPath2, "utf-8"));
|
|
17556
17556
|
const settings = raw.settings ?? {};
|
|
17557
17557
|
const approvers = settings.approvers ?? {};
|
|
17558
17558
|
approvers[channel] = approvers[channel] === false;
|
|
17559
17559
|
settings.approvers = approvers;
|
|
17560
17560
|
raw.settings = settings;
|
|
17561
|
-
|
|
17561
|
+
import_fs60.default.writeFileSync(configPath2, JSON.stringify(raw, null, 2) + "\n");
|
|
17562
17562
|
} catch (err2) {
|
|
17563
17563
|
process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
|
|
17564
17564
|
`);
|
|
@@ -17590,7 +17590,7 @@ async function startTail(options = {}) {
|
|
|
17590
17590
|
req2.end();
|
|
17591
17591
|
});
|
|
17592
17592
|
if (result.ok) {
|
|
17593
|
-
console.log(
|
|
17593
|
+
console.log(import_chalk34.default.green("\u2713 Flight Recorder buffer cleared."));
|
|
17594
17594
|
} else if (result.code === "ECONNREFUSED") {
|
|
17595
17595
|
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
17596
17596
|
} else if (result.code === "ETIMEDOUT") {
|
|
@@ -17636,7 +17636,7 @@ async function startTail(options = {}) {
|
|
|
17636
17636
|
const channel = name === "n" ? "native" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
|
|
17637
17637
|
if (channel) {
|
|
17638
17638
|
toggleApprover(channel);
|
|
17639
|
-
console.log(
|
|
17639
|
+
console.log(import_chalk34.default.dim(` Approvers: ${approverStatusLine()}`));
|
|
17640
17640
|
}
|
|
17641
17641
|
};
|
|
17642
17642
|
process.stdin.on("keypress", idleKeypressHandler);
|
|
@@ -17702,7 +17702,7 @@ async function startTail(options = {}) {
|
|
|
17702
17702
|
localAllowCounts.get(req2.toolName) ?? 0
|
|
17703
17703
|
)
|
|
17704
17704
|
);
|
|
17705
|
-
const decisionStamp = action === "always-allow" ?
|
|
17705
|
+
const decisionStamp = action === "always-allow" ? import_chalk34.default.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? import_chalk34.default.cyan("\u23F1 TRUST 30m") : action === "allow" ? import_chalk34.default.green("\u2713 ALLOWED") : action === "redirect" ? import_chalk34.default.yellow("\u21A9 REDIRECT AI") : import_chalk34.default.red("\u2717 DENIED");
|
|
17706
17706
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
|
|
17707
17707
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
17708
17708
|
process.stdout.write(SHOW_CURSOR);
|
|
@@ -17730,8 +17730,8 @@ async function startTail(options = {}) {
|
|
|
17730
17730
|
}
|
|
17731
17731
|
postDecisionHttp(req2.id, httpDecision, authToken, port, httpOpts).catch((err2) => {
|
|
17732
17732
|
try {
|
|
17733
|
-
|
|
17734
|
-
|
|
17733
|
+
import_fs60.default.appendFileSync(
|
|
17734
|
+
import_path58.default.join(import_os52.default.homedir(), ".node9", "hook-debug.log"),
|
|
17735
17735
|
`[tail] POST /decision failed: ${String(err2)}
|
|
17736
17736
|
`
|
|
17737
17737
|
);
|
|
@@ -17753,7 +17753,7 @@ async function startTail(options = {}) {
|
|
|
17753
17753
|
);
|
|
17754
17754
|
const stampedLines = buildCardLines(req2, priorCount);
|
|
17755
17755
|
if (externalDecision) {
|
|
17756
|
-
const source = externalDecision === "allow" ?
|
|
17756
|
+
const source = externalDecision === "allow" ? import_chalk34.default.green("\u2713 ALLOWED") : import_chalk34.default.red("\u2717 DENIED");
|
|
17757
17757
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
|
|
17758
17758
|
}
|
|
17759
17759
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
@@ -17795,31 +17795,31 @@ async function startTail(options = {}) {
|
|
|
17795
17795
|
};
|
|
17796
17796
|
process.stdin.on("keypress", onKeypress);
|
|
17797
17797
|
}
|
|
17798
|
-
const auditLog =
|
|
17798
|
+
const auditLog = import_path58.default.join(import_os52.default.homedir(), ".node9", "audit.log");
|
|
17799
17799
|
try {
|
|
17800
|
-
const unackedDlp =
|
|
17800
|
+
const unackedDlp = import_fs60.default.readFileSync(auditLog, "utf-8").split("\n").filter((l) => l.includes('"response-dlp"')).length;
|
|
17801
17801
|
if (unackedDlp > 0) {
|
|
17802
17802
|
console.log("");
|
|
17803
17803
|
console.log(
|
|
17804
|
-
|
|
17804
|
+
import_chalk34.default.bgRed.white.bold(
|
|
17805
17805
|
` \u26A0\uFE0F DLP ALERT: ${unackedDlp} secret${unackedDlp !== 1 ? "s" : ""} found in Claude response text \u2014 run: node9 dlp `
|
|
17806
17806
|
)
|
|
17807
17807
|
);
|
|
17808
17808
|
}
|
|
17809
17809
|
} catch {
|
|
17810
17810
|
}
|
|
17811
|
-
console.log(
|
|
17811
|
+
console.log(import_chalk34.default.cyan.bold(`
|
|
17812
17812
|
\u{1F6F0}\uFE0F Node9 tail`));
|
|
17813
17813
|
if (canApprove) {
|
|
17814
|
-
console.log(
|
|
17815
|
-
console.log(
|
|
17814
|
+
console.log(import_chalk34.default.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
|
|
17815
|
+
console.log(import_chalk34.default.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
|
|
17816
17816
|
}
|
|
17817
17817
|
const ctxStat = readSessionUsage();
|
|
17818
17818
|
if (ctxStat) console.log(" " + formatContextStat(ctxStat));
|
|
17819
17819
|
if (options.history) {
|
|
17820
|
-
console.log(
|
|
17820
|
+
console.log(import_chalk34.default.dim("Showing history + live events.\n"));
|
|
17821
17821
|
} else {
|
|
17822
|
-
console.log(
|
|
17822
|
+
console.log(import_chalk34.default.dim("Showing live events only. Use --history to include past.\n"));
|
|
17823
17823
|
}
|
|
17824
17824
|
process.on("SIGINT", () => {
|
|
17825
17825
|
exitIdleMode();
|
|
@@ -17829,7 +17829,7 @@ async function startTail(options = {}) {
|
|
|
17829
17829
|
import_readline6.default.clearLine(process.stdout, 0);
|
|
17830
17830
|
import_readline6.default.cursorTo(process.stdout, 0);
|
|
17831
17831
|
}
|
|
17832
|
-
console.log(
|
|
17832
|
+
console.log(import_chalk34.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
17833
17833
|
process.exit(0);
|
|
17834
17834
|
});
|
|
17835
17835
|
const STALL_THRESHOLD_MS = 6e4;
|
|
@@ -17837,11 +17837,11 @@ async function startTail(options = {}) {
|
|
|
17837
17837
|
if (stallWarned) return;
|
|
17838
17838
|
if (Date.now() - lastActivityFromDaemon < STALL_THRESHOLD_MS) return;
|
|
17839
17839
|
try {
|
|
17840
|
-
const auditMtime =
|
|
17840
|
+
const auditMtime = import_fs60.default.statSync(auditLog).mtimeMs;
|
|
17841
17841
|
if (Date.now() - auditMtime >= STALL_THRESHOLD_MS) return;
|
|
17842
17842
|
console.log("");
|
|
17843
17843
|
console.log(
|
|
17844
|
-
|
|
17844
|
+
import_chalk34.default.yellow(
|
|
17845
17845
|
"\u26A0\uFE0F Tail appears stalled \u2014 hooks are firing but no events are arriving. Try: node9 daemon restart"
|
|
17846
17846
|
)
|
|
17847
17847
|
);
|
|
@@ -17858,7 +17858,7 @@ async function startTail(options = {}) {
|
|
|
17858
17858
|
},
|
|
17859
17859
|
(res) => {
|
|
17860
17860
|
if (res.statusCode !== 200) {
|
|
17861
|
-
console.error(
|
|
17861
|
+
console.error(import_chalk34.default.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
17862
17862
|
process.exit(1);
|
|
17863
17863
|
}
|
|
17864
17864
|
if (canApprove) enterIdleMode();
|
|
@@ -17889,7 +17889,7 @@ async function startTail(options = {}) {
|
|
|
17889
17889
|
import_readline6.default.clearLine(process.stdout, 0);
|
|
17890
17890
|
import_readline6.default.cursorTo(process.stdout, 0);
|
|
17891
17891
|
}
|
|
17892
|
-
console.log(
|
|
17892
|
+
console.log(import_chalk34.default.red("\n\u274C Daemon disconnected."));
|
|
17893
17893
|
process.exit(1);
|
|
17894
17894
|
});
|
|
17895
17895
|
}
|
|
@@ -17902,7 +17902,7 @@ async function startTail(options = {}) {
|
|
|
17902
17902
|
const parsed = JSON.parse(rawData);
|
|
17903
17903
|
const msg = parsed.message ?? "Flight recorder is down \u2014 run: node9 daemon restart";
|
|
17904
17904
|
console.log("");
|
|
17905
|
-
console.log(
|
|
17905
|
+
console.log(import_chalk34.default.bgRed.white.bold(` \u26A0\uFE0F ${msg} `));
|
|
17906
17906
|
} catch {
|
|
17907
17907
|
}
|
|
17908
17908
|
return;
|
|
@@ -17987,9 +17987,9 @@ async function startTail(options = {}) {
|
|
|
17987
17987
|
const rawSummary = data.argsSummary ?? data.tool;
|
|
17988
17988
|
const summary = shortenPathSummary(rawSummary);
|
|
17989
17989
|
const fileCount = data.fileCount ?? 0;
|
|
17990
|
-
const files = fileCount > 0 ?
|
|
17990
|
+
const files = fileCount > 0 ? import_chalk34.default.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
|
|
17991
17991
|
process.stdout.write(
|
|
17992
|
-
`${
|
|
17992
|
+
`${import_chalk34.default.dim(time)} ${import_chalk34.default.cyan("\u{1F4F8} snapshot")} ${import_chalk34.default.dim(hash)} ${summary}${files}
|
|
17993
17993
|
`
|
|
17994
17994
|
);
|
|
17995
17995
|
return;
|
|
@@ -18006,36 +18006,36 @@ async function startTail(options = {}) {
|
|
|
18006
18006
|
if (event === "execution-result") {
|
|
18007
18007
|
const exec = data;
|
|
18008
18008
|
const time = new Date(Date.now()).toLocaleTimeString([], { hour12: false });
|
|
18009
|
-
const arrow = exec.isError ?
|
|
18009
|
+
const arrow = exec.isError ? import_chalk34.default.red(" \u21B3 \u2717") : import_chalk34.default.green(" \u21B3 \u2713");
|
|
18010
18010
|
const label2 = agentLabel(exec.agent, exec.mcpServer);
|
|
18011
18011
|
const tool = (exec.tool ?? "").slice(0, 16);
|
|
18012
|
-
const duration = typeof exec.durationMs === "number" ?
|
|
18012
|
+
const duration = typeof exec.durationMs === "number" ? import_chalk34.default.dim(` (${exec.durationMs}ms)`) : "";
|
|
18013
18013
|
console.log(
|
|
18014
|
-
`${
|
|
18014
|
+
`${import_chalk34.default.gray(time)} ${arrow} ${label2}${import_chalk34.default.dim(tool)}${import_chalk34.default.dim(" completed")}${duration}`
|
|
18015
18015
|
);
|
|
18016
18016
|
}
|
|
18017
18017
|
}
|
|
18018
18018
|
req.on("error", (err2) => {
|
|
18019
18019
|
const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
|
|
18020
|
-
console.error(
|
|
18020
|
+
console.error(import_chalk34.default.red(`
|
|
18021
18021
|
\u274C ${msg}`));
|
|
18022
18022
|
process.exit(1);
|
|
18023
18023
|
});
|
|
18024
18024
|
}
|
|
18025
|
-
var import_http3,
|
|
18025
|
+
var import_http3, import_chalk34, import_fs60, import_os52, import_path58, import_readline6, import_child_process14, PID_FILE, ICONS, MODEL_CONTEXT_LIMITS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, pendingShownForId, pendingWrappedLines, DIVIDER;
|
|
18026
18026
|
var init_tail = __esm({
|
|
18027
18027
|
"src/tui/tail.ts"() {
|
|
18028
18028
|
"use strict";
|
|
18029
18029
|
import_http3 = __toESM(require("http"));
|
|
18030
|
-
|
|
18031
|
-
|
|
18032
|
-
|
|
18033
|
-
|
|
18030
|
+
import_chalk34 = __toESM(require("chalk"));
|
|
18031
|
+
import_fs60 = __toESM(require("fs"));
|
|
18032
|
+
import_os52 = __toESM(require("os"));
|
|
18033
|
+
import_path58 = __toESM(require("path"));
|
|
18034
18034
|
import_readline6 = __toESM(require("readline"));
|
|
18035
|
-
|
|
18035
|
+
import_child_process14 = require("child_process");
|
|
18036
18036
|
init_daemon2();
|
|
18037
18037
|
init_daemon();
|
|
18038
|
-
PID_FILE =
|
|
18038
|
+
PID_FILE = import_path58.default.join(import_os52.default.homedir(), ".node9", "daemon.pid");
|
|
18039
18039
|
ICONS = {
|
|
18040
18040
|
bash: "\u{1F4BB}",
|
|
18041
18041
|
shell: "\u{1F4BB}",
|
|
@@ -18157,9 +18157,9 @@ function formatTimeLeft(resetsAt) {
|
|
|
18157
18157
|
return ` (${m}m left)`;
|
|
18158
18158
|
}
|
|
18159
18159
|
function safeReadJson(filePath) {
|
|
18160
|
-
if (!
|
|
18160
|
+
if (!import_fs61.default.existsSync(filePath)) return null;
|
|
18161
18161
|
try {
|
|
18162
|
-
return JSON.parse(
|
|
18162
|
+
return JSON.parse(import_fs61.default.readFileSync(filePath, "utf-8"));
|
|
18163
18163
|
} catch {
|
|
18164
18164
|
return null;
|
|
18165
18165
|
}
|
|
@@ -18180,12 +18180,12 @@ function countHooksInFile(filePath) {
|
|
|
18180
18180
|
return Object.keys(cfg.hooks).length;
|
|
18181
18181
|
}
|
|
18182
18182
|
function countRulesInDir(rulesDir) {
|
|
18183
|
-
if (!
|
|
18183
|
+
if (!import_fs61.default.existsSync(rulesDir)) return 0;
|
|
18184
18184
|
let count = 0;
|
|
18185
18185
|
try {
|
|
18186
|
-
for (const entry of
|
|
18186
|
+
for (const entry of import_fs61.default.readdirSync(rulesDir, { withFileTypes: true })) {
|
|
18187
18187
|
if (entry.isDirectory()) {
|
|
18188
|
-
count += countRulesInDir(
|
|
18188
|
+
count += countRulesInDir(import_path59.default.join(rulesDir, entry.name));
|
|
18189
18189
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
18190
18190
|
count++;
|
|
18191
18191
|
}
|
|
@@ -18196,46 +18196,46 @@ function countRulesInDir(rulesDir) {
|
|
|
18196
18196
|
}
|
|
18197
18197
|
function isSamePath(a, b) {
|
|
18198
18198
|
try {
|
|
18199
|
-
return
|
|
18199
|
+
return import_path59.default.resolve(a) === import_path59.default.resolve(b);
|
|
18200
18200
|
} catch {
|
|
18201
18201
|
return false;
|
|
18202
18202
|
}
|
|
18203
18203
|
}
|
|
18204
18204
|
function countConfigs(cwd) {
|
|
18205
|
-
const homeDir2 =
|
|
18206
|
-
const claudeDir =
|
|
18205
|
+
const homeDir2 = import_os53.default.homedir();
|
|
18206
|
+
const claudeDir = import_path59.default.join(homeDir2, ".claude");
|
|
18207
18207
|
let claudeMdCount = 0;
|
|
18208
18208
|
let rulesCount = 0;
|
|
18209
18209
|
let hooksCount = 0;
|
|
18210
18210
|
const userMcpServers = /* @__PURE__ */ new Set();
|
|
18211
18211
|
const projectMcpServers = /* @__PURE__ */ new Set();
|
|
18212
|
-
if (
|
|
18213
|
-
rulesCount += countRulesInDir(
|
|
18214
|
-
const userSettings =
|
|
18212
|
+
if (import_fs61.default.existsSync(import_path59.default.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
18213
|
+
rulesCount += countRulesInDir(import_path59.default.join(claudeDir, "rules"));
|
|
18214
|
+
const userSettings = import_path59.default.join(claudeDir, "settings.json");
|
|
18215
18215
|
for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
|
|
18216
18216
|
hooksCount += countHooksInFile(userSettings);
|
|
18217
|
-
const userClaudeJson =
|
|
18217
|
+
const userClaudeJson = import_path59.default.join(homeDir2, ".claude.json");
|
|
18218
18218
|
for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
|
|
18219
18219
|
for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
|
|
18220
18220
|
userMcpServers.delete(name);
|
|
18221
18221
|
}
|
|
18222
18222
|
if (cwd) {
|
|
18223
|
-
if (
|
|
18224
|
-
if (
|
|
18225
|
-
const projectClaudeDir =
|
|
18223
|
+
if (import_fs61.default.existsSync(import_path59.default.join(cwd, "CLAUDE.md"))) claudeMdCount++;
|
|
18224
|
+
if (import_fs61.default.existsSync(import_path59.default.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
|
|
18225
|
+
const projectClaudeDir = import_path59.default.join(cwd, ".claude");
|
|
18226
18226
|
const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
|
|
18227
18227
|
if (!overlapsUserScope) {
|
|
18228
|
-
if (
|
|
18229
|
-
rulesCount += countRulesInDir(
|
|
18230
|
-
const projSettings =
|
|
18228
|
+
if (import_fs61.default.existsSync(import_path59.default.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
18229
|
+
rulesCount += countRulesInDir(import_path59.default.join(projectClaudeDir, "rules"));
|
|
18230
|
+
const projSettings = import_path59.default.join(projectClaudeDir, "settings.json");
|
|
18231
18231
|
for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
|
|
18232
18232
|
hooksCount += countHooksInFile(projSettings);
|
|
18233
18233
|
}
|
|
18234
|
-
if (
|
|
18235
|
-
const localSettings =
|
|
18234
|
+
if (import_fs61.default.existsSync(import_path59.default.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
|
|
18235
|
+
const localSettings = import_path59.default.join(projectClaudeDir, "settings.local.json");
|
|
18236
18236
|
for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
|
|
18237
18237
|
hooksCount += countHooksInFile(localSettings);
|
|
18238
|
-
const mcpJsonServers = getMcpServerNames(
|
|
18238
|
+
const mcpJsonServers = getMcpServerNames(import_path59.default.join(cwd, ".mcp.json"));
|
|
18239
18239
|
const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
|
|
18240
18240
|
for (const name of disabledMcpJson) mcpJsonServers.delete(name);
|
|
18241
18241
|
for (const name of mcpJsonServers) projectMcpServers.add(name);
|
|
@@ -18268,12 +18268,12 @@ function readActiveShieldsHud() {
|
|
|
18268
18268
|
return shieldsCache.value;
|
|
18269
18269
|
}
|
|
18270
18270
|
try {
|
|
18271
|
-
const shieldsPath =
|
|
18272
|
-
if (!
|
|
18271
|
+
const shieldsPath = import_path59.default.join(import_os53.default.homedir(), ".node9", "shields.json");
|
|
18272
|
+
if (!import_fs61.default.existsSync(shieldsPath)) {
|
|
18273
18273
|
shieldsCache = { value: [], ts: now };
|
|
18274
18274
|
return [];
|
|
18275
18275
|
}
|
|
18276
|
-
const parsed = JSON.parse(
|
|
18276
|
+
const parsed = JSON.parse(import_fs61.default.readFileSync(shieldsPath, "utf-8"));
|
|
18277
18277
|
if (!Array.isArray(parsed.active)) {
|
|
18278
18278
|
shieldsCache = { value: [], ts: now };
|
|
18279
18279
|
return [];
|
|
@@ -18375,17 +18375,17 @@ function renderContextLine(stdin) {
|
|
|
18375
18375
|
async function main() {
|
|
18376
18376
|
try {
|
|
18377
18377
|
const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
|
|
18378
|
-
if (
|
|
18378
|
+
if (import_fs61.default.existsSync(import_path59.default.join(import_os53.default.homedir(), ".node9", "hud-debug"))) {
|
|
18379
18379
|
try {
|
|
18380
|
-
const logPath =
|
|
18380
|
+
const logPath = import_path59.default.join(import_os53.default.homedir(), ".node9", "hud-debug.log");
|
|
18381
18381
|
const MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
18382
18382
|
let size = 0;
|
|
18383
18383
|
try {
|
|
18384
|
-
size =
|
|
18384
|
+
size = import_fs61.default.statSync(logPath).size;
|
|
18385
18385
|
} catch {
|
|
18386
18386
|
}
|
|
18387
18387
|
if (size < MAX_LOG_SIZE) {
|
|
18388
|
-
|
|
18388
|
+
import_fs61.default.appendFileSync(
|
|
18389
18389
|
logPath,
|
|
18390
18390
|
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
|
|
18391
18391
|
);
|
|
@@ -18406,11 +18406,11 @@ async function main() {
|
|
|
18406
18406
|
try {
|
|
18407
18407
|
const cwd = stdin.cwd ?? process.cwd();
|
|
18408
18408
|
for (const configPath2 of [
|
|
18409
|
-
|
|
18410
|
-
|
|
18409
|
+
import_path59.default.join(cwd, "node9.config.json"),
|
|
18410
|
+
import_path59.default.join(import_os53.default.homedir(), ".node9", "config.json")
|
|
18411
18411
|
]) {
|
|
18412
|
-
if (!
|
|
18413
|
-
const cfg = JSON.parse(
|
|
18412
|
+
if (!import_fs61.default.existsSync(configPath2)) continue;
|
|
18413
|
+
const cfg = JSON.parse(import_fs61.default.readFileSync(configPath2, "utf-8"));
|
|
18414
18414
|
const hud = cfg.settings?.hud;
|
|
18415
18415
|
if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
|
|
18416
18416
|
}
|
|
@@ -18428,13 +18428,13 @@ async function main() {
|
|
|
18428
18428
|
renderOffline();
|
|
18429
18429
|
}
|
|
18430
18430
|
}
|
|
18431
|
-
var
|
|
18431
|
+
var import_fs61, import_path59, import_os53, import_http4, RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH, shieldsCache, SHIELDS_CACHE_TTL_MS;
|
|
18432
18432
|
var init_hud = __esm({
|
|
18433
18433
|
"src/cli/hud.ts"() {
|
|
18434
18434
|
"use strict";
|
|
18435
|
-
|
|
18436
|
-
|
|
18437
|
-
|
|
18435
|
+
import_fs61 = __toESM(require("fs"));
|
|
18436
|
+
import_path59 = __toESM(require("path"));
|
|
18437
|
+
import_os53 = __toESM(require("os"));
|
|
18438
18438
|
import_http4 = __toESM(require("http"));
|
|
18439
18439
|
init_daemon();
|
|
18440
18440
|
RESET3 = "\x1B[0m";
|
|
@@ -18460,11 +18460,11 @@ var import_commander = require("commander");
|
|
|
18460
18460
|
init_core();
|
|
18461
18461
|
init_setup();
|
|
18462
18462
|
init_daemon2();
|
|
18463
|
-
var
|
|
18464
|
-
var
|
|
18465
|
-
var
|
|
18466
|
-
var
|
|
18467
|
-
var
|
|
18463
|
+
var import_chalk35 = __toESM(require("chalk"));
|
|
18464
|
+
var import_fs62 = __toESM(require("fs"));
|
|
18465
|
+
var import_path60 = __toESM(require("path"));
|
|
18466
|
+
var import_os54 = __toESM(require("os"));
|
|
18467
|
+
var import_child_process15 = require("child_process");
|
|
18468
18468
|
var import_prompts2 = require("@inquirer/prompts");
|
|
18469
18469
|
|
|
18470
18470
|
// src/utils/duration.ts
|
|
@@ -24212,7 +24212,133 @@ function checkSecrets(ctx) {
|
|
|
24212
24212
|
}
|
|
24213
24213
|
|
|
24214
24214
|
// src/posture/egress.ts
|
|
24215
|
+
var import_fs47 = __toESM(require("fs"));
|
|
24215
24216
|
init_config();
|
|
24217
|
+
|
|
24218
|
+
// src/sandbox/templates.ts
|
|
24219
|
+
var AGENT_NPM_PACKAGE = {
|
|
24220
|
+
claude: "@anthropic-ai/claude-code",
|
|
24221
|
+
codex: "@openai/codex"
|
|
24222
|
+
};
|
|
24223
|
+
function pinnedNode9Version(hostVersion) {
|
|
24224
|
+
return hostVersion && /^\d+\.\d+\.\d+$/.test(hostVersion) ? hostVersion : "latest";
|
|
24225
|
+
}
|
|
24226
|
+
var AGENT_BIN = {
|
|
24227
|
+
claude: "claude",
|
|
24228
|
+
codex: "codex"
|
|
24229
|
+
};
|
|
24230
|
+
var RUN_AS_USER = "agent";
|
|
24231
|
+
var ALLOWED_DOMAINS_PATH = "/etc/node9-sandbox/allowed-domains.txt";
|
|
24232
|
+
function renderDockerfile(config, node9Version2) {
|
|
24233
|
+
const agentPkg = AGENT_NPM_PACKAGE[config.agent];
|
|
24234
|
+
return `# Auto-generated by node9 sandbox. Do not edit by hand.
|
|
24235
|
+
FROM node:22-bookworm
|
|
24236
|
+
|
|
24237
|
+
ENV DEBIAN_FRONTEND=noninteractive
|
|
24238
|
+
|
|
24239
|
+
# Wall + base tooling (iptables/ipset/dig/gosu) \u2014 ported from Isag.
|
|
24240
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \\
|
|
24241
|
+
ca-certificates curl git gosu iproute2 ipset iptables dnsutils jq \\
|
|
24242
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
24243
|
+
|
|
24244
|
+
# The worker (agent CLI).
|
|
24245
|
+
RUN npm install -g ${agentPkg}
|
|
24246
|
+
|
|
24247
|
+
# The guard (node9), pinned to the host version.
|
|
24248
|
+
RUN npm install -g node9-ai@${node9Version2}
|
|
24249
|
+
|
|
24250
|
+
# Non-root runtime user at uid 1000 (matches the typical single-user host so the
|
|
24251
|
+
# mounted ~/.claude / ~/.codex / project are read/writable). The node base image
|
|
24252
|
+
# already claims uid 1000 for the 'node' user \u2014 free it first (cf. Isag/ubuntu).
|
|
24253
|
+
RUN userdel -r node 2>/dev/null || true; \\
|
|
24254
|
+
userdel -r ubuntu 2>/dev/null || true; \\
|
|
24255
|
+
useradd --create-home --uid 1000 --shell /bin/bash ${RUN_AS_USER}
|
|
24256
|
+
|
|
24257
|
+
# Wire the agent's node9 hooks into the runtime user's home (build-time, static).
|
|
24258
|
+
RUN gosu ${RUN_AS_USER} node9 agents add ${config.agent} || true
|
|
24259
|
+
|
|
24260
|
+
RUN mkdir -p /workspace /etc/node9-sandbox \\
|
|
24261
|
+
&& chown ${RUN_AS_USER}:${RUN_AS_USER} /workspace
|
|
24262
|
+
|
|
24263
|
+
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
|
24264
|
+
RUN chmod +x /usr/local/bin/entrypoint.sh
|
|
24265
|
+
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
|
24266
|
+
`;
|
|
24267
|
+
}
|
|
24268
|
+
function renderEntrypoint(config) {
|
|
24269
|
+
const agentBin = AGENT_BIN[config.agent];
|
|
24270
|
+
return `#!/usr/bin/env bash
|
|
24271
|
+
# Auto-generated by node9 sandbox. Seals the egress wall (root), then drops to the
|
|
24272
|
+
# non-root agent which starts the node9 daemon + execs the agent.
|
|
24273
|
+
set -Eeuo pipefail
|
|
24274
|
+
|
|
24275
|
+
DOMAINS_FILE="${ALLOWED_DOMAINS_PATH}"
|
|
24276
|
+
RUN_AS_USER="${RUN_AS_USER}"
|
|
24277
|
+
|
|
24278
|
+
[[ -s "$DOMAINS_FILE" ]] || { echo "entrypoint: missing/empty $DOMAINS_FILE" >&2; exit 1; }
|
|
24279
|
+
|
|
24280
|
+
# Own the mounted node9 data dir so the agent user can write audit there.
|
|
24281
|
+
mkdir -p "/home/$RUN_AS_USER/.node9"
|
|
24282
|
+
chown -R "$RUN_AS_USER:$RUN_AS_USER" "/home/$RUN_AS_USER/.node9" || true
|
|
24283
|
+
|
|
24284
|
+
# \u2500\u2500 Resolve the allowlist \u2192 ipset (union of every resolver, like Isag) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
24285
|
+
mapfile -t LOCAL_DNS < <(awk '/^nameserver / {print $2}' /etc/resolv.conf)
|
|
24286
|
+
[[ \${#LOCAL_DNS[@]} -gt 0 ]] || { echo "entrypoint: no resolvers in /etc/resolv.conf" >&2; exit 1; }
|
|
24287
|
+
|
|
24288
|
+
ipset create node9_allowed hash:ip family inet -exist
|
|
24289
|
+
ipset flush node9_allowed
|
|
24290
|
+
|
|
24291
|
+
while IFS= read -r domain; do
|
|
24292
|
+
[[ -n "$domain" ]] || continue
|
|
24293
|
+
found=0
|
|
24294
|
+
# union the local resolver + each upstream so CDN/anycast IP rotation is covered
|
|
24295
|
+
for ip in $(getent ahostsv4 "$domain" 2>/dev/null | awk '{print $1}' | sort -u); do
|
|
24296
|
+
ipset add node9_allowed "$ip" -exist; found=1
|
|
24297
|
+
done
|
|
24298
|
+
for r in "\${LOCAL_DNS[@]}"; do
|
|
24299
|
+
for ip in $(dig +short +time=2 +tries=1 @"$r" A "$domain" 2>/dev/null | awk '/^[0-9.]+$/'); do
|
|
24300
|
+
ipset add node9_allowed "$ip" -exist; found=1
|
|
24301
|
+
done
|
|
24302
|
+
done
|
|
24303
|
+
[[ $found -eq 1 ]] || { echo "entrypoint: failed to resolve $domain" >&2; exit 1; }
|
|
24304
|
+
echo "entrypoint: allowed $domain"
|
|
24305
|
+
done < "$DOMAINS_FILE"
|
|
24306
|
+
|
|
24307
|
+
# \u2500\u2500 Seal iptables: deny-by-default except lo, established, DNS, the allowlist \u2500\u2500\u2500\u2500
|
|
24308
|
+
echo "entrypoint: sealing firewall..."
|
|
24309
|
+
iptables -F; iptables -X
|
|
24310
|
+
iptables -P INPUT DROP
|
|
24311
|
+
iptables -P FORWARD DROP
|
|
24312
|
+
iptables -P OUTPUT DROP
|
|
24313
|
+
iptables -A INPUT -i lo -j ACCEPT
|
|
24314
|
+
iptables -A OUTPUT -o lo -j ACCEPT
|
|
24315
|
+
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
|
24316
|
+
iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
|
24317
|
+
for r in "\${LOCAL_DNS[@]}"; do
|
|
24318
|
+
iptables -A OUTPUT -p udp -d "$r" --dport 53 -j ACCEPT
|
|
24319
|
+
iptables -A OUTPUT -p tcp -d "$r" --dport 53 -j ACCEPT
|
|
24320
|
+
done
|
|
24321
|
+
iptables -A OUTPUT -m set --match-set node9_allowed dst -j ACCEPT
|
|
24322
|
+
|
|
24323
|
+
# \u2500\u2500 Drop to the agent: start the node9 daemon (as the user), then exec the agent \u2500
|
|
24324
|
+
echo "entrypoint: starting node9 + ${agentBin} as $RUN_AS_USER"
|
|
24325
|
+
exec gosu "$RUN_AS_USER" bash -lc '
|
|
24326
|
+
set -e
|
|
24327
|
+
node9 daemon --background >/dev/null 2>&1 || true
|
|
24328
|
+
cd /workspace
|
|
24329
|
+
exec ${agentBin} "$@"
|
|
24330
|
+
' -- "$@"
|
|
24331
|
+
`;
|
|
24332
|
+
}
|
|
24333
|
+
|
|
24334
|
+
// src/posture/egress.ts
|
|
24335
|
+
function sandboxEgressWallActive() {
|
|
24336
|
+
try {
|
|
24337
|
+
return import_fs47.default.existsSync(ALLOWED_DOMAINS_PATH);
|
|
24338
|
+
} catch {
|
|
24339
|
+
return false;
|
|
24340
|
+
}
|
|
24341
|
+
}
|
|
24216
24342
|
function evaluateEgressConfig(egress) {
|
|
24217
24343
|
if (egress.enabled && egress.mode === "block") {
|
|
24218
24344
|
return {
|
|
@@ -24262,6 +24388,21 @@ function evaluateEgressConfig(egress) {
|
|
|
24262
24388
|
};
|
|
24263
24389
|
}
|
|
24264
24390
|
function checkEgress(ctx) {
|
|
24391
|
+
if (sandboxEgressWallActive()) {
|
|
24392
|
+
return [
|
|
24393
|
+
{
|
|
24394
|
+
category: "Egress",
|
|
24395
|
+
severity: "advisory",
|
|
24396
|
+
title: "Egress is hard-blocked by the sandbox kernel wall",
|
|
24397
|
+
what: "Outbound is deny-by-default at the kernel; only the allowlist is reachable.",
|
|
24398
|
+
why: "The sandbox seals egress with an ipset/iptables wall before the agent starts.",
|
|
24399
|
+
who: "Even a compromised agent can only reach the allowlisted hosts.",
|
|
24400
|
+
owner: "node9",
|
|
24401
|
+
detail: [],
|
|
24402
|
+
coverage: { state: "covered", level: "block", via: "sandbox egress wall" }
|
|
24403
|
+
}
|
|
24404
|
+
];
|
|
24405
|
+
}
|
|
24265
24406
|
const config = getConfig(ctx.cwd);
|
|
24266
24407
|
const egress = config.policy.egress;
|
|
24267
24408
|
return [evaluateEgressConfig({ enabled: egress.enabled, mode: egress.mode })];
|
|
@@ -24302,7 +24443,7 @@ async function checkGate(ctx) {
|
|
|
24302
24443
|
}
|
|
24303
24444
|
|
|
24304
24445
|
// src/posture/supply-chain.ts
|
|
24305
|
-
var
|
|
24446
|
+
var import_fs48 = __toESM(require("fs"));
|
|
24306
24447
|
var import_os42 = __toESM(require("os"));
|
|
24307
24448
|
var import_path48 = __toESM(require("path"));
|
|
24308
24449
|
var import_smol_toml3 = require("smol-toml");
|
|
@@ -24319,9 +24460,9 @@ function isNode9Managed(command, args = []) {
|
|
|
24319
24460
|
var MAX_CONFIG_BYTES = 10 * 1024 * 1024;
|
|
24320
24461
|
function readServers(file, format, agent) {
|
|
24321
24462
|
try {
|
|
24322
|
-
const stat =
|
|
24463
|
+
const stat = import_fs48.default.statSync(file);
|
|
24323
24464
|
if (!stat.isFile() || stat.size > MAX_CONFIG_BYTES) return [];
|
|
24324
|
-
const text =
|
|
24465
|
+
const text = import_fs48.default.readFileSync(file, "utf8");
|
|
24325
24466
|
const map = format === "toml" ? (0, import_smol_toml3.parse)(text)?.mcp_servers : JSON.parse(text)?.mcpServers;
|
|
24326
24467
|
if (!map || typeof map !== "object") return [];
|
|
24327
24468
|
return Object.entries(map).map(([name, v]) => ({
|
|
@@ -24417,11 +24558,12 @@ async function checkPrivilege(ctx) {
|
|
|
24417
24558
|
}
|
|
24418
24559
|
|
|
24419
24560
|
// src/posture/containment.ts
|
|
24420
|
-
var
|
|
24561
|
+
var import_fs49 = __toESM(require("fs"));
|
|
24562
|
+
var ISOLATION_WEIGHT = 12;
|
|
24421
24563
|
function inContainer() {
|
|
24422
|
-
if (
|
|
24564
|
+
if (import_fs49.default.existsSync("/.dockerenv") || import_fs49.default.existsSync("/run/.containerenv")) return true;
|
|
24423
24565
|
try {
|
|
24424
|
-
const cgroup =
|
|
24566
|
+
const cgroup = import_fs49.default.readFileSync("/proc/1/cgroup", "utf8");
|
|
24425
24567
|
if (/docker|kubepods|containerd|lxc|libpod/.test(cgroup)) return true;
|
|
24426
24568
|
} catch {
|
|
24427
24569
|
}
|
|
@@ -24440,14 +24582,28 @@ function checkContainment(_ctx) {
|
|
|
24440
24582
|
detail: [],
|
|
24441
24583
|
owner: "os",
|
|
24442
24584
|
node9Reduces: true,
|
|
24443
|
-
|
|
24444
|
-
|
|
24585
|
+
// The single biggest hardening gap, and node9 now fully remedies it
|
|
24586
|
+
// (`node9 sandbox run`). Deducts while open; closing it is the headline
|
|
24587
|
+
// payoff. No coverageProbe → stays OPEN (scored) until adopted; live
|
|
24588
|
+
// partial-credit for the lighter shield path is a fast-follow.
|
|
24589
|
+
scoreWeight: ISOLATION_WEIGHT,
|
|
24590
|
+
gain: "jailed container \xB7 kernel egress wall \xB7 scoped mounts \xB7 governed inside",
|
|
24591
|
+
cost: "the agent works inside /workspace, not your live host",
|
|
24592
|
+
fix: `Two ways to shrink the blast radius \u2014 pick by how much flexibility you need:
|
|
24593
|
+
Strongest \u2014 jail it (closes this gap, +${ISOLATION_WEIGHT}):
|
|
24594
|
+
\u2022 node9 sandbox run <agent>
|
|
24595
|
+
Lighter \u2014 harden in place, keep full host access (about +${Math.round(
|
|
24596
|
+
ISOLATION_WEIGHT / 2
|
|
24597
|
+
)}):
|
|
24598
|
+
\u2022 node9 shield enable project-jail \u2014 block stray credential reads
|
|
24599
|
+
\u2022 node9 egress lock \u2014 block data exfil`
|
|
24445
24600
|
}
|
|
24446
24601
|
];
|
|
24447
24602
|
}
|
|
24448
24603
|
|
|
24449
24604
|
// src/posture/inbound.ts
|
|
24450
|
-
var
|
|
24605
|
+
var import_fs50 = __toESM(require("fs"));
|
|
24606
|
+
var DB_EXPOSURE_WEIGHT = 4;
|
|
24451
24607
|
var KNOWN_SERVICE_PORTS = {
|
|
24452
24608
|
5432: "PostgreSQL",
|
|
24453
24609
|
6379: "Redis",
|
|
@@ -24533,7 +24689,7 @@ function collectListeners() {
|
|
|
24533
24689
|
const byPort = /* @__PURE__ */ new Map();
|
|
24534
24690
|
for (const file of ["/proc/net/tcp", "/proc/net/tcp6"]) {
|
|
24535
24691
|
try {
|
|
24536
|
-
for (const l of parseListeners(
|
|
24692
|
+
for (const l of parseListeners(import_fs50.default.readFileSync(file, "utf8"))) {
|
|
24537
24693
|
if (!byPort.has(l.port)) byPort.set(l.port, l);
|
|
24538
24694
|
}
|
|
24539
24695
|
} catch {
|
|
@@ -24545,11 +24701,11 @@ function readProc(pid) {
|
|
|
24545
24701
|
let comm = "unknown";
|
|
24546
24702
|
let cmdline = "";
|
|
24547
24703
|
try {
|
|
24548
|
-
comm =
|
|
24704
|
+
comm = import_fs50.default.readFileSync(`/proc/${pid}/comm`, "utf8").trim().replace(/-MainThread$/, "") || "unknown";
|
|
24549
24705
|
} catch {
|
|
24550
24706
|
}
|
|
24551
24707
|
try {
|
|
24552
|
-
cmdline =
|
|
24708
|
+
cmdline = import_fs50.default.readFileSync(`/proc/${pid}/cmdline`).toString().replace(/\0/g, " ").trim();
|
|
24553
24709
|
} catch {
|
|
24554
24710
|
}
|
|
24555
24711
|
return { comm, cmdline };
|
|
@@ -24559,21 +24715,21 @@ function resolveProcesses(inodes) {
|
|
|
24559
24715
|
if (inodes.size === 0) return map;
|
|
24560
24716
|
let pids;
|
|
24561
24717
|
try {
|
|
24562
|
-
pids =
|
|
24718
|
+
pids = import_fs50.default.readdirSync("/proc").filter((d) => /^\d+$/.test(d));
|
|
24563
24719
|
} catch {
|
|
24564
24720
|
return map;
|
|
24565
24721
|
}
|
|
24566
24722
|
for (const pid of pids) {
|
|
24567
24723
|
let fds;
|
|
24568
24724
|
try {
|
|
24569
|
-
fds =
|
|
24725
|
+
fds = import_fs50.default.readdirSync(`/proc/${pid}/fd`);
|
|
24570
24726
|
} catch {
|
|
24571
24727
|
continue;
|
|
24572
24728
|
}
|
|
24573
24729
|
for (const fd of fds) {
|
|
24574
24730
|
let link;
|
|
24575
24731
|
try {
|
|
24576
|
-
link =
|
|
24732
|
+
link = import_fs50.default.readlinkSync(`/proc/${pid}/fd/${fd}`);
|
|
24577
24733
|
} catch {
|
|
24578
24734
|
continue;
|
|
24579
24735
|
}
|
|
@@ -24628,8 +24784,15 @@ function checkInbound(ctx) {
|
|
|
24628
24784
|
// Only when node9 actually has a shield for an exposed service — otherwise
|
|
24629
24785
|
// (bare dev servers) it stays purely the user's to rebind.
|
|
24630
24786
|
node9Reduces: reduces,
|
|
24631
|
-
|
|
24632
|
-
|
|
24787
|
+
// When a db-shield applies this is real, node9-addressable hardening → it
|
|
24788
|
+
// scores (and stays OPEN, no cantFix probe). Bare dev servers node9 can't
|
|
24789
|
+
// touch stay can't-fix / your-part / unscored.
|
|
24790
|
+
...reduces ? {
|
|
24791
|
+
scoreWeight: DB_EXPOSURE_WEIGHT,
|
|
24792
|
+
gain: "blocks DROP TABLE / TRUNCATE / FLUSHALL on the exposed DB",
|
|
24793
|
+
cost: "you confirm legit destructive migrations"
|
|
24794
|
+
} : { coverageProbe: { kind: "cantFix" } },
|
|
24795
|
+
fix
|
|
24633
24796
|
});
|
|
24634
24797
|
}
|
|
24635
24798
|
return findings;
|
|
@@ -24679,16 +24842,20 @@ function scorePosture(findings, checksRun) {
|
|
|
24679
24842
|
const open = findings.filter(
|
|
24680
24843
|
(f) => f.coverage?.state !== "covered" && f.coverage?.state !== "cant-fix"
|
|
24681
24844
|
);
|
|
24682
|
-
const count = (sev) => open.filter((f) => f.severity === sev).length;
|
|
24683
|
-
|
|
24845
|
+
const count = (sev) => open.filter((f) => f.severity === sev && !f.scoreWeight).length;
|
|
24846
|
+
const base = computeSecurityScore({
|
|
24684
24847
|
critical: count("critical"),
|
|
24685
24848
|
high: count("high"),
|
|
24686
24849
|
medium: count("medium"),
|
|
24687
|
-
// Denominator = number of checks evaluated. With computeSecurityScore's
|
|
24688
|
-
// caps this makes any critical → critical tier, any high → at-risk, and a
|
|
24689
|
-
// fully clean run (0 findings, checksRun > 0) → 100/good.
|
|
24690
24850
|
total: Math.max(checksRun, 1)
|
|
24691
24851
|
});
|
|
24852
|
+
const headroom = open.reduce((sum, f) => sum + (f.scoreWeight ?? 0), 0);
|
|
24853
|
+
const score = Math.max(0, base.score - headroom);
|
|
24854
|
+
const tier = score >= 80 ? "good" : score >= 50 ? "at-risk" : "critical";
|
|
24855
|
+
return { score, tier };
|
|
24856
|
+
}
|
|
24857
|
+
function openHeadroom(findings) {
|
|
24858
|
+
return findings.filter((f) => f.coverage?.state !== "covered" && f.coverage?.state !== "cant-fix").reduce((sum, f) => sum + (f.scoreWeight ?? 0), 0);
|
|
24692
24859
|
}
|
|
24693
24860
|
|
|
24694
24861
|
// src/posture/headline.ts
|
|
@@ -24898,9 +25065,10 @@ var LABEL_WIDTH = 14;
|
|
|
24898
25065
|
function label(category) {
|
|
24899
25066
|
return import_chalk24.default.bold(category.padEnd(Math.max(LABEL_WIDTH, category.length + 1)));
|
|
24900
25067
|
}
|
|
24901
|
-
function renderFinding(f) {
|
|
25068
|
+
function renderFinding(f, showWeight = false) {
|
|
24902
25069
|
const lines = [];
|
|
24903
|
-
|
|
25070
|
+
const wt = showWeight && f.scoreWeight ? import_chalk24.default.cyan.bold(`+${f.scoreWeight} `) : "";
|
|
25071
|
+
lines.push(` ${ICON[f.severity]} ${label(f.category)}${wt}${f.title}`);
|
|
24904
25072
|
const indent = " ".repeat(2 + 3 + LABEL_WIDTH);
|
|
24905
25073
|
const width = 80 - indent.length;
|
|
24906
25074
|
for (const s of [f.what, f.why, f.who]) {
|
|
@@ -24916,6 +25084,16 @@ function renderFinding(f) {
|
|
|
24916
25084
|
}
|
|
24917
25085
|
}
|
|
24918
25086
|
}
|
|
25087
|
+
const tradeoff = [
|
|
25088
|
+
[f.gain, "gain: ", import_chalk24.default.green],
|
|
25089
|
+
[f.cost, "cost: ", import_chalk24.default.yellow]
|
|
25090
|
+
];
|
|
25091
|
+
for (const [text, lbl, color2] of tradeoff) {
|
|
25092
|
+
if (!text) continue;
|
|
25093
|
+
wrap(text, width - 6).forEach((l, i) => {
|
|
25094
|
+
lines.push(indent + (i === 0 ? color2(lbl) : " ") + import_chalk24.default.gray(l));
|
|
25095
|
+
});
|
|
25096
|
+
}
|
|
24919
25097
|
return lines;
|
|
24920
25098
|
}
|
|
24921
25099
|
function renderPosture(result) {
|
|
@@ -24925,15 +25103,11 @@ function renderPosture(result) {
|
|
|
24925
25103
|
lines.push(
|
|
24926
25104
|
import_chalk24.default.cyan.bold(`\u{1F6E1}\uFE0F Node9 Posture`) + import_chalk24.default.gray(` \u2014 ${result.agent}`) + ` ${import_chalk24.default.bold(`Score: ${result.score}/100`)} (${tier})`
|
|
24927
25105
|
);
|
|
24928
|
-
const
|
|
24929
|
-
|
|
24930
|
-
).length;
|
|
24931
|
-
if (advisories > 0) {
|
|
24932
|
-
const word = advisories === 1 ? "advisory" : "advisories";
|
|
24933
|
-
const verb = advisories === 1 ? "doesn't" : "don't";
|
|
25106
|
+
const headroom = openHeadroom(result.findings);
|
|
25107
|
+
if (headroom > 0) {
|
|
24934
25108
|
lines.push(
|
|
24935
25109
|
" " + import_chalk24.default.gray(
|
|
24936
|
-
`${
|
|
25110
|
+
`${headroom} pts of headroom below \u2014 each layer adds security at some cost to flexibility. You choose which to close.`
|
|
24937
25111
|
)
|
|
24938
25112
|
);
|
|
24939
25113
|
}
|
|
@@ -24949,7 +25123,7 @@ function renderPosture(result) {
|
|
|
24949
25123
|
const covered = result.findings.filter((f) => f.coverage?.state === "covered");
|
|
24950
25124
|
const open = result.findings.filter((f) => f.coverage?.state !== "covered");
|
|
24951
25125
|
if (covered.length > 0) {
|
|
24952
|
-
lines.push(" " + import_chalk24.default.green("\u{1F7E2} node9 is
|
|
25126
|
+
lines.push(" " + import_chalk24.default.green("\u{1F7E2} ON NOW \u2014 node9 is enforcing these (your floor)"));
|
|
24953
25127
|
for (const f of covered) {
|
|
24954
25128
|
const gated = f.coverage?.level === "review" ? "approval-gating" : "blocking";
|
|
24955
25129
|
const via = f.coverage?.via ?? "node9";
|
|
@@ -24964,18 +25138,16 @@ function renderPosture(result) {
|
|
|
24964
25138
|
const osOpen = open.filter((f) => f.owner !== "node9" && !f.node9Reduces);
|
|
24965
25139
|
if (node9Open.length > 0) {
|
|
24966
25140
|
lines.push(" " + import_chalk24.default.cyan.bold("\u{1F527} node9 can fix these \u2014 run the command"));
|
|
24967
|
-
for (const f of node9Open) lines.push(...renderFinding(f));
|
|
25141
|
+
for (const f of node9Open) lines.push(...renderFinding(f, true));
|
|
24968
25142
|
}
|
|
24969
25143
|
if (reduceOpen.length > 0) {
|
|
24970
25144
|
if (node9Open.length > 0) lines.push("");
|
|
24971
|
-
lines.push(
|
|
24972
|
-
|
|
24973
|
-
);
|
|
24974
|
-
for (const f of reduceOpen) lines.push(...renderFinding(f));
|
|
25145
|
+
lines.push(" " + import_chalk24.default.yellow.bold("\u{1F512} AVAILABLE \u2014 turn on to harden (each has a tradeoff)"));
|
|
25146
|
+
for (const f of reduceOpen) lines.push(...renderFinding(f, true));
|
|
24975
25147
|
}
|
|
24976
25148
|
if (osOpen.length > 0) {
|
|
24977
25149
|
if (node9Open.length > 0 || reduceOpen.length > 0) lines.push("");
|
|
24978
|
-
lines.push(" " + import_chalk24.default.bold("\u{1F9F1}
|
|
25150
|
+
lines.push(" " + import_chalk24.default.bold("\u{1F9F1} YOUR PART \u2014 node9 can't fix these (OS-level)"));
|
|
24979
25151
|
for (const f of osOpen) lines.push(...renderFinding(f));
|
|
24980
25152
|
}
|
|
24981
25153
|
for (const cat of result.passedCategories) {
|
|
@@ -25030,7 +25202,12 @@ function buildShipBody(result) {
|
|
|
25030
25202
|
// The runnable fix / OS action — commands + advice, never a path.
|
|
25031
25203
|
fix: f.fix,
|
|
25032
25204
|
// Whose job it is. Default 'os' so the SaaS never falsely claims node9 can fix it.
|
|
25033
|
-
owner: f.owner ?? "os"
|
|
25205
|
+
owner: f.owner ?? "os",
|
|
25206
|
+
// Hardening weight + the flexibility tradeoff (generic prose / a number —
|
|
25207
|
+
// no values or paths), so the fleet view can show the same headroom story.
|
|
25208
|
+
scoreWeight: f.scoreWeight,
|
|
25209
|
+
gain: f.gain,
|
|
25210
|
+
cost: f.cost
|
|
25034
25211
|
}))
|
|
25035
25212
|
};
|
|
25036
25213
|
}
|
|
@@ -25101,7 +25278,7 @@ function registerPostureCommand(program2) {
|
|
|
25101
25278
|
|
|
25102
25279
|
// src/cli/commands/egress.ts
|
|
25103
25280
|
var import_chalk26 = __toESM(require("chalk"));
|
|
25104
|
-
var
|
|
25281
|
+
var import_fs51 = __toESM(require("fs"));
|
|
25105
25282
|
var import_os45 = __toESM(require("os"));
|
|
25106
25283
|
var import_path49 = __toESM(require("path"));
|
|
25107
25284
|
init_config();
|
|
@@ -25119,7 +25296,7 @@ function configPath() {
|
|
|
25119
25296
|
function readRawConfig() {
|
|
25120
25297
|
let text;
|
|
25121
25298
|
try {
|
|
25122
|
-
text =
|
|
25299
|
+
text = import_fs51.default.readFileSync(configPath(), "utf8");
|
|
25123
25300
|
} catch (err2) {
|
|
25124
25301
|
if (err2.code === "ENOENT") return {};
|
|
25125
25302
|
throw err2;
|
|
@@ -25134,8 +25311,8 @@ function readRawConfig() {
|
|
|
25134
25311
|
}
|
|
25135
25312
|
function writeRawConfig(config) {
|
|
25136
25313
|
const p = configPath();
|
|
25137
|
-
|
|
25138
|
-
|
|
25314
|
+
import_fs51.default.mkdirSync(import_path49.default.dirname(p), { recursive: true });
|
|
25315
|
+
import_fs51.default.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
|
|
25139
25316
|
}
|
|
25140
25317
|
function applyEgress(config, change) {
|
|
25141
25318
|
const policy = config.policy = config.policy ?? {};
|
|
@@ -25226,11 +25403,365 @@ function registerEgressCommand(program2) {
|
|
|
25226
25403
|
egress.action(showStatus);
|
|
25227
25404
|
}
|
|
25228
25405
|
|
|
25229
|
-
// src/cli/commands/
|
|
25406
|
+
// src/cli/commands/sandbox.ts
|
|
25230
25407
|
var import_chalk27 = __toESM(require("chalk"));
|
|
25231
|
-
var
|
|
25408
|
+
var import_fs54 = __toESM(require("fs"));
|
|
25409
|
+
var import_path52 = __toESM(require("path"));
|
|
25410
|
+
var import_child_process13 = require("child_process");
|
|
25411
|
+
init_config();
|
|
25412
|
+
|
|
25413
|
+
// src/sandbox/config.ts
|
|
25414
|
+
var import_fs52 = __toESM(require("fs"));
|
|
25232
25415
|
var import_path50 = __toESM(require("path"));
|
|
25416
|
+
var import_yaml = require("yaml");
|
|
25417
|
+
var SANDBOX_CONFIG_FILE = "node9.sandbox.yaml";
|
|
25418
|
+
var FORBIDDEN_ENV = /* @__PURE__ */ new Set(["NODE9_API_KEY", "NODE9_API_URL"]);
|
|
25419
|
+
function defaultSandboxConfig(agent) {
|
|
25420
|
+
return {
|
|
25421
|
+
agent,
|
|
25422
|
+
workspace: { mount: ".", target: "/workspace", mode: "rw" },
|
|
25423
|
+
runtime: { engine: "docker", image: "node9-sandbox:local", rebuild: "auto" },
|
|
25424
|
+
outbound: {
|
|
25425
|
+
mode: "block",
|
|
25426
|
+
allow: agent === "codex" ? ["api.openai.com", "api.github.com", "github.com", "registry.npmjs.org"] : ["api.anthropic.com", "api.github.com", "github.com", "registry.npmjs.org"]
|
|
25427
|
+
},
|
|
25428
|
+
inbound: { expose: [] },
|
|
25429
|
+
// Provider key only — NODE9_API_KEY intentionally absent (fix #1).
|
|
25430
|
+
env: { pass: [agent === "codex" ? "OPENAI_API_KEY" : "ANTHROPIC_API_KEY"] },
|
|
25431
|
+
// Terminal-only approval in the MVP; cloud/native/browser off (fix #1).
|
|
25432
|
+
node9: {
|
|
25433
|
+
approvals: { terminal: true, native: false, browser: false, cloud: false },
|
|
25434
|
+
// Mount the agent's OAuth/creds dir so it can authenticate in the box.
|
|
25435
|
+
mountAgentCredentials: true
|
|
25436
|
+
}
|
|
25437
|
+
};
|
|
25438
|
+
}
|
|
25439
|
+
function asStringArray(v) {
|
|
25440
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
25441
|
+
}
|
|
25442
|
+
function mergeSandboxConfig(raw, fallbackAgent) {
|
|
25443
|
+
const r = raw && typeof raw === "object" ? raw : {};
|
|
25444
|
+
const agent = typeof r.agent === "string" ? r.agent : fallbackAgent;
|
|
25445
|
+
if (agent !== "claude" && agent !== "codex") {
|
|
25446
|
+
throw new Error(`sandbox: unsupported agent "${String(agent)}" (use claude or codex)`);
|
|
25447
|
+
}
|
|
25448
|
+
const d = defaultSandboxConfig(agent);
|
|
25449
|
+
const ws = r.workspace ?? {};
|
|
25450
|
+
const rt = r.runtime ?? {};
|
|
25451
|
+
const out = r.outbound ?? {};
|
|
25452
|
+
const inb = r.inbound ?? {};
|
|
25453
|
+
const env = r.env ?? {};
|
|
25454
|
+
const n9 = r.node9 ?? {};
|
|
25455
|
+
const appr = n9.approvals ?? {};
|
|
25456
|
+
const pass = asStringArray(env.pass).filter((k) => !FORBIDDEN_ENV.has(k));
|
|
25457
|
+
return {
|
|
25458
|
+
agent,
|
|
25459
|
+
workspace: {
|
|
25460
|
+
mount: typeof ws.mount === "string" ? ws.mount : d.workspace.mount,
|
|
25461
|
+
target: typeof ws.target === "string" ? ws.target : d.workspace.target,
|
|
25462
|
+
mode: ws.mode === "ro" ? "ro" : "rw"
|
|
25463
|
+
},
|
|
25464
|
+
runtime: {
|
|
25465
|
+
engine: rt.engine === "podman" ? "podman" : "docker",
|
|
25466
|
+
image: typeof rt.image === "string" ? rt.image : d.runtime.image,
|
|
25467
|
+
rebuild: rt.rebuild === "never" || rt.rebuild === "always" ? rt.rebuild : d.runtime.rebuild
|
|
25468
|
+
},
|
|
25469
|
+
outbound: { mode: "block", allow: out.allow ? asStringArray(out.allow) : d.outbound.allow },
|
|
25470
|
+
inbound: { expose: inb.expose ? asStringArray(inb.expose) : d.inbound.expose },
|
|
25471
|
+
env: { pass: env.pass ? pass : d.env.pass },
|
|
25472
|
+
node9: {
|
|
25473
|
+
approvals: {
|
|
25474
|
+
terminal: appr.terminal !== false,
|
|
25475
|
+
native: appr.native === true,
|
|
25476
|
+
browser: appr.browser === true,
|
|
25477
|
+
cloud: appr.cloud === true
|
|
25478
|
+
},
|
|
25479
|
+
mountAgentCredentials: n9.mountAgentCredentials !== false
|
|
25480
|
+
}
|
|
25481
|
+
};
|
|
25482
|
+
}
|
|
25483
|
+
function scaffoldSandboxYaml(agent) {
|
|
25484
|
+
const header = "# node9.sandbox.yaml \u2014 sandbox TOPOLOGY (what the agent may touch).\n# Security policy (shields / egress rules / approvers) lives in ~/.node9/config.json\n# and applies to both native and sandbox. NODE9_API_KEY is never passed into the box.\n\n";
|
|
25485
|
+
return header + (0, import_yaml.stringify)(defaultSandboxConfig(agent));
|
|
25486
|
+
}
|
|
25487
|
+
function sandboxConfigPath(cwd = process.cwd()) {
|
|
25488
|
+
return import_path50.default.join(cwd, SANDBOX_CONFIG_FILE);
|
|
25489
|
+
}
|
|
25490
|
+
function loadSandboxConfig(cwd = process.cwd(), fallbackAgent = "claude") {
|
|
25491
|
+
const p = sandboxConfigPath(cwd);
|
|
25492
|
+
if (!import_fs52.default.existsSync(p)) {
|
|
25493
|
+
throw new Error(`sandbox: ${SANDBOX_CONFIG_FILE} not found \u2014 run \`node9 sandbox new\` first.`);
|
|
25494
|
+
}
|
|
25495
|
+
let raw;
|
|
25496
|
+
try {
|
|
25497
|
+
raw = (0, import_yaml.parse)(import_fs52.default.readFileSync(p, "utf-8"));
|
|
25498
|
+
} catch (err2) {
|
|
25499
|
+
throw new Error(
|
|
25500
|
+
`sandbox: ${SANDBOX_CONFIG_FILE} is not valid YAML \u2014 ${err2.message}`
|
|
25501
|
+
);
|
|
25502
|
+
}
|
|
25503
|
+
return mergeSandboxConfig(raw, fallbackAgent);
|
|
25504
|
+
}
|
|
25505
|
+
|
|
25506
|
+
// src/sandbox/firewall.ts
|
|
25507
|
+
var AGENT_PROVIDER_HOST = {
|
|
25508
|
+
claude: ["api.anthropic.com"],
|
|
25509
|
+
codex: ["api.openai.com"]
|
|
25510
|
+
};
|
|
25511
|
+
var NODE9_SAAS_HOSTS = ["api.node9.ai", "app.node9.ai", "node9.ai"];
|
|
25512
|
+
function isValidHost2(host) {
|
|
25513
|
+
if (typeof host !== "string") return false;
|
|
25514
|
+
const h = host.trim().toLowerCase();
|
|
25515
|
+
if (!h || h.length > 253) return false;
|
|
25516
|
+
if (/[\s/:@?#\\]/.test(h)) return false;
|
|
25517
|
+
return /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(
|
|
25518
|
+
h
|
|
25519
|
+
);
|
|
25520
|
+
}
|
|
25521
|
+
function compileAllowlist(input) {
|
|
25522
|
+
const norm = (h) => h.trim().toLowerCase();
|
|
25523
|
+
const denySet = /* @__PURE__ */ new Set([...input.configDeny.map(norm), ...NODE9_SAAS_HOSTS.map(norm)]);
|
|
25524
|
+
const candidates = [
|
|
25525
|
+
...AGENT_PROVIDER_HOST[input.agent],
|
|
25526
|
+
...input.sandboxAllow,
|
|
25527
|
+
...input.configAllow
|
|
25528
|
+
].map(norm);
|
|
25529
|
+
const allow = /* @__PURE__ */ new Set();
|
|
25530
|
+
const rejected = [];
|
|
25531
|
+
const denied = [];
|
|
25532
|
+
for (const host of candidates) {
|
|
25533
|
+
if (!host) continue;
|
|
25534
|
+
if (!isValidHost2(host)) {
|
|
25535
|
+
if (!rejected.includes(host)) rejected.push(host);
|
|
25536
|
+
continue;
|
|
25537
|
+
}
|
|
25538
|
+
if (denySet.has(host)) {
|
|
25539
|
+
if (!denied.includes(host)) denied.push(host);
|
|
25540
|
+
continue;
|
|
25541
|
+
}
|
|
25542
|
+
allow.add(host);
|
|
25543
|
+
}
|
|
25544
|
+
return {
|
|
25545
|
+
allow: [...allow].sort(),
|
|
25546
|
+
rejected: rejected.sort(),
|
|
25547
|
+
denied: denied.sort()
|
|
25548
|
+
};
|
|
25549
|
+
}
|
|
25550
|
+
|
|
25551
|
+
// src/sandbox/runtime.ts
|
|
25552
|
+
var import_fs53 = __toESM(require("fs"));
|
|
25233
25553
|
var import_os46 = __toESM(require("os"));
|
|
25554
|
+
var import_path51 = __toESM(require("path"));
|
|
25555
|
+
var import_crypto13 = __toESM(require("crypto"));
|
|
25556
|
+
var import_child_process12 = require("child_process");
|
|
25557
|
+
function sandboxDataDir(cwd = process.cwd()) {
|
|
25558
|
+
return import_path51.default.join(cwd, ".node9", "sandbox", "data");
|
|
25559
|
+
}
|
|
25560
|
+
function detectEngine(engine) {
|
|
25561
|
+
const r = (0, import_child_process12.spawnSync)(engine, ["--version"], { encoding: "utf-8" });
|
|
25562
|
+
if (r.status === 0 && typeof r.stdout === "string") {
|
|
25563
|
+
return { available: true, version: r.stdout.trim() };
|
|
25564
|
+
}
|
|
25565
|
+
return { available: false };
|
|
25566
|
+
}
|
|
25567
|
+
function agentCredentialsMount(agent) {
|
|
25568
|
+
const rel = agent === "codex" ? ".codex/auth.json" : ".claude/.credentials.json";
|
|
25569
|
+
return { hostPath: import_path51.default.join(import_os46.default.homedir(), rel), target: `/home/${RUN_AS_USER}/${rel}` };
|
|
25570
|
+
}
|
|
25571
|
+
function buildRunArgs(opts) {
|
|
25572
|
+
const { config, workspaceHostPath, dataHostPath, allowlistHostPath, agentArgs } = opts;
|
|
25573
|
+
const args = ["run", "--rm", "-it", "--cap-add=NET_ADMIN"];
|
|
25574
|
+
args.push("-v", `${workspaceHostPath}:${config.workspace.target}:${config.workspace.mode}`);
|
|
25575
|
+
args.push("-v", `${dataHostPath}:/home/${RUN_AS_USER}/.node9`);
|
|
25576
|
+
args.push("-v", `${allowlistHostPath}:${ALLOWED_DOMAINS_PATH}:ro`);
|
|
25577
|
+
if (config.node9.mountAgentCredentials) {
|
|
25578
|
+
const creds = agentCredentialsMount(config.agent);
|
|
25579
|
+
if (import_fs53.default.existsSync(creds.hostPath)) {
|
|
25580
|
+
args.push("-v", `${creds.hostPath}:${creds.target}`);
|
|
25581
|
+
}
|
|
25582
|
+
}
|
|
25583
|
+
for (const key of config.env.pass) {
|
|
25584
|
+
if (process.env[key] !== void 0) args.push("-e", key);
|
|
25585
|
+
}
|
|
25586
|
+
for (const port of config.inbound.expose) {
|
|
25587
|
+
args.push("-p", port);
|
|
25588
|
+
}
|
|
25589
|
+
args.push(config.runtime.image);
|
|
25590
|
+
if (agentArgs.length) args.push(...agentArgs);
|
|
25591
|
+
return args;
|
|
25592
|
+
}
|
|
25593
|
+
function imageContentHash(dockerfile, entrypoint) {
|
|
25594
|
+
return import_crypto13.default.createHash("sha256").update(dockerfile).update("\0").update(entrypoint).digest("hex").slice(0, 16);
|
|
25595
|
+
}
|
|
25596
|
+
function sandboxBuildDir(cwd = process.cwd()) {
|
|
25597
|
+
return import_path51.default.join(cwd, ".node9", "sandbox", "build");
|
|
25598
|
+
}
|
|
25599
|
+
function writeBuildContext(cwd, dockerfile, entrypoint) {
|
|
25600
|
+
const dir = sandboxBuildDir(cwd);
|
|
25601
|
+
import_fs53.default.mkdirSync(dir, { recursive: true });
|
|
25602
|
+
import_fs53.default.writeFileSync(import_path51.default.join(dir, "Dockerfile"), dockerfile);
|
|
25603
|
+
import_fs53.default.writeFileSync(import_path51.default.join(dir, "entrypoint.sh"), entrypoint);
|
|
25604
|
+
return dir;
|
|
25605
|
+
}
|
|
25606
|
+
function writeAllowlist(cwd, hosts) {
|
|
25607
|
+
const dir = import_path51.default.join(cwd, ".node9", "sandbox");
|
|
25608
|
+
import_fs53.default.mkdirSync(dir, { recursive: true });
|
|
25609
|
+
const p = import_path51.default.join(dir, "allowed-domains.txt");
|
|
25610
|
+
import_fs53.default.writeFileSync(p, hosts.join("\n") + "\n");
|
|
25611
|
+
return p;
|
|
25612
|
+
}
|
|
25613
|
+
function resolveHomePath(p) {
|
|
25614
|
+
return p.startsWith("~") ? import_path51.default.join(import_os46.default.homedir(), p.slice(1)) : import_path51.default.resolve(p);
|
|
25615
|
+
}
|
|
25616
|
+
|
|
25617
|
+
// src/cli/commands/sandbox.ts
|
|
25618
|
+
function seedDataDirConfig(dataDir, sandbox) {
|
|
25619
|
+
import_fs54.default.mkdirSync(dataDir, { recursive: true });
|
|
25620
|
+
const configPath2 = import_path52.default.join(dataDir, "config.json");
|
|
25621
|
+
const seed = {
|
|
25622
|
+
settings: {
|
|
25623
|
+
approvers: {
|
|
25624
|
+
terminal: sandbox.node9.approvals.terminal,
|
|
25625
|
+
native: false,
|
|
25626
|
+
browser: false,
|
|
25627
|
+
cloud: false
|
|
25628
|
+
}
|
|
25629
|
+
}
|
|
25630
|
+
};
|
|
25631
|
+
import_fs54.default.writeFileSync(configPath2, JSON.stringify(seed, null, 2), { mode: 384 });
|
|
25632
|
+
}
|
|
25633
|
+
function registerSandboxCommand(program2, version2) {
|
|
25634
|
+
const node9Version2 = pinnedNode9Version(version2);
|
|
25635
|
+
const cmd = program2.command("sandbox").description("Run an agent in a disposable, jailed container \u2014 governed + audited inside");
|
|
25636
|
+
cmd.command("new").description(`Scaffold ${SANDBOX_CONFIG_FILE} in this project`).option("--agent <agent>", "claude (default) or codex", "claude").action((opts) => {
|
|
25637
|
+
const agent = opts.agent === "codex" ? "codex" : "claude";
|
|
25638
|
+
const p = sandboxConfigPath();
|
|
25639
|
+
if (import_fs54.default.existsSync(p)) {
|
|
25640
|
+
console.log(
|
|
25641
|
+
import_chalk27.default.yellow(` ${SANDBOX_CONFIG_FILE} already exists \u2014 leaving it untouched.`)
|
|
25642
|
+
);
|
|
25643
|
+
return;
|
|
25644
|
+
}
|
|
25645
|
+
import_fs54.default.writeFileSync(p, scaffoldSandboxYaml(agent));
|
|
25646
|
+
console.log(
|
|
25647
|
+
import_chalk27.default.green(` \u2713 wrote ${SANDBOX_CONFIG_FILE}`) + import_chalk27.default.dim(` (agent: ${agent})`)
|
|
25648
|
+
);
|
|
25649
|
+
console.log(
|
|
25650
|
+
import_chalk27.default.dim(" Edit it (mounts / allow / expose), then: ") + import_chalk27.default.cyan("node9 sandbox run")
|
|
25651
|
+
);
|
|
25652
|
+
});
|
|
25653
|
+
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) => {
|
|
25654
|
+
const cwd = process.cwd();
|
|
25655
|
+
const sandbox = loadSandboxConfig(cwd, agentArg || "claude");
|
|
25656
|
+
if (agentArg === "claude" || agentArg === "codex") sandbox.agent = agentArg;
|
|
25657
|
+
const engine = detectEngine(sandbox.runtime.engine);
|
|
25658
|
+
if (!engine.available) {
|
|
25659
|
+
console.error(
|
|
25660
|
+
import_chalk27.default.red(` ${sandbox.runtime.engine} not found.`) + import_chalk27.default.dim(` Install it first \u2014 node9 sandbox needs a container runtime.`)
|
|
25661
|
+
);
|
|
25662
|
+
process.exit(1);
|
|
25663
|
+
}
|
|
25664
|
+
const node9Config = getConfig(cwd);
|
|
25665
|
+
const compiled = compileAllowlist({
|
|
25666
|
+
agent: sandbox.agent,
|
|
25667
|
+
sandboxAllow: sandbox.outbound.allow,
|
|
25668
|
+
configAllow: node9Config.policy.egress.allow,
|
|
25669
|
+
configDeny: node9Config.policy.egress.deny
|
|
25670
|
+
});
|
|
25671
|
+
if (compiled.rejected.length) {
|
|
25672
|
+
console.log(
|
|
25673
|
+
import_chalk27.default.yellow(` \u26A0 ignoring invalid allow hosts: ${compiled.rejected.join(", ")}`)
|
|
25674
|
+
);
|
|
25675
|
+
}
|
|
25676
|
+
if (compiled.denied.length) {
|
|
25677
|
+
console.log(import_chalk27.default.dim(` (denied: ${compiled.denied.join(", ")})`));
|
|
25678
|
+
}
|
|
25679
|
+
const allowlistPath = writeAllowlist(cwd, compiled.allow);
|
|
25680
|
+
const dockerfile = renderDockerfile(sandbox, node9Version2);
|
|
25681
|
+
const entrypoint = renderEntrypoint(sandbox);
|
|
25682
|
+
const buildDir = writeBuildContext(cwd, dockerfile, entrypoint);
|
|
25683
|
+
const hash = imageContentHash(dockerfile, entrypoint);
|
|
25684
|
+
const image = sandbox.runtime.image;
|
|
25685
|
+
const hashFile = import_path52.default.join(sandboxBuildDir(cwd), ".image-hash");
|
|
25686
|
+
const lastHash = import_fs54.default.existsSync(hashFile) ? import_fs54.default.readFileSync(hashFile, "utf-8").trim() : "";
|
|
25687
|
+
const imageExists = (0, import_child_process13.spawnSync)(sandbox.runtime.engine, ["image", "inspect", image], { stdio: "ignore" }).status === 0;
|
|
25688
|
+
const needBuild = sandbox.runtime.rebuild === "always" || !imageExists || sandbox.runtime.rebuild !== "never" && lastHash !== hash;
|
|
25689
|
+
if (needBuild) {
|
|
25690
|
+
console.log(import_chalk27.default.dim(` building ${image} \u2026`));
|
|
25691
|
+
const b = (0, import_child_process13.spawnSync)(sandbox.runtime.engine, ["build", "-t", image, buildDir], {
|
|
25692
|
+
stdio: "inherit"
|
|
25693
|
+
});
|
|
25694
|
+
if (b.status !== 0) {
|
|
25695
|
+
console.error(import_chalk27.default.red(" build failed."));
|
|
25696
|
+
process.exit(b.status ?? 1);
|
|
25697
|
+
}
|
|
25698
|
+
import_fs54.default.writeFileSync(hashFile, hash);
|
|
25699
|
+
}
|
|
25700
|
+
const dataDir = sandboxDataDir(cwd);
|
|
25701
|
+
seedDataDirConfig(dataDir, sandbox);
|
|
25702
|
+
const passthru = command.args.slice(agentArg ? 1 : 0);
|
|
25703
|
+
const runArgs = buildRunArgs({
|
|
25704
|
+
config: sandbox,
|
|
25705
|
+
workspaceHostPath: resolveHomePath(sandbox.workspace.mount),
|
|
25706
|
+
dataHostPath: dataDir,
|
|
25707
|
+
allowlistHostPath: allowlistPath,
|
|
25708
|
+
agentArgs: passthru
|
|
25709
|
+
});
|
|
25710
|
+
if (sandbox.node9.mountAgentCredentials) {
|
|
25711
|
+
const creds = agentCredentialsMount(sandbox.agent);
|
|
25712
|
+
if (import_fs54.default.existsSync(creds.hostPath)) {
|
|
25713
|
+
console.log(import_chalk27.default.dim(` mounting ${creds.hostPath} (agent credentials, rw)`));
|
|
25714
|
+
} else {
|
|
25715
|
+
console.log(
|
|
25716
|
+
import_chalk27.default.yellow(` \u26A0 ${creds.hostPath} not found \u2014 `) + import_chalk27.default.dim(`the agent must auth via an env key in env.pass.`)
|
|
25717
|
+
);
|
|
25718
|
+
}
|
|
25719
|
+
}
|
|
25720
|
+
console.log(
|
|
25721
|
+
import_chalk27.default.green(` \u{1F6E1}\uFE0F ${sandbox.agent} jailed \u2014 ${compiled.allow.length} hosts allowed
|
|
25722
|
+
`)
|
|
25723
|
+
);
|
|
25724
|
+
const r = (0, import_child_process13.spawnSync)(sandbox.runtime.engine, runArgs, { stdio: "inherit" });
|
|
25725
|
+
process.exit(r.status ?? 0);
|
|
25726
|
+
});
|
|
25727
|
+
cmd.command("tail").description("Stream the sandbox's audit log (host-side)").action(() => {
|
|
25728
|
+
const auditPath = import_path52.default.join(sandboxDataDir(), "audit.log");
|
|
25729
|
+
if (!import_fs54.default.existsSync(auditPath)) {
|
|
25730
|
+
console.log(import_chalk27.default.dim(" no sandbox audit yet."));
|
|
25731
|
+
return;
|
|
25732
|
+
}
|
|
25733
|
+
(0, import_child_process13.spawnSync)("tail", ["-f", auditPath], { stdio: "inherit" });
|
|
25734
|
+
});
|
|
25735
|
+
cmd.command("logs").description("Dump the sandbox's audit log").action(() => {
|
|
25736
|
+
const auditPath = import_path52.default.join(sandboxDataDir(), "audit.log");
|
|
25737
|
+
if (!import_fs54.default.existsSync(auditPath)) {
|
|
25738
|
+
console.log(import_chalk27.default.dim(" no sandbox audit yet."));
|
|
25739
|
+
return;
|
|
25740
|
+
}
|
|
25741
|
+
process.stdout.write(import_fs54.default.readFileSync(auditPath, "utf-8"));
|
|
25742
|
+
});
|
|
25743
|
+
cmd.command("clean").description("Remove the sandbox image, build context, and data").action(() => {
|
|
25744
|
+
const cwd = process.cwd();
|
|
25745
|
+
let sandbox = null;
|
|
25746
|
+
try {
|
|
25747
|
+
sandbox = loadSandboxConfig(cwd);
|
|
25748
|
+
} catch {
|
|
25749
|
+
}
|
|
25750
|
+
if (sandbox) {
|
|
25751
|
+
(0, import_child_process13.spawnSync)(sandbox.runtime.engine, ["image", "rm", "-f", sandbox.runtime.image], {
|
|
25752
|
+
stdio: "ignore"
|
|
25753
|
+
});
|
|
25754
|
+
}
|
|
25755
|
+
import_fs54.default.rmSync(import_path52.default.join(cwd, ".node9", "sandbox"), { recursive: true, force: true });
|
|
25756
|
+
console.log(import_chalk27.default.green(" \u2713 sandbox image + build + data removed."));
|
|
25757
|
+
});
|
|
25758
|
+
}
|
|
25759
|
+
|
|
25760
|
+
// src/cli/commands/sessions.ts
|
|
25761
|
+
var import_chalk28 = __toESM(require("chalk"));
|
|
25762
|
+
var import_fs55 = __toESM(require("fs"));
|
|
25763
|
+
var import_path53 = __toESM(require("path"));
|
|
25764
|
+
var import_os47 = __toESM(require("os"));
|
|
25234
25765
|
init_scan_summary();
|
|
25235
25766
|
init_litellm();
|
|
25236
25767
|
init_cost_gemini();
|
|
@@ -25251,10 +25782,10 @@ function encodeProjectPath(projectPath) {
|
|
|
25251
25782
|
}
|
|
25252
25783
|
function sessionJsonlPath(projectPath, sessionId) {
|
|
25253
25784
|
const encoded = encodeProjectPath(projectPath);
|
|
25254
|
-
return
|
|
25785
|
+
return import_path53.default.join(import_os47.default.homedir(), ".claude", "projects", encoded, `${sessionId}.jsonl`);
|
|
25255
25786
|
}
|
|
25256
25787
|
function projectLabel(projectPath) {
|
|
25257
|
-
return projectPath.replace(
|
|
25788
|
+
return projectPath.replace(import_os47.default.homedir(), "~");
|
|
25258
25789
|
}
|
|
25259
25790
|
function parseHistoryLines(lines) {
|
|
25260
25791
|
const entries = [];
|
|
@@ -25323,10 +25854,10 @@ function parseSessionLines(lines) {
|
|
|
25323
25854
|
return { toolCalls, costUSD, hasSnapshot, modifiedFiles };
|
|
25324
25855
|
}
|
|
25325
25856
|
function loadAuditEntries(auditPath) {
|
|
25326
|
-
const aPath = auditPath ??
|
|
25857
|
+
const aPath = auditPath ?? import_path53.default.join(import_os47.default.homedir(), ".node9", "audit.log");
|
|
25327
25858
|
let raw;
|
|
25328
25859
|
try {
|
|
25329
|
-
raw =
|
|
25860
|
+
raw = import_fs55.default.readFileSync(aPath, "utf-8");
|
|
25330
25861
|
} catch {
|
|
25331
25862
|
return [];
|
|
25332
25863
|
}
|
|
@@ -25362,8 +25893,8 @@ function auditEntriesInWindow(entries, windowStart, windowEnd) {
|
|
|
25362
25893
|
return result;
|
|
25363
25894
|
}
|
|
25364
25895
|
function buildGeminiSessions(days, allAuditEntries) {
|
|
25365
|
-
const tmpDir =
|
|
25366
|
-
if (!
|
|
25896
|
+
const tmpDir = import_path53.default.join(import_os47.default.homedir(), ".gemini", "tmp");
|
|
25897
|
+
if (!import_fs55.default.existsSync(tmpDir)) return [];
|
|
25367
25898
|
const cutoff = days !== null ? (() => {
|
|
25368
25899
|
const d = /* @__PURE__ */ new Date();
|
|
25369
25900
|
d.setDate(d.getDate() - days);
|
|
@@ -25372,35 +25903,35 @@ function buildGeminiSessions(days, allAuditEntries) {
|
|
|
25372
25903
|
})() : null;
|
|
25373
25904
|
let slugDirs;
|
|
25374
25905
|
try {
|
|
25375
|
-
slugDirs =
|
|
25906
|
+
slugDirs = import_fs55.default.readdirSync(tmpDir);
|
|
25376
25907
|
} catch {
|
|
25377
25908
|
return [];
|
|
25378
25909
|
}
|
|
25379
25910
|
const summaries = [];
|
|
25380
25911
|
for (const slug of slugDirs) {
|
|
25381
|
-
const slugPath =
|
|
25912
|
+
const slugPath = import_path53.default.join(tmpDir, slug);
|
|
25382
25913
|
try {
|
|
25383
|
-
if (!
|
|
25914
|
+
if (!import_fs55.default.statSync(slugPath).isDirectory()) continue;
|
|
25384
25915
|
} catch {
|
|
25385
25916
|
continue;
|
|
25386
25917
|
}
|
|
25387
|
-
let projectRoot =
|
|
25918
|
+
let projectRoot = import_path53.default.join(import_os47.default.homedir(), slug);
|
|
25388
25919
|
try {
|
|
25389
|
-
projectRoot =
|
|
25920
|
+
projectRoot = import_fs55.default.readFileSync(import_path53.default.join(slugPath, ".project_root"), "utf-8").trim();
|
|
25390
25921
|
} catch {
|
|
25391
25922
|
}
|
|
25392
|
-
const chatsDir =
|
|
25393
|
-
if (!
|
|
25923
|
+
const chatsDir = import_path53.default.join(slugPath, "chats");
|
|
25924
|
+
if (!import_fs55.default.existsSync(chatsDir)) continue;
|
|
25394
25925
|
let chatFiles;
|
|
25395
25926
|
try {
|
|
25396
|
-
chatFiles =
|
|
25927
|
+
chatFiles = import_fs55.default.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
|
|
25397
25928
|
} catch {
|
|
25398
25929
|
continue;
|
|
25399
25930
|
}
|
|
25400
25931
|
for (const chatFile of chatFiles) {
|
|
25401
25932
|
let raw;
|
|
25402
25933
|
try {
|
|
25403
|
-
raw =
|
|
25934
|
+
raw = import_fs55.default.readFileSync(import_path53.default.join(chatsDir, chatFile), "utf-8");
|
|
25404
25935
|
} catch {
|
|
25405
25936
|
continue;
|
|
25406
25937
|
}
|
|
@@ -25480,8 +26011,8 @@ function buildGeminiSessions(days, allAuditEntries) {
|
|
|
25480
26011
|
return summaries;
|
|
25481
26012
|
}
|
|
25482
26013
|
function buildCodexSessions(days, allAuditEntries) {
|
|
25483
|
-
const sessionsBase =
|
|
25484
|
-
if (!
|
|
26014
|
+
const sessionsBase = import_path53.default.join(import_os47.default.homedir(), ".codex", "sessions");
|
|
26015
|
+
if (!import_fs55.default.existsSync(sessionsBase)) return [];
|
|
25485
26016
|
const cutoff = days !== null ? (() => {
|
|
25486
26017
|
const d = /* @__PURE__ */ new Date();
|
|
25487
26018
|
d.setDate(d.getDate() - days);
|
|
@@ -25490,29 +26021,29 @@ function buildCodexSessions(days, allAuditEntries) {
|
|
|
25490
26021
|
})() : null;
|
|
25491
26022
|
const jsonlFiles = [];
|
|
25492
26023
|
try {
|
|
25493
|
-
for (const year of
|
|
25494
|
-
const yearPath =
|
|
26024
|
+
for (const year of import_fs55.default.readdirSync(sessionsBase)) {
|
|
26025
|
+
const yearPath = import_path53.default.join(sessionsBase, year);
|
|
25495
26026
|
try {
|
|
25496
|
-
if (!
|
|
26027
|
+
if (!import_fs55.default.statSync(yearPath).isDirectory()) continue;
|
|
25497
26028
|
} catch {
|
|
25498
26029
|
continue;
|
|
25499
26030
|
}
|
|
25500
|
-
for (const month of
|
|
25501
|
-
const monthPath =
|
|
26031
|
+
for (const month of import_fs55.default.readdirSync(yearPath)) {
|
|
26032
|
+
const monthPath = import_path53.default.join(yearPath, month);
|
|
25502
26033
|
try {
|
|
25503
|
-
if (!
|
|
26034
|
+
if (!import_fs55.default.statSync(monthPath).isDirectory()) continue;
|
|
25504
26035
|
} catch {
|
|
25505
26036
|
continue;
|
|
25506
26037
|
}
|
|
25507
|
-
for (const day of
|
|
25508
|
-
const dayPath =
|
|
26038
|
+
for (const day of import_fs55.default.readdirSync(monthPath)) {
|
|
26039
|
+
const dayPath = import_path53.default.join(monthPath, day);
|
|
25509
26040
|
try {
|
|
25510
|
-
if (!
|
|
26041
|
+
if (!import_fs55.default.statSync(dayPath).isDirectory()) continue;
|
|
25511
26042
|
} catch {
|
|
25512
26043
|
continue;
|
|
25513
26044
|
}
|
|
25514
|
-
for (const file of
|
|
25515
|
-
if (file.endsWith(".jsonl")) jsonlFiles.push(
|
|
26045
|
+
for (const file of import_fs55.default.readdirSync(dayPath)) {
|
|
26046
|
+
if (file.endsWith(".jsonl")) jsonlFiles.push(import_path53.default.join(dayPath, file));
|
|
25516
26047
|
}
|
|
25517
26048
|
}
|
|
25518
26049
|
}
|
|
@@ -25524,7 +26055,7 @@ function buildCodexSessions(days, allAuditEntries) {
|
|
|
25524
26055
|
for (const filePath of jsonlFiles) {
|
|
25525
26056
|
let lines;
|
|
25526
26057
|
try {
|
|
25527
|
-
lines =
|
|
26058
|
+
lines = import_fs55.default.readFileSync(filePath, "utf-8").split("\n");
|
|
25528
26059
|
} catch {
|
|
25529
26060
|
continue;
|
|
25530
26061
|
}
|
|
@@ -25610,10 +26141,10 @@ function buildCodexSessions(days, allAuditEntries) {
|
|
|
25610
26141
|
return summaries;
|
|
25611
26142
|
}
|
|
25612
26143
|
function buildSessions(days, historyPath) {
|
|
25613
|
-
const hPath = historyPath ??
|
|
26144
|
+
const hPath = historyPath ?? import_path53.default.join(import_os47.default.homedir(), ".claude", "history.jsonl");
|
|
25614
26145
|
let historyRaw = "";
|
|
25615
26146
|
try {
|
|
25616
|
-
historyRaw =
|
|
26147
|
+
historyRaw = import_fs55.default.readFileSync(hPath, "utf-8");
|
|
25617
26148
|
} catch {
|
|
25618
26149
|
}
|
|
25619
26150
|
const cutoff = days !== null ? (() => {
|
|
@@ -25637,7 +26168,7 @@ function buildSessions(days, historyPath) {
|
|
|
25637
26168
|
const jsonlFile = sessionJsonlPath(entry.project, entry.sessionId);
|
|
25638
26169
|
let sessionLines = [];
|
|
25639
26170
|
try {
|
|
25640
|
-
sessionLines =
|
|
26171
|
+
sessionLines = import_fs55.default.readFileSync(jsonlFile, "utf-8").split("\n");
|
|
25641
26172
|
} catch {
|
|
25642
26173
|
}
|
|
25643
26174
|
const { toolCalls, costUSD, hasSnapshot, modifiedFiles } = parseSessionLines(sessionLines);
|
|
@@ -25723,11 +26254,11 @@ function toolInputSummary(tool, input) {
|
|
|
25723
26254
|
}
|
|
25724
26255
|
function toolColor(tool) {
|
|
25725
26256
|
const t = tool.toLowerCase();
|
|
25726
|
-
if (t === "bash" || t === "execute_bash") return
|
|
25727
|
-
if (t === "write") return
|
|
25728
|
-
if (t === "edit" || t === "notebookedit") return
|
|
25729
|
-
if (t === "read") return
|
|
25730
|
-
return
|
|
26257
|
+
if (t === "bash" || t === "execute_bash") return import_chalk28.default.red;
|
|
26258
|
+
if (t === "write") return import_chalk28.default.green;
|
|
26259
|
+
if (t === "edit" || t === "notebookedit") return import_chalk28.default.yellow;
|
|
26260
|
+
if (t === "read") return import_chalk28.default.cyan;
|
|
26261
|
+
return import_chalk28.default.gray;
|
|
25731
26262
|
}
|
|
25732
26263
|
function barStr2(value, max, width) {
|
|
25733
26264
|
if (max === 0 || width <= 0) return "\u2591".repeat(width);
|
|
@@ -25737,7 +26268,7 @@ function barStr2(value, max, width) {
|
|
|
25737
26268
|
function colorBar2(value, max, width) {
|
|
25738
26269
|
const s = barStr2(value, max, width);
|
|
25739
26270
|
const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
|
|
25740
|
-
return
|
|
26271
|
+
return import_chalk28.default.cyan(s.slice(0, filled)) + import_chalk28.default.dim(s.slice(filled));
|
|
25741
26272
|
}
|
|
25742
26273
|
function renderSummary(summaries) {
|
|
25743
26274
|
const totalTools = summaries.reduce((n, s) => n + s.toolCalls.length, 0);
|
|
@@ -25767,45 +26298,45 @@ function renderSummary(summaries) {
|
|
|
25767
26298
|
}
|
|
25768
26299
|
const topProjects = [...projCosts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
25769
26300
|
const W = 20;
|
|
25770
|
-
console.log(
|
|
26301
|
+
console.log(import_chalk28.default.dim(" " + "\u2500".repeat(70)));
|
|
25771
26302
|
console.log(
|
|
25772
|
-
" " +
|
|
26303
|
+
" " + import_chalk28.default.bold.white(String(summaries.length).padEnd(4)) + import_chalk28.default.dim("sessions ") + import_chalk28.default.bold.yellow(fmtCost3(totalCost).padEnd(10)) + import_chalk28.default.dim("total ") + import_chalk28.default.bold.white(String(totalTools).padEnd(6)) + import_chalk28.default.dim("tool calls ") + import_chalk28.default.bold.white(String(totalFiles)) + import_chalk28.default.dim(" files modified") + (totalBlocked > 0 ? import_chalk28.default.dim(" ") + import_chalk28.default.red.bold(String(totalBlocked)) + import_chalk28.default.dim(" blocked by node9") : "")
|
|
25773
26304
|
);
|
|
25774
26305
|
console.log(
|
|
25775
|
-
" " +
|
|
26306
|
+
" " + import_chalk28.default.dim("avg ") + import_chalk28.default.white(fmtCost3(avgCost).padEnd(10)) + import_chalk28.default.dim("/session ") + import_chalk28.default.green(String(snapshots)) + import_chalk28.default.dim(` of ${summaries.length} sessions had snapshots`)
|
|
25776
26307
|
);
|
|
25777
26308
|
console.log("");
|
|
25778
|
-
console.log(" " +
|
|
26309
|
+
console.log(" " + import_chalk28.default.dim("Tool breakdown:"));
|
|
25779
26310
|
const maxGroup = Math.max(...Object.values(groups));
|
|
25780
26311
|
for (const [label2, count] of Object.entries(groups)) {
|
|
25781
26312
|
if (count === 0) continue;
|
|
25782
26313
|
const pct = totalTools > 0 ? Math.round(count / totalTools * 100) : 0;
|
|
25783
26314
|
console.log(
|
|
25784
|
-
" " + label2.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " +
|
|
26315
|
+
" " + label2.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " + import_chalk28.default.white(String(count).padStart(4)) + import_chalk28.default.dim(` (${String(pct)}%)`)
|
|
25785
26316
|
);
|
|
25786
26317
|
}
|
|
25787
26318
|
console.log("");
|
|
25788
26319
|
if (topProjects.length > 1) {
|
|
25789
|
-
console.log(" " +
|
|
26320
|
+
console.log(" " + import_chalk28.default.dim("Cost by project:"));
|
|
25790
26321
|
const maxProjCost = topProjects[0][1];
|
|
25791
26322
|
for (const [proj, cost] of topProjects) {
|
|
25792
26323
|
console.log(
|
|
25793
|
-
" " + proj.slice(0, 28).padEnd(28) + " " + colorBar2(cost, maxProjCost, W) + " " +
|
|
26324
|
+
" " + proj.slice(0, 28).padEnd(28) + " " + colorBar2(cost, maxProjCost, W) + " " + import_chalk28.default.yellow(fmtCost3(cost))
|
|
25794
26325
|
);
|
|
25795
26326
|
}
|
|
25796
26327
|
console.log("");
|
|
25797
26328
|
}
|
|
25798
|
-
console.log(
|
|
26329
|
+
console.log(import_chalk28.default.dim(" " + "\u2500".repeat(70)));
|
|
25799
26330
|
console.log("");
|
|
25800
26331
|
}
|
|
25801
26332
|
function renderList(summaries, totalCost) {
|
|
25802
26333
|
if (summaries.length === 0) {
|
|
25803
|
-
console.log(
|
|
26334
|
+
console.log(import_chalk28.default.yellow(" No sessions found in the requested range.\n"));
|
|
25804
26335
|
return;
|
|
25805
26336
|
}
|
|
25806
|
-
const totalLabel = totalCost > 0 ?
|
|
26337
|
+
const totalLabel = totalCost > 0 ? import_chalk28.default.dim(" ~" + fmtCost3(totalCost) + " total") : "";
|
|
25807
26338
|
console.log(
|
|
25808
|
-
" " +
|
|
26339
|
+
" " + import_chalk28.default.white(String(summaries.length)) + import_chalk28.default.dim(` session${summaries.length !== 1 ? "s" : ""}`) + totalLabel
|
|
25809
26340
|
);
|
|
25810
26341
|
console.log("");
|
|
25811
26342
|
let lastGroup = "";
|
|
@@ -25813,51 +26344,51 @@ function renderList(summaries, totalCost) {
|
|
|
25813
26344
|
const activeDate = fmtDate2(s.lastActiveTime);
|
|
25814
26345
|
const group = activeDate + " " + s.projectLabel;
|
|
25815
26346
|
if (group !== lastGroup) {
|
|
25816
|
-
console.log(
|
|
26347
|
+
console.log(import_chalk28.default.dim(" \u2500\u2500\u2500 ") + import_chalk28.default.bold(activeDate) + import_chalk28.default.dim(" " + s.projectLabel));
|
|
25817
26348
|
lastGroup = group;
|
|
25818
26349
|
}
|
|
25819
26350
|
const startDate = fmtDate2(s.startTime);
|
|
25820
|
-
const dateRange = startDate !== activeDate ?
|
|
25821
|
-
const timeStr =
|
|
25822
|
-
const prompt =
|
|
25823
|
-
const tools = s.toolCalls.length > 0 ?
|
|
25824
|
-
const cost = s.costUSD > 0 ?
|
|
25825
|
-
const blocked = s.blockedCalls.length > 0 ?
|
|
25826
|
-
const snap = s.hasSnapshot ?
|
|
25827
|
-
const agentBadge =
|
|
26351
|
+
const dateRange = startDate !== activeDate ? import_chalk28.default.dim(" (" + startDate + " \u2192 " + activeDate + ")") : "";
|
|
26352
|
+
const timeStr = import_chalk28.default.dim(fmtTime(s.startTime));
|
|
26353
|
+
const prompt = import_chalk28.default.white(truncate(s.firstPrompt.replace(/\n/g, " "), 50).padEnd(50));
|
|
26354
|
+
const tools = s.toolCalls.length > 0 ? import_chalk28.default.dim(String(s.toolCalls.length).padStart(3) + " tools") : import_chalk28.default.dim(" 0 tools");
|
|
26355
|
+
const cost = s.costUSD > 0 ? import_chalk28.default.dim(" " + fmtCost3(s.costUSD).padEnd(8)) : " ";
|
|
26356
|
+
const blocked = s.blockedCalls.length > 0 ? import_chalk28.default.red(" \u{1F6D1} " + String(s.blockedCalls.length)) : "";
|
|
26357
|
+
const snap = s.hasSnapshot ? import_chalk28.default.green(" \u{1F4F8}") : "";
|
|
26358
|
+
const agentBadge = import_chalk28.default[agentColorName(s.agent ?? "claude")](
|
|
25828
26359
|
" " + agentBadgeText(s.agent ?? "claude", 0)
|
|
25829
26360
|
);
|
|
25830
|
-
const sid =
|
|
26361
|
+
const sid = import_chalk28.default.dim(" " + s.sessionId.slice(0, 8));
|
|
25831
26362
|
console.log(
|
|
25832
26363
|
` ${timeStr} ${prompt} ${tools}${cost}${blocked}${snap}${agentBadge}${sid}${dateRange}`
|
|
25833
26364
|
);
|
|
25834
26365
|
}
|
|
25835
26366
|
console.log("");
|
|
25836
26367
|
console.log(
|
|
25837
|
-
|
|
26368
|
+
import_chalk28.default.dim(" Run") + " " + import_chalk28.default.cyan("node9 sessions --detail <session-id>") + import_chalk28.default.dim(" for full tool trace.")
|
|
25838
26369
|
);
|
|
25839
26370
|
console.log("");
|
|
25840
26371
|
}
|
|
25841
26372
|
function renderDetail(s) {
|
|
25842
26373
|
console.log("");
|
|
25843
|
-
console.log(
|
|
26374
|
+
console.log(import_chalk28.default.bold(" Session ") + import_chalk28.default.dim(s.sessionId));
|
|
25844
26375
|
console.log(
|
|
25845
|
-
|
|
26376
|
+
import_chalk28.default.bold(" Prompt ") + import_chalk28.default.white(s.firstPrompt.replace(/\n/g, " ").slice(0, 120))
|
|
25846
26377
|
);
|
|
25847
|
-
console.log(
|
|
26378
|
+
console.log(import_chalk28.default.bold(" Project ") + import_chalk28.default.white(s.projectLabel));
|
|
25848
26379
|
if (s.agent) {
|
|
25849
|
-
const agentLabel2 =
|
|
25850
|
-
console.log(
|
|
26380
|
+
const agentLabel2 = import_chalk28.default[agentColorName(s.agent)](agentDisplayName(s.agent));
|
|
26381
|
+
console.log(import_chalk28.default.bold(" Agent ") + agentLabel2);
|
|
25851
26382
|
}
|
|
25852
|
-
console.log(
|
|
26383
|
+
console.log(import_chalk28.default.bold(" When ") + import_chalk28.default.white(fmtDateTime(s.startTime)));
|
|
25853
26384
|
if (s.costUSD > 0)
|
|
25854
|
-
console.log(
|
|
26385
|
+
console.log(import_chalk28.default.bold(" Cost ") + import_chalk28.default.yellow("~" + fmtCost3(s.costUSD)));
|
|
25855
26386
|
console.log(
|
|
25856
|
-
|
|
26387
|
+
import_chalk28.default.bold(" Snapshot ") + (s.hasSnapshot ? import_chalk28.default.green("\u2713 taken") : import_chalk28.default.dim("none"))
|
|
25857
26388
|
);
|
|
25858
26389
|
console.log("");
|
|
25859
26390
|
if (s.toolCalls.length === 0 && s.blockedCalls.length === 0) {
|
|
25860
|
-
console.log(
|
|
26391
|
+
console.log(import_chalk28.default.dim(" No tool calls recorded.\n"));
|
|
25861
26392
|
return;
|
|
25862
26393
|
}
|
|
25863
26394
|
const timeline = [
|
|
@@ -25870,32 +26401,32 @@ function renderDetail(s) {
|
|
|
25870
26401
|
});
|
|
25871
26402
|
const headerParts = [`Tool calls (${s.toolCalls.length})`];
|
|
25872
26403
|
if (s.blockedCalls.length > 0)
|
|
25873
|
-
headerParts.push(
|
|
25874
|
-
console.log(
|
|
26404
|
+
headerParts.push(import_chalk28.default.red(`${s.blockedCalls.length} blocked by node9`));
|
|
26405
|
+
console.log(import_chalk28.default.bold(" " + headerParts.join(" \xB7 ")));
|
|
25875
26406
|
console.log("");
|
|
25876
26407
|
for (const entry of timeline) {
|
|
25877
26408
|
if (entry.kind === "tool") {
|
|
25878
26409
|
const tc = entry.tc;
|
|
25879
26410
|
const colorFn = toolColor(tc.tool);
|
|
25880
26411
|
const toolPad = colorFn(tc.tool.padEnd(16));
|
|
25881
|
-
const detail =
|
|
25882
|
-
const ts = tc.timestamp ?
|
|
26412
|
+
const detail = import_chalk28.default.gray(truncate(toolInputSummary(tc.tool, tc.input), 70));
|
|
26413
|
+
const ts = tc.timestamp ? import_chalk28.default.dim(fmtTime(tc.timestamp) + " ") : " ";
|
|
25883
26414
|
console.log(` ${ts}${toolPad} ${detail}`);
|
|
25884
26415
|
} else {
|
|
25885
26416
|
const bc = entry.bc;
|
|
25886
|
-
const ts = bc.timestamp ?
|
|
25887
|
-
const label2 =
|
|
25888
|
-
const toolName =
|
|
25889
|
-
const argsSummary = bc.args ?
|
|
25890
|
-
const reason = bc.checkedBy ?
|
|
26417
|
+
const ts = bc.timestamp ? import_chalk28.default.dim(fmtTime(bc.timestamp) + " ") : " ";
|
|
26418
|
+
const label2 = import_chalk28.default.red("\u{1F6D1} BLOCKED".padEnd(16));
|
|
26419
|
+
const toolName = import_chalk28.default.red(bc.tool.padEnd(10));
|
|
26420
|
+
const argsSummary = bc.args ? import_chalk28.default.gray(truncate(toolInputSummary(bc.tool, bc.args), 40)) : import_chalk28.default.dim("[args not logged]");
|
|
26421
|
+
const reason = bc.checkedBy ? import_chalk28.default.dim(" \u2190 " + bc.checkedBy) : "";
|
|
25891
26422
|
console.log(` ${ts}${label2} ${toolName} ${argsSummary}${reason}`);
|
|
25892
26423
|
}
|
|
25893
26424
|
}
|
|
25894
26425
|
console.log("");
|
|
25895
26426
|
if (s.modifiedFiles.length > 0) {
|
|
25896
|
-
console.log(
|
|
26427
|
+
console.log(import_chalk28.default.bold(` Files modified (${s.modifiedFiles.length}):`));
|
|
25897
26428
|
for (const f of s.modifiedFiles) {
|
|
25898
|
-
console.log(" " +
|
|
26429
|
+
console.log(" " + import_chalk28.default.yellow(f));
|
|
25899
26430
|
}
|
|
25900
26431
|
console.log("");
|
|
25901
26432
|
}
|
|
@@ -25903,13 +26434,13 @@ function renderDetail(s) {
|
|
|
25903
26434
|
function registerSessionsCommand(program2) {
|
|
25904
26435
|
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) => {
|
|
25905
26436
|
console.log("");
|
|
25906
|
-
console.log(
|
|
26437
|
+
console.log(import_chalk28.default.cyan.bold("\u{1F4CB} node9 sessions") + import_chalk28.default.dim(" \u2014 what your AI agent did"));
|
|
25907
26438
|
console.log("");
|
|
25908
26439
|
const days = options.detail || options.all ? null : Math.max(1, parseInt(options.days, 10) || 7);
|
|
25909
26440
|
const rangeLabel = options.detail ? "all time" : options.all ? "all time" : `last ${String(days)} days`;
|
|
25910
|
-
console.log(
|
|
26441
|
+
console.log(import_chalk28.default.dim(" " + rangeLabel));
|
|
25911
26442
|
console.log("");
|
|
25912
|
-
process.stdout.write(
|
|
26443
|
+
process.stdout.write(import_chalk28.default.dim(" Loading\u2026"));
|
|
25913
26444
|
const summaries = buildSessions(days);
|
|
25914
26445
|
if (process.stdout.isTTY) {
|
|
25915
26446
|
process.stdout.clearLine(0);
|
|
@@ -25922,8 +26453,8 @@ function registerSessionsCommand(program2) {
|
|
|
25922
26453
|
(s) => s.sessionId === options.detail || s.sessionId.startsWith(options.detail)
|
|
25923
26454
|
);
|
|
25924
26455
|
if (!target) {
|
|
25925
|
-
console.log(
|
|
25926
|
-
console.log(
|
|
26456
|
+
console.log(import_chalk28.default.red(` Session not found: ${options.detail}`));
|
|
26457
|
+
console.log(import_chalk28.default.dim(" Run `node9 sessions` to list recent sessions.\n"));
|
|
25927
26458
|
return;
|
|
25928
26459
|
}
|
|
25929
26460
|
renderDetail(target);
|
|
@@ -25936,7 +26467,7 @@ function registerSessionsCommand(program2) {
|
|
|
25936
26467
|
}
|
|
25937
26468
|
|
|
25938
26469
|
// src/cli/commands/session-taint.ts
|
|
25939
|
-
var
|
|
26470
|
+
var import_chalk29 = __toESM(require("chalk"));
|
|
25940
26471
|
init_daemon();
|
|
25941
26472
|
function resolveSessionId(records, query) {
|
|
25942
26473
|
const exact = records.find((r) => r.sessionId === query);
|
|
@@ -25963,22 +26494,22 @@ function registerSessionTaintCommand(program2) {
|
|
|
25963
26494
|
const records = await listSessionTaints();
|
|
25964
26495
|
console.log("");
|
|
25965
26496
|
if (records.length === 0) {
|
|
25966
|
-
console.log(
|
|
25967
|
-
console.log(
|
|
26497
|
+
console.log(import_chalk29.default.dim(" No tainted sessions."));
|
|
26498
|
+
console.log(import_chalk29.default.dim(" (Taint lives in the daemon \u2014 a stopped daemon has none.)") + "\n");
|
|
25968
26499
|
return;
|
|
25969
26500
|
}
|
|
25970
26501
|
console.log(
|
|
25971
|
-
" " +
|
|
26502
|
+
" " + import_chalk29.default.bold(String(records.length)) + import_chalk29.default.dim(` tainted session${records.length !== 1 ? "s" : ""}`)
|
|
25972
26503
|
);
|
|
25973
26504
|
console.log("");
|
|
25974
26505
|
for (const r of records) {
|
|
25975
26506
|
console.log(
|
|
25976
|
-
" " +
|
|
26507
|
+
" " + import_chalk29.default.yellow(r.sessionId.slice(0, 8).padEnd(10)) + import_chalk29.default.red(r.source) + sourceGap(r.source) + import_chalk29.default.dim("clears in " + fmtRemaining(r.expiresAt))
|
|
25977
26508
|
);
|
|
25978
26509
|
}
|
|
25979
26510
|
console.log("");
|
|
25980
26511
|
console.log(
|
|
25981
|
-
|
|
26512
|
+
import_chalk29.default.dim(" Run ") + import_chalk29.default.cyan("node9 session-taint clear <id>") + import_chalk29.default.dim(" to release one, or ") + import_chalk29.default.cyan("--all") + import_chalk29.default.dim(" for every session.") + "\n"
|
|
25982
26513
|
);
|
|
25983
26514
|
});
|
|
25984
26515
|
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) => {
|
|
@@ -25986,32 +26517,32 @@ function registerSessionTaintCommand(program2) {
|
|
|
25986
26517
|
if (opts.all) {
|
|
25987
26518
|
const res2 = await clearSessionTaint({ all: true });
|
|
25988
26519
|
if (res2.daemonUnavailable) {
|
|
25989
|
-
console.log(
|
|
26520
|
+
console.log(import_chalk29.default.dim(" node9 daemon not running \u2014 no active taints to clear.") + "\n");
|
|
25990
26521
|
return;
|
|
25991
26522
|
}
|
|
25992
26523
|
console.log(
|
|
25993
|
-
|
|
26524
|
+
import_chalk29.default.green(" \u2713 ") + `Cleared ${import_chalk29.default.bold(String(res2.cleared))} session taint${res2.cleared !== 1 ? "s" : ""}.
|
|
25994
26525
|
`
|
|
25995
26526
|
);
|
|
25996
26527
|
return;
|
|
25997
26528
|
}
|
|
25998
26529
|
if (!sessionId) {
|
|
25999
|
-
console.log(
|
|
26000
|
-
console.log(
|
|
26530
|
+
console.log(import_chalk29.default.red(" Provide a session id or --all."));
|
|
26531
|
+
console.log(import_chalk29.default.dim(" Run `node9 session-taint list` to see tainted sessions.") + "\n");
|
|
26001
26532
|
return;
|
|
26002
26533
|
}
|
|
26003
26534
|
const records = await listSessionTaints();
|
|
26004
26535
|
if (records.length === 0) {
|
|
26005
|
-
console.log(
|
|
26536
|
+
console.log(import_chalk29.default.dim(" No tainted sessions \u2014 nothing to clear.") + "\n");
|
|
26006
26537
|
return;
|
|
26007
26538
|
}
|
|
26008
26539
|
const resolved = resolveSessionId(records, sessionId);
|
|
26009
26540
|
if ("error" in resolved) {
|
|
26010
26541
|
if (resolved.error === "not-found") {
|
|
26011
|
-
console.log(
|
|
26542
|
+
console.log(import_chalk29.default.red(` No tainted session matches "${sessionId}".`));
|
|
26012
26543
|
} else {
|
|
26013
|
-
console.log(
|
|
26014
|
-
for (const m of resolved.matches) console.log(
|
|
26544
|
+
console.log(import_chalk29.default.red(` "${sessionId}" is ambiguous \u2014 matches:`));
|
|
26545
|
+
for (const m of resolved.matches) console.log(import_chalk29.default.dim(" " + m));
|
|
26015
26546
|
}
|
|
26016
26547
|
console.log("");
|
|
26017
26548
|
return;
|
|
@@ -26019,24 +26550,24 @@ function registerSessionTaintCommand(program2) {
|
|
|
26019
26550
|
const res = await clearSessionTaint({ sessionId: resolved.record.sessionId });
|
|
26020
26551
|
if (res.cleared > 0) {
|
|
26021
26552
|
console.log(
|
|
26022
|
-
|
|
26553
|
+
import_chalk29.default.green(" \u2713 ") + `Cleared taint for ${import_chalk29.default.yellow(resolved.record.sessionId.slice(0, 8))} ` + import_chalk29.default.dim(`(was flagged by ${resolved.record.source}).`) + "\n"
|
|
26023
26554
|
);
|
|
26024
26555
|
} else {
|
|
26025
26556
|
console.log(
|
|
26026
|
-
|
|
26557
|
+
import_chalk29.default.dim(` Session ${resolved.record.sessionId.slice(0, 8)} was already clear.`) + "\n"
|
|
26027
26558
|
);
|
|
26028
26559
|
}
|
|
26029
26560
|
});
|
|
26030
26561
|
}
|
|
26031
26562
|
|
|
26032
26563
|
// src/cli/commands/skill-pin.ts
|
|
26033
|
-
var
|
|
26034
|
-
var
|
|
26035
|
-
var
|
|
26036
|
-
var
|
|
26564
|
+
var import_chalk30 = __toESM(require("chalk"));
|
|
26565
|
+
var import_fs56 = __toESM(require("fs"));
|
|
26566
|
+
var import_os48 = __toESM(require("os"));
|
|
26567
|
+
var import_path54 = __toESM(require("path"));
|
|
26037
26568
|
function wipeSkillSessions() {
|
|
26038
26569
|
try {
|
|
26039
|
-
|
|
26570
|
+
import_fs56.default.rmSync(import_path54.default.join(import_os48.default.homedir(), ".node9", "skill-sessions"), {
|
|
26040
26571
|
recursive: true,
|
|
26041
26572
|
force: true
|
|
26042
26573
|
});
|
|
@@ -26050,29 +26581,29 @@ function registerSkillPinCommand(program2) {
|
|
|
26050
26581
|
const result = readSkillPinsSafe();
|
|
26051
26582
|
if (!result.ok) {
|
|
26052
26583
|
if (result.reason === "missing") {
|
|
26053
|
-
console.log(
|
|
26584
|
+
console.log(import_chalk30.default.gray("\nNo skill roots are pinned yet."));
|
|
26054
26585
|
console.log(
|
|
26055
|
-
|
|
26586
|
+
import_chalk30.default.gray("Pins are created automatically on the first tool call of each session.\n")
|
|
26056
26587
|
);
|
|
26057
26588
|
return;
|
|
26058
26589
|
}
|
|
26059
|
-
console.error(
|
|
26590
|
+
console.error(import_chalk30.default.red(`
|
|
26060
26591
|
\u274C Pin file is corrupt: ${result.detail}`));
|
|
26061
|
-
console.error(
|
|
26592
|
+
console.error(import_chalk30.default.yellow(" Run: node9 skill pin reset\n"));
|
|
26062
26593
|
process.exit(1);
|
|
26063
26594
|
}
|
|
26064
26595
|
const entries = Object.entries(result.pins.roots);
|
|
26065
26596
|
if (entries.length === 0) {
|
|
26066
|
-
console.log(
|
|
26597
|
+
console.log(import_chalk30.default.gray("\nNo skill roots are pinned yet.\n"));
|
|
26067
26598
|
return;
|
|
26068
26599
|
}
|
|
26069
|
-
console.log(
|
|
26600
|
+
console.log(import_chalk30.default.bold("\n\u{1F512} Pinned Skill Roots\n"));
|
|
26070
26601
|
for (const [key, entry] of entries) {
|
|
26071
|
-
const missing = entry.exists ? "" :
|
|
26072
|
-
console.log(` ${
|
|
26602
|
+
const missing = entry.exists ? "" : import_chalk30.default.yellow(" (not present at pin time)");
|
|
26603
|
+
console.log(` ${import_chalk30.default.cyan(key)} ${import_chalk30.default.gray(entry.rootPath)}${missing}`);
|
|
26073
26604
|
console.log(` Files (${entry.fileCount})`);
|
|
26074
|
-
console.log(` Hash: ${
|
|
26075
|
-
console.log(` Pinned: ${
|
|
26605
|
+
console.log(` Hash: ${import_chalk30.default.gray(entry.contentHash.slice(0, 16))}...`);
|
|
26606
|
+
console.log(` Pinned: ${import_chalk30.default.gray(entry.pinnedAt)}
|
|
26076
26607
|
`);
|
|
26077
26608
|
}
|
|
26078
26609
|
});
|
|
@@ -26081,52 +26612,52 @@ function registerSkillPinCommand(program2) {
|
|
|
26081
26612
|
try {
|
|
26082
26613
|
pins = readSkillPins();
|
|
26083
26614
|
} catch {
|
|
26084
|
-
console.error(
|
|
26085
|
-
console.error(
|
|
26615
|
+
console.error(import_chalk30.default.red("\n\u274C Pin file is corrupt."));
|
|
26616
|
+
console.error(import_chalk30.default.yellow(" Run: node9 skill pin reset\n"));
|
|
26086
26617
|
process.exit(1);
|
|
26087
26618
|
}
|
|
26088
26619
|
if (!pins.roots[rootKey]) {
|
|
26089
|
-
console.error(
|
|
26620
|
+
console.error(import_chalk30.default.red(`
|
|
26090
26621
|
\u274C No pin found for root key "${rootKey}"
|
|
26091
26622
|
`));
|
|
26092
|
-
console.error(`Run ${
|
|
26623
|
+
console.error(`Run ${import_chalk30.default.cyan("node9 skill pin list")} to see pinned roots.
|
|
26093
26624
|
`);
|
|
26094
26625
|
process.exit(1);
|
|
26095
26626
|
}
|
|
26096
26627
|
const rootPath = pins.roots[rootKey].rootPath;
|
|
26097
26628
|
removePin2(rootKey);
|
|
26098
26629
|
wipeSkillSessions();
|
|
26099
|
-
console.log(
|
|
26100
|
-
\u{1F513} Pin removed for ${
|
|
26101
|
-
console.log(
|
|
26102
|
-
console.log(
|
|
26630
|
+
console.log(import_chalk30.default.green(`
|
|
26631
|
+
\u{1F513} Pin removed for ${import_chalk30.default.cyan(rootKey)}`));
|
|
26632
|
+
console.log(import_chalk30.default.gray(` ${rootPath}`));
|
|
26633
|
+
console.log(import_chalk30.default.gray(" Next session will re-pin with current state.\n"));
|
|
26103
26634
|
});
|
|
26104
26635
|
pinSubCmd.command("reset").description("Clear all skill pins and wipe session verification flags").action(() => {
|
|
26105
26636
|
const result = readSkillPinsSafe();
|
|
26106
26637
|
if (!result.ok && result.reason === "missing") {
|
|
26107
26638
|
wipeSkillSessions();
|
|
26108
|
-
console.log(
|
|
26639
|
+
console.log(import_chalk30.default.gray("\nNo pins to clear.\n"));
|
|
26109
26640
|
return;
|
|
26110
26641
|
}
|
|
26111
26642
|
const count = result.ok ? Object.keys(result.pins.roots).length : "?";
|
|
26112
26643
|
clearAllPins2();
|
|
26113
26644
|
wipeSkillSessions();
|
|
26114
|
-
console.log(
|
|
26645
|
+
console.log(import_chalk30.default.green(`
|
|
26115
26646
|
\u{1F513} Cleared ${count} skill pin(s).`));
|
|
26116
|
-
console.log(
|
|
26647
|
+
console.log(import_chalk30.default.gray(" Next session will re-pin with current state.\n"));
|
|
26117
26648
|
});
|
|
26118
26649
|
}
|
|
26119
26650
|
|
|
26120
26651
|
// src/cli/commands/decisions.ts
|
|
26121
|
-
var
|
|
26122
|
-
var
|
|
26123
|
-
var
|
|
26124
|
-
var
|
|
26125
|
-
var DECISIONS_FILE2 =
|
|
26652
|
+
var import_fs57 = __toESM(require("fs"));
|
|
26653
|
+
var import_os49 = __toESM(require("os"));
|
|
26654
|
+
var import_path55 = __toESM(require("path"));
|
|
26655
|
+
var import_chalk31 = __toESM(require("chalk"));
|
|
26656
|
+
var DECISIONS_FILE2 = import_path55.default.join(import_os49.default.homedir(), ".node9", "decisions.json");
|
|
26126
26657
|
function readDecisions() {
|
|
26127
26658
|
try {
|
|
26128
|
-
if (!
|
|
26129
|
-
const raw =
|
|
26659
|
+
if (!import_fs57.default.existsSync(DECISIONS_FILE2)) return {};
|
|
26660
|
+
const raw = import_fs57.default.readFileSync(DECISIONS_FILE2, "utf-8");
|
|
26130
26661
|
const parsed = JSON.parse(raw);
|
|
26131
26662
|
const out = {};
|
|
26132
26663
|
for (const [k, v] of Object.entries(parsed)) {
|
|
@@ -26138,11 +26669,11 @@ function readDecisions() {
|
|
|
26138
26669
|
}
|
|
26139
26670
|
}
|
|
26140
26671
|
function writeDecisions(d) {
|
|
26141
|
-
const dir =
|
|
26142
|
-
if (!
|
|
26672
|
+
const dir = import_path55.default.dirname(DECISIONS_FILE2);
|
|
26673
|
+
if (!import_fs57.default.existsSync(dir)) import_fs57.default.mkdirSync(dir, { recursive: true });
|
|
26143
26674
|
const tmp = `${DECISIONS_FILE2}.${process.pid}.tmp`;
|
|
26144
|
-
|
|
26145
|
-
|
|
26675
|
+
import_fs57.default.writeFileSync(tmp, JSON.stringify(d, null, 2));
|
|
26676
|
+
import_fs57.default.renameSync(tmp, DECISIONS_FILE2);
|
|
26146
26677
|
}
|
|
26147
26678
|
function registerDecisionsCommand(program2) {
|
|
26148
26679
|
const cmd = program2.command("decisions").description('Manage persistent "Always Allow" / "Always Deny" tool decisions');
|
|
@@ -26150,67 +26681,67 @@ function registerDecisionsCommand(program2) {
|
|
|
26150
26681
|
const decisions = readDecisions();
|
|
26151
26682
|
const entries = Object.entries(decisions);
|
|
26152
26683
|
if (entries.length === 0) {
|
|
26153
|
-
console.log(
|
|
26684
|
+
console.log(import_chalk31.default.gray(" No persistent decisions stored."));
|
|
26154
26685
|
console.log(
|
|
26155
|
-
|
|
26156
|
-
`) +
|
|
26686
|
+
import_chalk31.default.gray(` File: ${DECISIONS_FILE2}
|
|
26687
|
+
`) + import_chalk31.default.gray(' Decisions are written when you click "Always Allow" or')
|
|
26157
26688
|
);
|
|
26158
|
-
console.log(
|
|
26689
|
+
console.log(import_chalk31.default.gray(' "Always Deny" in node9 tail or the native popup.'));
|
|
26159
26690
|
return;
|
|
26160
26691
|
}
|
|
26161
|
-
console.log(
|
|
26692
|
+
console.log(import_chalk31.default.bold(`
|
|
26162
26693
|
Persistent decisions (${entries.length})
|
|
26163
26694
|
`));
|
|
26164
26695
|
const w = Math.max(...entries.map(([k]) => k.length));
|
|
26165
26696
|
for (const [tool, verdict] of entries.sort()) {
|
|
26166
|
-
const colored = verdict === "allow" ?
|
|
26697
|
+
const colored = verdict === "allow" ? import_chalk31.default.green(verdict) : import_chalk31.default.red(verdict);
|
|
26167
26698
|
console.log(` ${tool.padEnd(w)} ${colored}`);
|
|
26168
26699
|
}
|
|
26169
26700
|
console.log(
|
|
26170
|
-
|
|
26701
|
+
import_chalk31.default.gray(`
|
|
26171
26702
|
Stored in ${DECISIONS_FILE2}
|
|
26172
|
-
`) +
|
|
26703
|
+
`) + import_chalk31.default.gray(" Run `node9 decisions clear <tool>` to remove an entry.")
|
|
26173
26704
|
);
|
|
26174
26705
|
});
|
|
26175
26706
|
cmd.command("clear <toolName>").description("Remove a persistent decision for one tool").action((toolName) => {
|
|
26176
26707
|
const decisions = readDecisions();
|
|
26177
26708
|
if (!(toolName in decisions)) {
|
|
26178
|
-
console.log(
|
|
26709
|
+
console.log(import_chalk31.default.yellow(` No persistent decision for "${toolName}". Nothing to clear.`));
|
|
26179
26710
|
process.exitCode = 1;
|
|
26180
26711
|
return;
|
|
26181
26712
|
}
|
|
26182
26713
|
delete decisions[toolName];
|
|
26183
26714
|
writeDecisions(decisions);
|
|
26184
|
-
console.log(
|
|
26715
|
+
console.log(import_chalk31.default.green(` \u2713 Cleared persistent decision for "${toolName}".`));
|
|
26185
26716
|
});
|
|
26186
26717
|
cmd.command("clear-all").description("Remove every persistent decision (irreversible)").action(() => {
|
|
26187
26718
|
const decisions = readDecisions();
|
|
26188
26719
|
const count = Object.keys(decisions).length;
|
|
26189
26720
|
if (count === 0) {
|
|
26190
|
-
console.log(
|
|
26721
|
+
console.log(import_chalk31.default.gray(" Nothing to clear \u2014 no persistent decisions stored."));
|
|
26191
26722
|
return;
|
|
26192
26723
|
}
|
|
26193
26724
|
writeDecisions({});
|
|
26194
26725
|
console.log(
|
|
26195
|
-
|
|
26726
|
+
import_chalk31.default.green(` \u2713 Cleared ${count} persistent decision${count === 1 ? "" : "s"}.`)
|
|
26196
26727
|
);
|
|
26197
26728
|
});
|
|
26198
26729
|
}
|
|
26199
26730
|
|
|
26200
26731
|
// src/cli/commands/dlp.ts
|
|
26201
|
-
var
|
|
26202
|
-
var
|
|
26203
|
-
var
|
|
26204
|
-
var
|
|
26205
|
-
var AUDIT_LOG =
|
|
26206
|
-
var RESOLVED_FILE =
|
|
26732
|
+
var import_chalk32 = __toESM(require("chalk"));
|
|
26733
|
+
var import_fs58 = __toESM(require("fs"));
|
|
26734
|
+
var import_path56 = __toESM(require("path"));
|
|
26735
|
+
var import_os50 = __toESM(require("os"));
|
|
26736
|
+
var AUDIT_LOG = import_path56.default.join(import_os50.default.homedir(), ".node9", "audit.log");
|
|
26737
|
+
var RESOLVED_FILE = import_path56.default.join(import_os50.default.homedir(), ".node9", "dlp-resolved.json");
|
|
26207
26738
|
var ANSI_RE = /\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g;
|
|
26208
26739
|
function stripAnsi(s) {
|
|
26209
26740
|
return s.replace(ANSI_RE, "");
|
|
26210
26741
|
}
|
|
26211
26742
|
function loadResolved() {
|
|
26212
26743
|
try {
|
|
26213
|
-
const raw = JSON.parse(
|
|
26744
|
+
const raw = JSON.parse(import_fs58.default.readFileSync(RESOLVED_FILE, "utf-8"));
|
|
26214
26745
|
return new Set(raw);
|
|
26215
26746
|
} catch {
|
|
26216
26747
|
return /* @__PURE__ */ new Set();
|
|
@@ -26218,13 +26749,13 @@ function loadResolved() {
|
|
|
26218
26749
|
}
|
|
26219
26750
|
function saveResolved(resolved) {
|
|
26220
26751
|
try {
|
|
26221
|
-
|
|
26752
|
+
import_fs58.default.writeFileSync(RESOLVED_FILE, JSON.stringify([...resolved], null, 2), { mode: 384 });
|
|
26222
26753
|
} catch {
|
|
26223
26754
|
}
|
|
26224
26755
|
}
|
|
26225
26756
|
function loadDlpFindings() {
|
|
26226
|
-
if (!
|
|
26227
|
-
return
|
|
26757
|
+
if (!import_fs58.default.existsSync(AUDIT_LOG)) return [];
|
|
26758
|
+
return import_fs58.default.readFileSync(AUDIT_LOG, "utf-8").split("\n").flatMap((line) => {
|
|
26228
26759
|
if (!line.trim()) return [];
|
|
26229
26760
|
try {
|
|
26230
26761
|
const e = JSON.parse(line);
|
|
@@ -26253,14 +26784,14 @@ function registerDlpCommand(program2) {
|
|
|
26253
26784
|
cmd.command("resolve").description("Mark all current DLP findings as resolved").action(() => {
|
|
26254
26785
|
const findings = loadDlpFindings();
|
|
26255
26786
|
if (findings.length === 0) {
|
|
26256
|
-
console.log(
|
|
26787
|
+
console.log(import_chalk32.default.green("\n \u2705 No response-DLP findings to resolve.\n"));
|
|
26257
26788
|
return;
|
|
26258
26789
|
}
|
|
26259
26790
|
const resolved = loadResolved();
|
|
26260
26791
|
for (const e of findings) resolved.add(entryKey2(e));
|
|
26261
26792
|
saveResolved(resolved);
|
|
26262
26793
|
console.log(
|
|
26263
|
-
|
|
26794
|
+
import_chalk32.default.green(
|
|
26264
26795
|
`
|
|
26265
26796
|
\u2705 ${findings.length} finding${findings.length !== 1 ? "s" : ""} marked as resolved.
|
|
26266
26797
|
`
|
|
@@ -26274,63 +26805,63 @@ function registerDlpCommand(program2) {
|
|
|
26274
26805
|
const resolvedCount = findings.length - open.length;
|
|
26275
26806
|
console.log("");
|
|
26276
26807
|
console.log(
|
|
26277
|
-
|
|
26808
|
+
import_chalk32.default.bold.cyan("\u{1F510} node9 dlp") + import_chalk32.default.dim(" \u2014 secrets found in Claude response text")
|
|
26278
26809
|
);
|
|
26279
26810
|
console.log("");
|
|
26280
26811
|
if (open.length === 0) {
|
|
26281
26812
|
if (resolvedCount > 0) {
|
|
26282
|
-
console.log(
|
|
26813
|
+
console.log(import_chalk32.default.green(` \u2705 No open findings \xB7 ${resolvedCount} previously resolved`));
|
|
26283
26814
|
} else {
|
|
26284
26815
|
console.log(
|
|
26285
|
-
|
|
26816
|
+
import_chalk32.default.green(" \u2705 No findings \u2014 Claude has not leaked secrets in response text")
|
|
26286
26817
|
);
|
|
26287
26818
|
}
|
|
26288
26819
|
console.log("");
|
|
26289
26820
|
return;
|
|
26290
26821
|
}
|
|
26291
26822
|
console.log(
|
|
26292
|
-
|
|
26823
|
+
import_chalk32.default.bgRed.white.bold(` \u26A0\uFE0F ${open.length} open finding${open.length !== 1 ? "s" : ""} `) + import_chalk32.default.dim(resolvedCount > 0 ? ` (${resolvedCount} resolved)` : "")
|
|
26293
26824
|
);
|
|
26294
26825
|
console.log("");
|
|
26295
26826
|
console.log(
|
|
26296
|
-
|
|
26827
|
+
import_chalk32.default.dim(" These secrets were included in Claude's response text \u2014 NOT blocked.")
|
|
26297
26828
|
);
|
|
26298
|
-
console.log(
|
|
26829
|
+
console.log(import_chalk32.default.dim(" Rotate each affected key immediately.\n"));
|
|
26299
26830
|
for (const e of open) {
|
|
26300
26831
|
console.log(
|
|
26301
|
-
" " +
|
|
26832
|
+
" " + import_chalk32.default.red("\u25CF") + " " + import_chalk32.default.white(e.dlpPattern ?? "Secret") + import_chalk32.default.dim(" " + fmtDate3(e.ts))
|
|
26302
26833
|
);
|
|
26303
26834
|
if (e.dlpSample) {
|
|
26304
|
-
console.log(" " +
|
|
26835
|
+
console.log(" " + import_chalk32.default.dim("Sample: ") + import_chalk32.default.yellow(stripAnsi(e.dlpSample)));
|
|
26305
26836
|
}
|
|
26306
26837
|
if (e.project) {
|
|
26307
|
-
console.log(" " +
|
|
26838
|
+
console.log(" " + import_chalk32.default.dim("Project: ") + import_chalk32.default.dim(stripAnsi(e.project)));
|
|
26308
26839
|
}
|
|
26309
26840
|
console.log("");
|
|
26310
26841
|
}
|
|
26311
|
-
console.log(" " +
|
|
26312
|
-
console.log(" " +
|
|
26842
|
+
console.log(" " + import_chalk32.default.bold("Next steps:"));
|
|
26843
|
+
console.log(" " + import_chalk32.default.cyan("1.") + " Rotate any exposed keys shown above");
|
|
26313
26844
|
console.log(
|
|
26314
|
-
" " +
|
|
26845
|
+
" " + import_chalk32.default.cyan("2.") + " Run " + import_chalk32.default.white("node9 dlp resolve") + " to acknowledge"
|
|
26315
26846
|
);
|
|
26316
26847
|
console.log(
|
|
26317
|
-
" " +
|
|
26848
|
+
" " + import_chalk32.default.cyan("3.") + " Run " + import_chalk32.default.white("node9 report") + " for full audit history"
|
|
26318
26849
|
);
|
|
26319
26850
|
console.log("");
|
|
26320
26851
|
});
|
|
26321
26852
|
}
|
|
26322
26853
|
|
|
26323
26854
|
// src/cli/commands/mask.ts
|
|
26324
|
-
var
|
|
26325
|
-
var
|
|
26326
|
-
var
|
|
26327
|
-
var
|
|
26855
|
+
var import_chalk33 = __toESM(require("chalk"));
|
|
26856
|
+
var import_fs59 = __toESM(require("fs"));
|
|
26857
|
+
var import_path57 = __toESM(require("path"));
|
|
26858
|
+
var import_os51 = __toESM(require("os"));
|
|
26328
26859
|
init_dlp();
|
|
26329
26860
|
function findJsonlFiles(dir) {
|
|
26330
26861
|
const results = [];
|
|
26331
|
-
if (!
|
|
26332
|
-
for (const entry of
|
|
26333
|
-
const full =
|
|
26862
|
+
if (!import_fs59.default.existsSync(dir)) return results;
|
|
26863
|
+
for (const entry of import_fs59.default.readdirSync(dir, { withFileTypes: true })) {
|
|
26864
|
+
const full = import_path57.default.join(dir, entry.name);
|
|
26334
26865
|
if (entry.isDirectory()) results.push(...findJsonlFiles(full));
|
|
26335
26866
|
else if (entry.isFile() && entry.name.endsWith(".jsonl")) results.push(full);
|
|
26336
26867
|
}
|
|
@@ -26373,7 +26904,7 @@ function redactJson(obj) {
|
|
|
26373
26904
|
function processFile(filePath, dryRun) {
|
|
26374
26905
|
let raw;
|
|
26375
26906
|
try {
|
|
26376
|
-
raw =
|
|
26907
|
+
raw = import_fs59.default.readFileSync(filePath, "utf-8");
|
|
26377
26908
|
} catch {
|
|
26378
26909
|
return { redactedLines: 0, patterns: [] };
|
|
26379
26910
|
}
|
|
@@ -26405,14 +26936,14 @@ function processFile(filePath, dryRun) {
|
|
|
26405
26936
|
}
|
|
26406
26937
|
}
|
|
26407
26938
|
if (!dryRun && redactedLines > 0) {
|
|
26408
|
-
|
|
26939
|
+
import_fs59.default.writeFileSync(filePath, newLines.join("\n"), "utf-8");
|
|
26409
26940
|
}
|
|
26410
26941
|
return { redactedLines, patterns };
|
|
26411
26942
|
}
|
|
26412
26943
|
function processJsonFile(filePath, dryRun) {
|
|
26413
26944
|
let raw;
|
|
26414
26945
|
try {
|
|
26415
|
-
raw =
|
|
26946
|
+
raw = import_fs59.default.readFileSync(filePath, "utf-8");
|
|
26416
26947
|
} catch {
|
|
26417
26948
|
return { redactedLines: 0, patterns: [] };
|
|
26418
26949
|
}
|
|
@@ -26425,15 +26956,15 @@ function processJsonFile(filePath, dryRun) {
|
|
|
26425
26956
|
const { value, modified, found } = redactJson(parsed);
|
|
26426
26957
|
if (!modified) return { redactedLines: 0, patterns: [] };
|
|
26427
26958
|
if (!dryRun) {
|
|
26428
|
-
|
|
26959
|
+
import_fs59.default.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
|
|
26429
26960
|
}
|
|
26430
26961
|
return { redactedLines: 1, patterns: found };
|
|
26431
26962
|
}
|
|
26432
26963
|
function findJsonFiles(dir) {
|
|
26433
26964
|
const results = [];
|
|
26434
|
-
if (!
|
|
26435
|
-
for (const entry of
|
|
26436
|
-
const full =
|
|
26965
|
+
if (!import_fs59.default.existsSync(dir)) return results;
|
|
26966
|
+
for (const entry of import_fs59.default.readdirSync(dir, { withFileTypes: true })) {
|
|
26967
|
+
const full = import_path57.default.join(dir, entry.name);
|
|
26437
26968
|
if (entry.isDirectory()) results.push(...findJsonFiles(full));
|
|
26438
26969
|
else if (entry.isFile() && entry.name.endsWith(".json")) results.push(full);
|
|
26439
26970
|
}
|
|
@@ -26442,9 +26973,9 @@ function findJsonFiles(dir) {
|
|
|
26442
26973
|
function registerMaskCommand(program2) {
|
|
26443
26974
|
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) => {
|
|
26444
26975
|
const dryRun = !!options.dryRun;
|
|
26445
|
-
const home =
|
|
26446
|
-
const claudeDir =
|
|
26447
|
-
const geminiDir =
|
|
26976
|
+
const home = import_os51.default.homedir();
|
|
26977
|
+
const claudeDir = import_path57.default.join(home, ".claude", "projects");
|
|
26978
|
+
const geminiDir = import_path57.default.join(home, ".gemini", "tmp");
|
|
26448
26979
|
const allFiles = [
|
|
26449
26980
|
...findJsonlFiles(claudeDir).map((p) => ({ path: p, type: "jsonl" })),
|
|
26450
26981
|
...findJsonFiles(geminiDir).map((p) => ({ path: p, type: "json" }))
|
|
@@ -26452,18 +26983,18 @@ function registerMaskCommand(program2) {
|
|
|
26452
26983
|
const cutoff = options.all ? null : new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3);
|
|
26453
26984
|
const filtered = cutoff ? allFiles.filter((f) => {
|
|
26454
26985
|
try {
|
|
26455
|
-
return
|
|
26986
|
+
return import_fs59.default.statSync(f.path).mtime >= cutoff;
|
|
26456
26987
|
} catch {
|
|
26457
26988
|
return false;
|
|
26458
26989
|
}
|
|
26459
26990
|
}) : allFiles;
|
|
26460
26991
|
if (filtered.length === 0) {
|
|
26461
|
-
console.log(
|
|
26992
|
+
console.log(import_chalk33.default.yellow(" No session files found."));
|
|
26462
26993
|
return;
|
|
26463
26994
|
}
|
|
26464
26995
|
console.log("");
|
|
26465
26996
|
if (dryRun) {
|
|
26466
|
-
console.log(
|
|
26997
|
+
console.log(import_chalk33.default.dim(" Dry run \u2014 no files will be modified.\n"));
|
|
26467
26998
|
}
|
|
26468
26999
|
let totalFiles = 0;
|
|
26469
27000
|
let totalLines = 0;
|
|
@@ -26479,23 +27010,23 @@ function registerMaskCommand(program2) {
|
|
|
26479
27010
|
});
|
|
26480
27011
|
const verb = dryRun ? "Would redact" : "Redacted";
|
|
26481
27012
|
console.log(
|
|
26482
|
-
" " +
|
|
27013
|
+
" " + import_chalk33.default.dim(shortPath.slice(0, 60).padEnd(62)) + import_chalk33.default.red(`${verb}: `) + import_chalk33.default.yellow(patterns.join(", ")) + import_chalk33.default.dim(` (${redactedLines} line${redactedLines !== 1 ? "s" : ""})`)
|
|
26483
27014
|
);
|
|
26484
27015
|
}
|
|
26485
27016
|
}
|
|
26486
27017
|
console.log("");
|
|
26487
27018
|
if (totalFiles === 0) {
|
|
26488
|
-
console.log(
|
|
27019
|
+
console.log(import_chalk33.default.green(" No secrets found in session history."));
|
|
26489
27020
|
} else {
|
|
26490
27021
|
const verb = dryRun ? "would be modified" : "modified";
|
|
26491
27022
|
console.log(
|
|
26492
|
-
|
|
27023
|
+
import_chalk33.default.bold(` ${totalFiles} file${totalFiles !== 1 ? "s" : ""} ${verb}`) + import_chalk33.default.dim(`, ${totalLines} line${totalLines !== 1 ? "s" : ""} redacted`)
|
|
26493
27024
|
);
|
|
26494
|
-
console.log(" Patterns: " +
|
|
27025
|
+
console.log(" Patterns: " + import_chalk33.default.yellow(totalPatterns.join(", ")));
|
|
26495
27026
|
if (!dryRun) {
|
|
26496
27027
|
console.log("");
|
|
26497
27028
|
console.log(
|
|
26498
|
-
|
|
27029
|
+
import_chalk33.default.dim(
|
|
26499
27030
|
" Note: secrets were already sent to the AI provider during the active session.\n This cleans your local disk only. Rotate any exposed keys."
|
|
26500
27031
|
)
|
|
26501
27032
|
);
|
|
@@ -26508,20 +27039,20 @@ function registerMaskCommand(program2) {
|
|
|
26508
27039
|
// src/cli.ts
|
|
26509
27040
|
init_blast();
|
|
26510
27041
|
var { version } = JSON.parse(
|
|
26511
|
-
|
|
27042
|
+
import_fs62.default.readFileSync(import_path60.default.join(__dirname, "../package.json"), "utf-8")
|
|
26512
27043
|
);
|
|
26513
27044
|
var program = new import_commander.Command();
|
|
26514
27045
|
program.name("node9").description("The Sudo Command for AI Agents").version(version);
|
|
26515
27046
|
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) => {
|
|
26516
27047
|
const DEFAULT_API_URL2 = "https://api.node9.ai/api/v1/intercept";
|
|
26517
|
-
const credPath =
|
|
26518
|
-
if (!
|
|
26519
|
-
|
|
27048
|
+
const credPath = import_path60.default.join(import_os54.default.homedir(), ".node9", "credentials.json");
|
|
27049
|
+
if (!import_fs62.default.existsSync(import_path60.default.dirname(credPath)))
|
|
27050
|
+
import_fs62.default.mkdirSync(import_path60.default.dirname(credPath), { recursive: true });
|
|
26520
27051
|
const profileName = options.profile || "default";
|
|
26521
27052
|
let existingCreds = {};
|
|
26522
27053
|
try {
|
|
26523
|
-
if (
|
|
26524
|
-
const raw = JSON.parse(
|
|
27054
|
+
if (import_fs62.default.existsSync(credPath)) {
|
|
27055
|
+
const raw = JSON.parse(import_fs62.default.readFileSync(credPath, "utf-8"));
|
|
26525
27056
|
if (raw.apiKey) {
|
|
26526
27057
|
existingCreds = {
|
|
26527
27058
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL2 }
|
|
@@ -26533,14 +27064,14 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
26533
27064
|
} catch {
|
|
26534
27065
|
}
|
|
26535
27066
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL2 };
|
|
26536
|
-
|
|
27067
|
+
import_fs62.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
26537
27068
|
let effectiveCloud = null;
|
|
26538
27069
|
if (profileName === "default") {
|
|
26539
|
-
const configPath2 =
|
|
27070
|
+
const configPath2 = import_path60.default.join(import_os54.default.homedir(), ".node9", "config.json");
|
|
26540
27071
|
let config = {};
|
|
26541
27072
|
try {
|
|
26542
|
-
if (
|
|
26543
|
-
config = JSON.parse(
|
|
27073
|
+
if (import_fs62.default.existsSync(configPath2))
|
|
27074
|
+
config = JSON.parse(import_fs62.default.readFileSync(configPath2, "utf-8"));
|
|
26544
27075
|
} catch {
|
|
26545
27076
|
}
|
|
26546
27077
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -26555,38 +27086,38 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
26555
27086
|
approvers.cloud = false;
|
|
26556
27087
|
}
|
|
26557
27088
|
s.approvers = approvers;
|
|
26558
|
-
if (!
|
|
26559
|
-
|
|
26560
|
-
|
|
27089
|
+
if (!import_fs62.default.existsSync(import_path60.default.dirname(configPath2)))
|
|
27090
|
+
import_fs62.default.mkdirSync(import_path60.default.dirname(configPath2), { recursive: true });
|
|
27091
|
+
import_fs62.default.writeFileSync(configPath2, JSON.stringify(config, null, 2), { mode: 384 });
|
|
26561
27092
|
effectiveCloud = approvers.cloud === true;
|
|
26562
27093
|
}
|
|
26563
27094
|
if (options.profile && profileName !== "default") {
|
|
26564
|
-
console.log(
|
|
26565
|
-
console.log(
|
|
27095
|
+
console.log(import_chalk35.default.green(`\u2705 Profile "${profileName}" saved`));
|
|
27096
|
+
console.log(import_chalk35.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
26566
27097
|
} else if (options.local || effectiveCloud === false) {
|
|
26567
|
-
console.log(
|
|
26568
|
-
console.log(
|
|
27098
|
+
console.log(import_chalk35.default.green(`\u2705 Key saved \u2014 Privacy mode \u{1F6E1}\uFE0F`));
|
|
27099
|
+
console.log(import_chalk35.default.gray(` All decisions stay on this machine. Nothing syncs to the cloud.`));
|
|
26569
27100
|
if (!options.local) {
|
|
26570
27101
|
console.log(
|
|
26571
|
-
|
|
27102
|
+
import_chalk35.default.yellow(` Your config has cloud approvals OFF (settings.approvers.cloud).`)
|
|
26572
27103
|
);
|
|
26573
27104
|
console.log(
|
|
26574
|
-
|
|
27105
|
+
import_chalk35.default.gray(` To enable team policy + dashboard sync: set it to true, or re-init.`)
|
|
26575
27106
|
);
|
|
26576
27107
|
}
|
|
26577
27108
|
} else {
|
|
26578
|
-
console.log(
|
|
26579
|
-
console.log(
|
|
27109
|
+
console.log(import_chalk35.default.green(`\u2705 Logged in \u2014 agent mode`));
|
|
27110
|
+
console.log(import_chalk35.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
26580
27111
|
}
|
|
26581
27112
|
});
|
|
26582
27113
|
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) => {
|
|
26583
27114
|
const route = options.login ? "auth/login" : "auth/signup";
|
|
26584
27115
|
const url = `https://node9.ai/${route}?ref=cli_cmd`;
|
|
26585
27116
|
console.log("");
|
|
26586
|
-
console.log(" " +
|
|
27117
|
+
console.log(" " + import_chalk35.default.dim("Opening ") + import_chalk35.default.cyan.underline(url));
|
|
26587
27118
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
26588
27119
|
try {
|
|
26589
|
-
const child = (0,
|
|
27120
|
+
const child = (0, import_child_process15.spawn)(opener, [url], {
|
|
26590
27121
|
stdio: "ignore",
|
|
26591
27122
|
detached: true,
|
|
26592
27123
|
shell: process.platform === "win32"
|
|
@@ -26616,7 +27147,7 @@ program.command("addto", { hidden: true }).description("Integrate Node9 with an
|
|
|
26616
27147
|
if (target === "hermes") return setupHermes();
|
|
26617
27148
|
if (target === "hud") return setupHud();
|
|
26618
27149
|
console.error(
|
|
26619
|
-
|
|
27150
|
+
import_chalk35.default.red(
|
|
26620
27151
|
`Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
|
|
26621
27152
|
)
|
|
26622
27153
|
);
|
|
@@ -26630,20 +27161,20 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
|
|
|
26630
27161
|
"The agent to protect: claude | antigravity | copilot | gemini | cursor | codex | windsurf | vscode | hud"
|
|
26631
27162
|
).action(async (target) => {
|
|
26632
27163
|
if (!target) {
|
|
26633
|
-
console.log(
|
|
26634
|
-
console.log(" Usage: " +
|
|
27164
|
+
console.log(import_chalk35.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
27165
|
+
console.log(" Usage: " + import_chalk35.default.white("node9 setup <target>") + "\n");
|
|
26635
27166
|
console.log(" Targets:");
|
|
26636
|
-
console.log(" " +
|
|
26637
|
-
console.log(" " +
|
|
26638
|
-
console.log(" " +
|
|
26639
|
-
console.log(" " +
|
|
26640
|
-
console.log(" " +
|
|
26641
|
-
console.log(" " +
|
|
26642
|
-
console.log(" " +
|
|
26643
|
-
console.log(" " +
|
|
26644
|
-
console.log(" " +
|
|
27167
|
+
console.log(" " + import_chalk35.default.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
27168
|
+
console.log(" " + import_chalk35.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
27169
|
+
console.log(" " + import_chalk35.default.green("antigravity") + " \u2014 Antigravity / agy (hook mode)");
|
|
27170
|
+
console.log(" " + import_chalk35.default.green("copilot") + " \u2014 GitHub Copilot CLI (hook mode)");
|
|
27171
|
+
console.log(" " + import_chalk35.default.green("cursor") + " \u2014 Cursor (MCP proxy)");
|
|
27172
|
+
console.log(" " + import_chalk35.default.green("codex") + " \u2014 OpenAI Codex CLI (MCP proxy)");
|
|
27173
|
+
console.log(" " + import_chalk35.default.green("windsurf") + " \u2014 Windsurf (MCP proxy)");
|
|
27174
|
+
console.log(" " + import_chalk35.default.green("vscode") + " \u2014 VSCode / Copilot (MCP proxy)");
|
|
27175
|
+
console.log(" " + import_chalk35.default.green("hermes") + " \u2014 Hermes Agent (hook mode)");
|
|
26645
27176
|
process.stdout.write(
|
|
26646
|
-
" " +
|
|
27177
|
+
" " + import_chalk35.default.green("hud") + " \u2014 Claude Code security statusline\n"
|
|
26647
27178
|
);
|
|
26648
27179
|
console.log("");
|
|
26649
27180
|
return;
|
|
@@ -26660,7 +27191,7 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
|
|
|
26660
27191
|
if (t === "hermes") return setupHermes();
|
|
26661
27192
|
if (t === "hud") return setupHud();
|
|
26662
27193
|
console.error(
|
|
26663
|
-
|
|
27194
|
+
import_chalk35.default.red(
|
|
26664
27195
|
`Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
|
|
26665
27196
|
)
|
|
26666
27197
|
);
|
|
@@ -26686,33 +27217,33 @@ program.command("removefrom", { hidden: true }).description("Remove Node9 hooks
|
|
|
26686
27217
|
else if (target === "hud") fn = teardownHud;
|
|
26687
27218
|
else {
|
|
26688
27219
|
console.error(
|
|
26689
|
-
|
|
27220
|
+
import_chalk35.default.red(
|
|
26690
27221
|
`Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
|
|
26691
27222
|
)
|
|
26692
27223
|
);
|
|
26693
27224
|
process.exit(1);
|
|
26694
27225
|
}
|
|
26695
|
-
console.log(
|
|
27226
|
+
console.log(import_chalk35.default.cyan(`
|
|
26696
27227
|
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
26697
27228
|
`));
|
|
26698
27229
|
try {
|
|
26699
27230
|
fn();
|
|
26700
27231
|
} catch (err2) {
|
|
26701
|
-
console.error(
|
|
27232
|
+
console.error(import_chalk35.default.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
26702
27233
|
process.exit(1);
|
|
26703
27234
|
}
|
|
26704
|
-
console.log(
|
|
27235
|
+
console.log(import_chalk35.default.gray("\n Restart the agent for changes to take effect."));
|
|
26705
27236
|
});
|
|
26706
27237
|
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) => {
|
|
26707
|
-
console.log(
|
|
26708
|
-
console.log(
|
|
27238
|
+
console.log(import_chalk35.default.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
27239
|
+
console.log(import_chalk35.default.bold("Stopping daemon..."));
|
|
26709
27240
|
try {
|
|
26710
27241
|
stopDaemon();
|
|
26711
|
-
console.log(
|
|
27242
|
+
console.log(import_chalk35.default.green(" \u2705 Daemon stopped"));
|
|
26712
27243
|
} catch {
|
|
26713
|
-
console.log(
|
|
27244
|
+
console.log(import_chalk35.default.blue(" \u2139\uFE0F Daemon was not running"));
|
|
26714
27245
|
}
|
|
26715
|
-
console.log(
|
|
27246
|
+
console.log(import_chalk35.default.bold("\nRemoving hooks..."));
|
|
26716
27247
|
let teardownFailed = false;
|
|
26717
27248
|
for (const [label2, fn] of [
|
|
26718
27249
|
["Claude", teardownClaude],
|
|
@@ -26728,45 +27259,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
26728
27259
|
} catch (err2) {
|
|
26729
27260
|
teardownFailed = true;
|
|
26730
27261
|
console.error(
|
|
26731
|
-
|
|
27262
|
+
import_chalk35.default.red(
|
|
26732
27263
|
` \u26A0\uFE0F Failed to remove ${label2} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
26733
27264
|
)
|
|
26734
27265
|
);
|
|
26735
27266
|
}
|
|
26736
27267
|
}
|
|
26737
27268
|
if (options.purge) {
|
|
26738
|
-
const node9Dir =
|
|
26739
|
-
if (
|
|
27269
|
+
const node9Dir = import_path60.default.join(import_os54.default.homedir(), ".node9");
|
|
27270
|
+
if (import_fs62.default.existsSync(node9Dir)) {
|
|
26740
27271
|
const confirmed = await (0, import_prompts2.confirm)({
|
|
26741
27272
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
26742
27273
|
default: false
|
|
26743
27274
|
});
|
|
26744
27275
|
if (confirmed) {
|
|
26745
|
-
|
|
26746
|
-
if (
|
|
27276
|
+
import_fs62.default.rmSync(node9Dir, { recursive: true });
|
|
27277
|
+
if (import_fs62.default.existsSync(node9Dir)) {
|
|
26747
27278
|
console.error(
|
|
26748
|
-
|
|
27279
|
+
import_chalk35.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
26749
27280
|
);
|
|
26750
27281
|
} else {
|
|
26751
|
-
console.log(
|
|
27282
|
+
console.log(import_chalk35.default.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
26752
27283
|
}
|
|
26753
27284
|
} else {
|
|
26754
|
-
console.log(
|
|
27285
|
+
console.log(import_chalk35.default.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
26755
27286
|
}
|
|
26756
27287
|
} else {
|
|
26757
|
-
console.log(
|
|
27288
|
+
console.log(import_chalk35.default.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
26758
27289
|
}
|
|
26759
27290
|
} else {
|
|
26760
27291
|
console.log(
|
|
26761
|
-
|
|
27292
|
+
import_chalk35.default.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
26762
27293
|
);
|
|
26763
27294
|
}
|
|
26764
27295
|
if (teardownFailed) {
|
|
26765
|
-
console.error(
|
|
27296
|
+
console.error(import_chalk35.default.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
26766
27297
|
process.exit(1);
|
|
26767
27298
|
}
|
|
26768
|
-
console.log(
|
|
26769
|
-
console.log(
|
|
27299
|
+
console.log(import_chalk35.default.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g node9-ai"));
|
|
27300
|
+
console.log(import_chalk35.default.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
|
|
26770
27301
|
});
|
|
26771
27302
|
registerDoctorCommand(program, version);
|
|
26772
27303
|
program.command("explain").description(
|
|
@@ -26779,7 +27310,7 @@ program.command("explain").description(
|
|
|
26779
27310
|
try {
|
|
26780
27311
|
args = JSON.parse(trimmed);
|
|
26781
27312
|
} catch {
|
|
26782
|
-
console.error(
|
|
27313
|
+
console.error(import_chalk35.default.red(`
|
|
26783
27314
|
\u274C Invalid JSON: ${trimmed}
|
|
26784
27315
|
`));
|
|
26785
27316
|
process.exit(1);
|
|
@@ -26790,54 +27321,54 @@ program.command("explain").description(
|
|
|
26790
27321
|
}
|
|
26791
27322
|
const result = await explainPolicy(tool, args);
|
|
26792
27323
|
console.log("");
|
|
26793
|
-
console.log(
|
|
27324
|
+
console.log(import_chalk35.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
26794
27325
|
console.log("");
|
|
26795
|
-
console.log(` ${
|
|
27326
|
+
console.log(` ${import_chalk35.default.bold("Tool:")} ${import_chalk35.default.white(result.tool)}`);
|
|
26796
27327
|
if (argsRaw) {
|
|
26797
27328
|
const preview2 = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
26798
|
-
console.log(` ${
|
|
27329
|
+
console.log(` ${import_chalk35.default.bold("Input:")} ${import_chalk35.default.gray(preview2)}`);
|
|
26799
27330
|
}
|
|
26800
27331
|
console.log("");
|
|
26801
|
-
console.log(
|
|
27332
|
+
console.log(import_chalk35.default.bold("Config Sources (Waterfall):"));
|
|
26802
27333
|
for (const tier of result.waterfall) {
|
|
26803
|
-
const num3 =
|
|
27334
|
+
const num3 = import_chalk35.default.gray(` ${tier.tier}.`);
|
|
26804
27335
|
const label2 = tier.label.padEnd(16);
|
|
26805
27336
|
let statusStr;
|
|
26806
27337
|
if (tier.tier === 1) {
|
|
26807
|
-
statusStr =
|
|
27338
|
+
statusStr = import_chalk35.default.gray(tier.note ?? "");
|
|
26808
27339
|
} else if (tier.status === "active") {
|
|
26809
|
-
const loc = tier.path ?
|
|
26810
|
-
const note = tier.note ?
|
|
26811
|
-
statusStr =
|
|
27340
|
+
const loc = tier.path ? import_chalk35.default.gray(tier.path) : "";
|
|
27341
|
+
const note = tier.note ? import_chalk35.default.gray(`(${tier.note})`) : "";
|
|
27342
|
+
statusStr = import_chalk35.default.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
|
|
26812
27343
|
} else {
|
|
26813
|
-
statusStr =
|
|
27344
|
+
statusStr = import_chalk35.default.gray("\u25CB " + (tier.note ?? "not found"));
|
|
26814
27345
|
}
|
|
26815
|
-
console.log(`${num3} ${
|
|
27346
|
+
console.log(`${num3} ${import_chalk35.default.white(label2)} ${statusStr}`);
|
|
26816
27347
|
}
|
|
26817
27348
|
console.log("");
|
|
26818
|
-
console.log(
|
|
27349
|
+
console.log(import_chalk35.default.bold("Policy Evaluation:"));
|
|
26819
27350
|
for (const step of result.steps) {
|
|
26820
27351
|
const isFinal = step.isFinal;
|
|
26821
27352
|
let icon;
|
|
26822
|
-
if (step.outcome === "allow") icon =
|
|
26823
|
-
else if (step.outcome === "review") icon =
|
|
26824
|
-
else if (step.outcome === "skip") icon =
|
|
26825
|
-
else icon =
|
|
27353
|
+
if (step.outcome === "allow") icon = import_chalk35.default.green(" \u2705");
|
|
27354
|
+
else if (step.outcome === "review") icon = import_chalk35.default.red(" \u{1F534}");
|
|
27355
|
+
else if (step.outcome === "skip") icon = import_chalk35.default.gray(" \u2500 ");
|
|
27356
|
+
else icon = import_chalk35.default.gray(" \u25CB ");
|
|
26826
27357
|
const name = step.name.padEnd(18);
|
|
26827
|
-
const nameStr = isFinal ?
|
|
26828
|
-
const detail = isFinal ?
|
|
26829
|
-
const arrow = isFinal ?
|
|
27358
|
+
const nameStr = isFinal ? import_chalk35.default.white.bold(name) : import_chalk35.default.white(name);
|
|
27359
|
+
const detail = isFinal ? import_chalk35.default.white(step.detail) : import_chalk35.default.gray(step.detail);
|
|
27360
|
+
const arrow = isFinal ? import_chalk35.default.yellow(" \u2190 STOP") : "";
|
|
26830
27361
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
26831
27362
|
}
|
|
26832
27363
|
console.log("");
|
|
26833
27364
|
if (result.decision === "allow") {
|
|
26834
|
-
console.log(
|
|
27365
|
+
console.log(import_chalk35.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk35.default.gray(" \u2014 no approval needed"));
|
|
26835
27366
|
} else {
|
|
26836
27367
|
console.log(
|
|
26837
|
-
|
|
27368
|
+
import_chalk35.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk35.default.gray(" \u2014 human approval required")
|
|
26838
27369
|
);
|
|
26839
27370
|
if (result.blockedByLabel) {
|
|
26840
|
-
console.log(
|
|
27371
|
+
console.log(import_chalk35.default.gray(` Reason: ${result.blockedByLabel}`));
|
|
26841
27372
|
}
|
|
26842
27373
|
}
|
|
26843
27374
|
console.log("");
|
|
@@ -26852,18 +27383,18 @@ program.command("tail").description("Stream live agent activity to the terminal"
|
|
|
26852
27383
|
try {
|
|
26853
27384
|
await startTail2(options);
|
|
26854
27385
|
} catch (err2) {
|
|
26855
|
-
console.error(
|
|
27386
|
+
console.error(import_chalk35.default.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
26856
27387
|
process.exit(1);
|
|
26857
27388
|
}
|
|
26858
27389
|
});
|
|
26859
27390
|
program.command("monitor").description("Live interactive dashboard \u2014 activity feed, approvals, security signals").action(async () => {
|
|
26860
27391
|
try {
|
|
26861
|
-
const dashboardPath =
|
|
27392
|
+
const dashboardPath = import_path60.default.join(__dirname, "dashboard.mjs");
|
|
26862
27393
|
const dynamicImport = new Function("id", "return import(id)");
|
|
26863
27394
|
const mod = await dynamicImport(`file://${dashboardPath}`);
|
|
26864
27395
|
await mod.startMonitor();
|
|
26865
27396
|
} catch (err2) {
|
|
26866
|
-
console.error(
|
|
27397
|
+
console.error(import_chalk35.default.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
26867
27398
|
process.exit(1);
|
|
26868
27399
|
}
|
|
26869
27400
|
});
|
|
@@ -26896,14 +27427,14 @@ Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
|
|
|
26896
27427
|
Run "node9 addto claude" to register it as the statusLine.`
|
|
26897
27428
|
).argument("[subcommand]", 'Optional: "debug on" / "debug off" to toggle stdin logging').argument("[state]", 'on|off \u2014 used with "debug" subcommand').action(async (subcommand, state) => {
|
|
26898
27429
|
if (subcommand === "debug") {
|
|
26899
|
-
const flagFile =
|
|
27430
|
+
const flagFile = import_path60.default.join(import_os54.default.homedir(), ".node9", "hud-debug");
|
|
26900
27431
|
if (state === "on") {
|
|
26901
|
-
|
|
26902
|
-
|
|
27432
|
+
import_fs62.default.mkdirSync(import_path60.default.dirname(flagFile), { recursive: true });
|
|
27433
|
+
import_fs62.default.writeFileSync(flagFile, "");
|
|
26903
27434
|
console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
|
|
26904
27435
|
console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
|
|
26905
27436
|
} else if (state === "off") {
|
|
26906
|
-
if (
|
|
27437
|
+
if (import_fs62.default.existsSync(flagFile)) import_fs62.default.unlinkSync(flagFile);
|
|
26907
27438
|
console.log("HUD debug logging disabled.");
|
|
26908
27439
|
} else {
|
|
26909
27440
|
console.error("Usage: node9 hud debug on|off");
|
|
@@ -26918,7 +27449,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
26918
27449
|
const ms = parseDuration(options.duration);
|
|
26919
27450
|
if (ms === null) {
|
|
26920
27451
|
console.error(
|
|
26921
|
-
|
|
27452
|
+
import_chalk35.default.red(`
|
|
26922
27453
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
26923
27454
|
`)
|
|
26924
27455
|
);
|
|
@@ -26926,20 +27457,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
26926
27457
|
}
|
|
26927
27458
|
pauseNode9(ms, options.duration);
|
|
26928
27459
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
26929
|
-
console.log(
|
|
27460
|
+
console.log(import_chalk35.default.yellow(`
|
|
26930
27461
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
26931
|
-
console.log(
|
|
26932
|
-
console.log(
|
|
27462
|
+
console.log(import_chalk35.default.gray(` All tool calls will be allowed without review.`));
|
|
27463
|
+
console.log(import_chalk35.default.gray(` Run "node9 resume" to re-enable early.
|
|
26933
27464
|
`));
|
|
26934
27465
|
});
|
|
26935
27466
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
26936
27467
|
const { paused } = checkPause();
|
|
26937
27468
|
if (!paused) {
|
|
26938
|
-
console.log(
|
|
27469
|
+
console.log(import_chalk35.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
26939
27470
|
return;
|
|
26940
27471
|
}
|
|
26941
27472
|
resumeNode9();
|
|
26942
|
-
console.log(
|
|
27473
|
+
console.log(import_chalk35.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
26943
27474
|
});
|
|
26944
27475
|
var HOOK_BASED_AGENTS = {
|
|
26945
27476
|
claude: "claude",
|
|
@@ -26955,15 +27486,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
26955
27486
|
if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
|
|
26956
27487
|
const target = HOOK_BASED_AGENTS[firstArg2];
|
|
26957
27488
|
console.error(
|
|
26958
|
-
|
|
27489
|
+
import_chalk35.default.yellow(`
|
|
26959
27490
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
26960
27491
|
);
|
|
26961
|
-
console.error(
|
|
27492
|
+
console.error(import_chalk35.default.white(`
|
|
26962
27493
|
"${target}" uses its own hook system. Use:`));
|
|
26963
27494
|
console.error(
|
|
26964
|
-
|
|
27495
|
+
import_chalk35.default.green(` node9 addto ${target} `) + import_chalk35.default.gray("# one-time setup")
|
|
26965
27496
|
);
|
|
26966
|
-
console.error(
|
|
27497
|
+
console.error(import_chalk35.default.green(` ${target} `) + import_chalk35.default.gray("# run normally"));
|
|
26967
27498
|
process.exit(1);
|
|
26968
27499
|
}
|
|
26969
27500
|
const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
|
|
@@ -26980,7 +27511,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
26980
27511
|
}
|
|
26981
27512
|
);
|
|
26982
27513
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
26983
|
-
console.error(
|
|
27514
|
+
console.error(import_chalk35.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
26984
27515
|
const daemonReady = await autoStartDaemonAndWait();
|
|
26985
27516
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
26986
27517
|
}
|
|
@@ -26993,12 +27524,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
26993
27524
|
}
|
|
26994
27525
|
if (!result.approved) {
|
|
26995
27526
|
console.error(
|
|
26996
|
-
|
|
27527
|
+
import_chalk35.default.red(`
|
|
26997
27528
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
26998
27529
|
);
|
|
26999
27530
|
process.exit(1);
|
|
27000
27531
|
}
|
|
27001
|
-
console.error(
|
|
27532
|
+
console.error(import_chalk35.default.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
27002
27533
|
await runProxy(fullCommand);
|
|
27003
27534
|
} else {
|
|
27004
27535
|
program.help();
|
|
@@ -27013,6 +27544,7 @@ registerAgentsCommand(program);
|
|
|
27013
27544
|
registerScanCommand(program);
|
|
27014
27545
|
registerPostureCommand(program);
|
|
27015
27546
|
registerEgressCommand(program);
|
|
27547
|
+
registerSandboxCommand(program, version);
|
|
27016
27548
|
registerSessionsCommand(program);
|
|
27017
27549
|
registerSessionTaintCommand(program);
|
|
27018
27550
|
registerDlpCommand(program);
|
|
@@ -27023,9 +27555,9 @@ if (process.argv[2] !== "daemon") {
|
|
|
27023
27555
|
const isCheckHook = process.argv[2] === "check";
|
|
27024
27556
|
if (isCheckHook) {
|
|
27025
27557
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
27026
|
-
const logPath =
|
|
27558
|
+
const logPath = import_path60.default.join(import_os54.default.homedir(), ".node9", "hook-debug.log");
|
|
27027
27559
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
27028
|
-
|
|
27560
|
+
import_fs62.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
27029
27561
|
`);
|
|
27030
27562
|
}
|
|
27031
27563
|
process.exit(0);
|