@node9/proxy 1.5.3 → 1.5.4
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 +56 -14
- package/dist/cli.js +665 -357
- package/dist/cli.mjs +660 -352
- package/dist/index.js +53 -10
- package/dist/index.mjs +53 -10
- package/package.json +6 -5
package/dist/cli.mjs
CHANGED
|
@@ -139,8 +139,8 @@ function sanitizeConfig(raw) {
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
const lines = result.error.issues.map((issue) => {
|
|
142
|
-
const
|
|
143
|
-
return ` \u2022 ${
|
|
142
|
+
const path30 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
143
|
+
return ` \u2022 ${path30}: ${issue.message}`;
|
|
144
144
|
});
|
|
145
145
|
return {
|
|
146
146
|
sanitized,
|
|
@@ -841,7 +841,9 @@ var init_config = __esm({
|
|
|
841
841
|
{
|
|
842
842
|
field: "command",
|
|
843
843
|
op: "matches",
|
|
844
|
-
|
|
844
|
+
// Require the recursive flag to be preceded by whitespace so that
|
|
845
|
+
// filenames containing "-r" (e.g. "ai-review.yml") don't false-positive.
|
|
846
|
+
value: "rm\\b.*\\s(-[rRfF]*[rR][rRfF]*|--recursive)(\\s|$)"
|
|
845
847
|
},
|
|
846
848
|
{
|
|
847
849
|
field: "command",
|
|
@@ -1755,9 +1757,9 @@ function matchesPattern(text, patterns) {
|
|
|
1755
1757
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1756
1758
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1757
1759
|
}
|
|
1758
|
-
function getNestedValue(obj,
|
|
1760
|
+
function getNestedValue(obj, path30) {
|
|
1759
1761
|
if (!obj || typeof obj !== "object") return null;
|
|
1760
|
-
return
|
|
1762
|
+
return path30.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1761
1763
|
}
|
|
1762
1764
|
function shouldSnapshot(toolName, args, config) {
|
|
1763
1765
|
if (!config.settings.enableUndo) return false;
|
|
@@ -2975,6 +2977,7 @@ var init_native = __esm({
|
|
|
2975
2977
|
// src/auth/cloud.ts
|
|
2976
2978
|
import fs10 from "fs";
|
|
2977
2979
|
import os9 from "os";
|
|
2980
|
+
import path13 from "path";
|
|
2978
2981
|
function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
2979
2982
|
return fetch(`${creds.apiUrl}/audit`, {
|
|
2980
2983
|
method: "POST",
|
|
@@ -2999,6 +3002,33 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
2999
3002
|
async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
3000
3003
|
const controller = new AbortController();
|
|
3001
3004
|
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
3005
|
+
if (!creds.apiKey) throw new Error("Node9 API Key is missing");
|
|
3006
|
+
let ciContext;
|
|
3007
|
+
if (process.env.CI) {
|
|
3008
|
+
try {
|
|
3009
|
+
const ciContextPath = path13.join(os9.homedir(), ".node9", "ci-context.json");
|
|
3010
|
+
const stats = fs10.statSync(ciContextPath);
|
|
3011
|
+
if (stats.size > 1e4) throw new Error("ci-context.json exceeds 10 KB");
|
|
3012
|
+
const raw = fs10.readFileSync(ciContextPath, "utf8");
|
|
3013
|
+
const parsed = JSON.parse(raw);
|
|
3014
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
3015
|
+
throw new Error("ci-context.json is not a plain object");
|
|
3016
|
+
}
|
|
3017
|
+
const p = parsed;
|
|
3018
|
+
ciContext = {
|
|
3019
|
+
tests_after: p["tests_after"],
|
|
3020
|
+
files_changed: p["files_changed"],
|
|
3021
|
+
issues_found: p["issues_found"],
|
|
3022
|
+
issues_fixed: p["issues_fixed"],
|
|
3023
|
+
github_repository: p["github_repository"],
|
|
3024
|
+
github_head_ref: p["github_head_ref"],
|
|
3025
|
+
iteration: p["iteration"],
|
|
3026
|
+
draft_pr_number: p["draft_pr_number"],
|
|
3027
|
+
draft_pr_url: p["draft_pr_url"]
|
|
3028
|
+
};
|
|
3029
|
+
} catch {
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3002
3032
|
try {
|
|
3003
3033
|
const response = await fetch(creds.apiUrl, {
|
|
3004
3034
|
method: "POST",
|
|
@@ -3013,7 +3043,8 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
|
3013
3043
|
cwd: process.cwd(),
|
|
3014
3044
|
platform: os9.platform()
|
|
3015
3045
|
},
|
|
3016
|
-
...riskMetadata && { riskMetadata }
|
|
3046
|
+
...riskMetadata && { riskMetadata },
|
|
3047
|
+
...ciContext && { ciContext }
|
|
3017
3048
|
}),
|
|
3018
3049
|
signal: controller.signal
|
|
3019
3050
|
});
|
|
@@ -3039,12 +3070,17 @@ async function pollNode9SaaS(requestId, creds, signal) {
|
|
|
3039
3070
|
});
|
|
3040
3071
|
clearTimeout(pollTimer);
|
|
3041
3072
|
if (!statusRes.ok) continue;
|
|
3042
|
-
const
|
|
3073
|
+
const statusBody = await statusRes.json();
|
|
3074
|
+
const { status } = statusBody;
|
|
3043
3075
|
if (status === "APPROVED") {
|
|
3044
|
-
return { approved: true, reason };
|
|
3076
|
+
return { approved: true, reason: statusBody.reason };
|
|
3045
3077
|
}
|
|
3046
3078
|
if (status === "DENIED" || status === "AUTO_BLOCKED" || status === "TIMED_OUT") {
|
|
3047
|
-
return { approved: false, reason };
|
|
3079
|
+
return { approved: false, reason: statusBody.reason };
|
|
3080
|
+
}
|
|
3081
|
+
if (status === "FIX") {
|
|
3082
|
+
const feedbackText = statusBody.feedbackText ?? statusBody.reason ?? "Run again with feedback.";
|
|
3083
|
+
return { approved: false, reason: feedbackText };
|
|
3048
3084
|
}
|
|
3049
3085
|
} catch {
|
|
3050
3086
|
}
|
|
@@ -3276,7 +3312,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3276
3312
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
3277
3313
|
if (policyResult.decision === "allow") {
|
|
3278
3314
|
if (approvers.cloud && creds?.apiKey)
|
|
3279
|
-
auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
3315
|
+
await auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
3280
3316
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta, hashAuditArgs);
|
|
3281
3317
|
return { approved: true, checkedBy: "local-policy" };
|
|
3282
3318
|
}
|
|
@@ -3297,9 +3333,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3297
3333
|
if (predicatesMet && policyResult.recoveryCommand) {
|
|
3298
3334
|
statefulRecoveryCommand = policyResult.recoveryCommand;
|
|
3299
3335
|
}
|
|
3336
|
+
} else if (isDaemonRunning() && !isTestEnv2) {
|
|
3337
|
+
if (!isManual)
|
|
3338
|
+
appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
|
|
3339
|
+
if (approvers.cloud && creds?.apiKey)
|
|
3340
|
+
auditLocalAllow(toolName, args, "smart-rule-block", creds, meta);
|
|
3300
3341
|
} else {
|
|
3301
3342
|
if (!isManual)
|
|
3302
3343
|
appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
|
|
3344
|
+
if (approvers.cloud && creds?.apiKey)
|
|
3345
|
+
auditLocalAllow(toolName, args, "smart-rule-block", creds, meta);
|
|
3303
3346
|
return {
|
|
3304
3347
|
approved: false,
|
|
3305
3348
|
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
@@ -5322,7 +5365,7 @@ var init_suggestion_tracker = __esm({
|
|
|
5322
5365
|
|
|
5323
5366
|
// src/daemon/taint-store.ts
|
|
5324
5367
|
import fs12 from "fs";
|
|
5325
|
-
import
|
|
5368
|
+
import path15 from "path";
|
|
5326
5369
|
var DEFAULT_TTL_MS, TaintStore;
|
|
5327
5370
|
var init_taint_store = __esm({
|
|
5328
5371
|
"src/daemon/taint-store.ts"() {
|
|
@@ -5391,9 +5434,9 @@ var init_taint_store = __esm({
|
|
|
5391
5434
|
/** Resolve to absolute path, falling back to path.resolve if file doesn't exist yet. */
|
|
5392
5435
|
_resolve(filePath) {
|
|
5393
5436
|
try {
|
|
5394
|
-
return fs12.realpathSync.native(
|
|
5437
|
+
return fs12.realpathSync.native(path15.resolve(filePath));
|
|
5395
5438
|
} catch {
|
|
5396
|
-
return
|
|
5439
|
+
return path15.resolve(filePath);
|
|
5397
5440
|
}
|
|
5398
5441
|
}
|
|
5399
5442
|
};
|
|
@@ -5504,7 +5547,7 @@ var init_session_history = __esm({
|
|
|
5504
5547
|
// src/daemon/state.ts
|
|
5505
5548
|
import net2 from "net";
|
|
5506
5549
|
import fs13 from "fs";
|
|
5507
|
-
import
|
|
5550
|
+
import path16 from "path";
|
|
5508
5551
|
import os11 from "os";
|
|
5509
5552
|
import { spawn as spawn2 } from "child_process";
|
|
5510
5553
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
@@ -5550,7 +5593,7 @@ function markRejectionHandlerRegistered() {
|
|
|
5550
5593
|
daemonRejectionHandlerRegistered = true;
|
|
5551
5594
|
}
|
|
5552
5595
|
function atomicWriteSync2(filePath, data, options) {
|
|
5553
|
-
const dir =
|
|
5596
|
+
const dir = path16.dirname(filePath);
|
|
5554
5597
|
if (!fs13.existsSync(dir)) fs13.mkdirSync(dir, { recursive: true });
|
|
5555
5598
|
const tmpPath = `${filePath}.${randomUUID3()}.tmp`;
|
|
5556
5599
|
try {
|
|
@@ -5590,7 +5633,7 @@ function appendAuditLog(data) {
|
|
|
5590
5633
|
decision: data.decision,
|
|
5591
5634
|
source: "daemon"
|
|
5592
5635
|
};
|
|
5593
|
-
const dir =
|
|
5636
|
+
const dir = path16.dirname(AUDIT_LOG_FILE);
|
|
5594
5637
|
if (!fs13.existsSync(dir)) fs13.mkdirSync(dir, { recursive: true });
|
|
5595
5638
|
fs13.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
|
|
5596
5639
|
} catch {
|
|
@@ -5668,6 +5711,7 @@ function readBody(req) {
|
|
|
5668
5711
|
});
|
|
5669
5712
|
}
|
|
5670
5713
|
function openBrowser(url) {
|
|
5714
|
+
if (process.env.NODE9_TESTING === "1") return;
|
|
5671
5715
|
try {
|
|
5672
5716
|
const args = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
|
|
5673
5717
|
spawn2(args[0], args.slice(1), { detached: true, stdio: "ignore" }).unref();
|
|
@@ -5804,13 +5848,13 @@ var init_state2 = __esm({
|
|
|
5804
5848
|
init_session_counters();
|
|
5805
5849
|
init_session_history();
|
|
5806
5850
|
homeDir = os11.homedir();
|
|
5807
|
-
DAEMON_PID_FILE =
|
|
5808
|
-
DECISIONS_FILE =
|
|
5809
|
-
AUDIT_LOG_FILE =
|
|
5810
|
-
TRUST_FILE2 =
|
|
5811
|
-
GLOBAL_CONFIG_FILE =
|
|
5812
|
-
CREDENTIALS_FILE =
|
|
5813
|
-
INSIGHT_COUNTS_FILE =
|
|
5851
|
+
DAEMON_PID_FILE = path16.join(homeDir, ".node9", "daemon.pid");
|
|
5852
|
+
DECISIONS_FILE = path16.join(homeDir, ".node9", "decisions.json");
|
|
5853
|
+
AUDIT_LOG_FILE = path16.join(homeDir, ".node9", "audit.log");
|
|
5854
|
+
TRUST_FILE2 = path16.join(homeDir, ".node9", "trust.json");
|
|
5855
|
+
GLOBAL_CONFIG_FILE = path16.join(homeDir, ".node9", "config.json");
|
|
5856
|
+
CREDENTIALS_FILE = path16.join(homeDir, ".node9", "credentials.json");
|
|
5857
|
+
INSIGHT_COUNTS_FILE = path16.join(homeDir, ".node9", "insight-counts.json");
|
|
5814
5858
|
pending = /* @__PURE__ */ new Map();
|
|
5815
5859
|
sseClients = /* @__PURE__ */ new Set();
|
|
5816
5860
|
suggestionTracker = new SuggestionTracker(3);
|
|
@@ -5828,7 +5872,7 @@ var init_state2 = __esm({
|
|
|
5828
5872
|
"2h": 2 * 60 * 6e4
|
|
5829
5873
|
};
|
|
5830
5874
|
autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
5831
|
-
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
5875
|
+
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path16.join(os11.tmpdir(), "node9-activity.sock");
|
|
5832
5876
|
ACTIVITY_RING_SIZE = 100;
|
|
5833
5877
|
activityRing = [];
|
|
5834
5878
|
SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
@@ -5848,7 +5892,7 @@ var init_state2 = __esm({
|
|
|
5848
5892
|
|
|
5849
5893
|
// src/config/patch.ts
|
|
5850
5894
|
import fs14 from "fs";
|
|
5851
|
-
import
|
|
5895
|
+
import path17 from "path";
|
|
5852
5896
|
import os12 from "os";
|
|
5853
5897
|
function patchConfig(configPath, patch) {
|
|
5854
5898
|
let config = {};
|
|
@@ -5873,7 +5917,7 @@ function patchConfig(configPath, patch) {
|
|
|
5873
5917
|
ignored.push(patch.toolName);
|
|
5874
5918
|
}
|
|
5875
5919
|
}
|
|
5876
|
-
const dir =
|
|
5920
|
+
const dir = path17.dirname(configPath);
|
|
5877
5921
|
fs14.mkdirSync(dir, { recursive: true });
|
|
5878
5922
|
const tmp = configPath + ".node9-tmp";
|
|
5879
5923
|
try {
|
|
@@ -5899,14 +5943,14 @@ var GLOBAL_CONFIG_PATH;
|
|
|
5899
5943
|
var init_patch = __esm({
|
|
5900
5944
|
"src/config/patch.ts"() {
|
|
5901
5945
|
"use strict";
|
|
5902
|
-
GLOBAL_CONFIG_PATH =
|
|
5946
|
+
GLOBAL_CONFIG_PATH = path17.join(os12.homedir(), ".node9", "config.json");
|
|
5903
5947
|
}
|
|
5904
5948
|
});
|
|
5905
5949
|
|
|
5906
5950
|
// src/daemon/server.ts
|
|
5907
5951
|
import http from "http";
|
|
5908
5952
|
import fs15 from "fs";
|
|
5909
|
-
import
|
|
5953
|
+
import path18 from "path";
|
|
5910
5954
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
5911
5955
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
5912
5956
|
import chalk2 from "chalk";
|
|
@@ -6088,7 +6132,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
6088
6132
|
status: "pending"
|
|
6089
6133
|
});
|
|
6090
6134
|
}
|
|
6091
|
-
const projectCwd = typeof cwd === "string" &&
|
|
6135
|
+
const projectCwd = typeof cwd === "string" && path18.isAbsolute(cwd) ? cwd : void 0;
|
|
6092
6136
|
const projectConfig = getConfig(projectCwd);
|
|
6093
6137
|
const browserEnabled = projectConfig.settings.approvers?.browser !== false;
|
|
6094
6138
|
const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
|
|
@@ -6476,8 +6520,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
6476
6520
|
const body = await readBody(req);
|
|
6477
6521
|
const data = body ? JSON.parse(body) : {};
|
|
6478
6522
|
const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
|
|
6479
|
-
const node9Dir =
|
|
6480
|
-
if (!
|
|
6523
|
+
const node9Dir = path18.dirname(GLOBAL_CONFIG_PATH);
|
|
6524
|
+
if (!path18.resolve(configPath).startsWith(node9Dir + path18.sep)) {
|
|
6481
6525
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6482
6526
|
return res.end(
|
|
6483
6527
|
JSON.stringify({ error: "configPath must be within the node9 config directory" })
|
|
@@ -6722,11 +6766,11 @@ __export(tail_exports, {
|
|
|
6722
6766
|
startTail: () => startTail
|
|
6723
6767
|
});
|
|
6724
6768
|
import http2 from "http";
|
|
6725
|
-
import
|
|
6769
|
+
import chalk17 from "chalk";
|
|
6726
6770
|
import fs24 from "fs";
|
|
6727
6771
|
import os20 from "os";
|
|
6728
|
-
import
|
|
6729
|
-
import
|
|
6772
|
+
import path27 from "path";
|
|
6773
|
+
import readline4 from "readline";
|
|
6730
6774
|
import { spawn as spawn9, execSync as execSync3 } from "child_process";
|
|
6731
6775
|
function getIcon(tool) {
|
|
6732
6776
|
const t = tool.toLowerCase();
|
|
@@ -6741,27 +6785,27 @@ function formatBase(activity) {
|
|
|
6741
6785
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
6742
6786
|
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
|
|
6743
6787
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
6744
|
-
return `${
|
|
6788
|
+
return `${chalk17.gray(time)} ${icon} ${chalk17.white.bold(toolName)} ${chalk17.dim(argsPreview)}`;
|
|
6745
6789
|
}
|
|
6746
6790
|
function renderResult(activity, result) {
|
|
6747
6791
|
const base = formatBase(activity);
|
|
6748
6792
|
let status;
|
|
6749
6793
|
if (result.status === "allow") {
|
|
6750
|
-
status =
|
|
6794
|
+
status = chalk17.green("\u2713 ALLOW");
|
|
6751
6795
|
} else if (result.status === "dlp") {
|
|
6752
|
-
status =
|
|
6796
|
+
status = chalk17.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
6753
6797
|
} else {
|
|
6754
|
-
status =
|
|
6798
|
+
status = chalk17.red("\u2717 BLOCK");
|
|
6755
6799
|
}
|
|
6756
6800
|
if (process.stdout.isTTY) {
|
|
6757
|
-
|
|
6758
|
-
|
|
6801
|
+
readline4.clearLine(process.stdout, 0);
|
|
6802
|
+
readline4.cursorTo(process.stdout, 0);
|
|
6759
6803
|
}
|
|
6760
6804
|
console.log(`${base} ${status}`);
|
|
6761
6805
|
}
|
|
6762
6806
|
function renderPending(activity) {
|
|
6763
6807
|
if (!process.stdout.isTTY) return;
|
|
6764
|
-
process.stdout.write(`${formatBase(activity)} ${
|
|
6808
|
+
process.stdout.write(`${formatBase(activity)} ${chalk17.yellow("\u25CF \u2026")}\r`);
|
|
6765
6809
|
}
|
|
6766
6810
|
async function ensureDaemon() {
|
|
6767
6811
|
let pidPort = null;
|
|
@@ -6770,7 +6814,7 @@ async function ensureDaemon() {
|
|
|
6770
6814
|
const { port } = JSON.parse(fs24.readFileSync(PID_FILE, "utf-8"));
|
|
6771
6815
|
pidPort = port;
|
|
6772
6816
|
} catch {
|
|
6773
|
-
console.error(
|
|
6817
|
+
console.error(chalk17.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
6774
6818
|
}
|
|
6775
6819
|
}
|
|
6776
6820
|
const checkPort = pidPort ?? DAEMON_PORT;
|
|
@@ -6781,7 +6825,7 @@ async function ensureDaemon() {
|
|
|
6781
6825
|
if (res.ok) return checkPort;
|
|
6782
6826
|
} catch {
|
|
6783
6827
|
}
|
|
6784
|
-
console.log(
|
|
6828
|
+
console.log(chalk17.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
6785
6829
|
const child = spawn9(process.execPath, [process.argv[1], "daemon"], {
|
|
6786
6830
|
detached: true,
|
|
6787
6831
|
stdio: "ignore",
|
|
@@ -6798,7 +6842,7 @@ async function ensureDaemon() {
|
|
|
6798
6842
|
} catch {
|
|
6799
6843
|
}
|
|
6800
6844
|
}
|
|
6801
|
-
console.error(
|
|
6845
|
+
console.error(chalk17.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
6802
6846
|
process.exit(1);
|
|
6803
6847
|
}
|
|
6804
6848
|
function postDecisionHttp(id, decision, csrfToken, port, opts) {
|
|
@@ -6840,23 +6884,23 @@ function buildCardLines(req, localCount = 0) {
|
|
|
6840
6884
|
const blockedBy = req.riskMetadata?.blockedByLabel ?? "Policy rule";
|
|
6841
6885
|
const lines = [
|
|
6842
6886
|
``,
|
|
6843
|
-
`${
|
|
6844
|
-
`${CYAN}\u2551${
|
|
6845
|
-
`${CYAN}\u2551${
|
|
6887
|
+
`${BOLD2}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET2}`,
|
|
6888
|
+
`${CYAN}\u2551${RESET2} Tool: ${BOLD2}${req.toolName}${RESET2}`,
|
|
6889
|
+
`${CYAN}\u2551${RESET2} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET2}`
|
|
6846
6890
|
];
|
|
6847
6891
|
if (req.riskMetadata?.ruleName && blockedBy.includes("Taint")) {
|
|
6848
|
-
lines.push(`${CYAN}\u2551${
|
|
6892
|
+
lines.push(`${CYAN}\u2551${RESET2} ${YELLOW}\u26A0 ${req.riskMetadata.ruleName}${RESET2}`);
|
|
6849
6893
|
}
|
|
6850
|
-
lines.push(`${CYAN}\u2551${
|
|
6894
|
+
lines.push(`${CYAN}\u2551${RESET2} Args: ${GRAY}${argsPreview}${RESET2}`);
|
|
6851
6895
|
if (localCount >= 2) {
|
|
6852
6896
|
lines.push(
|
|
6853
|
-
`${CYAN}\u2551${
|
|
6897
|
+
`${CYAN}\u2551${RESET2} ${YELLOW}\u{1F4A1}${RESET2} Approved ${localCount}\xD7 before \u2014 ${BOLD2}[a]${RESET2}${YELLOW} creates a permanent rule${RESET2}`
|
|
6854
6898
|
);
|
|
6855
6899
|
}
|
|
6856
6900
|
lines.push(
|
|
6857
|
-
`${CYAN}\u255A${
|
|
6901
|
+
`${CYAN}\u255A${RESET2}`,
|
|
6858
6902
|
``,
|
|
6859
|
-
` ${
|
|
6903
|
+
` ${BOLD2}${GREEN}[\u21B5/y]${RESET2} Allow ${BOLD2}${RED}[n]${RESET2} Deny ${BOLD2}${YELLOW}[a]${RESET2} Always Allow ${BOLD2}${CYAN}[t]${RESET2} Trust 30m`,
|
|
6860
6904
|
``
|
|
6861
6905
|
);
|
|
6862
6906
|
return lines;
|
|
@@ -6866,23 +6910,23 @@ function buildRecoveryCardLines(req) {
|
|
|
6866
6910
|
const command = typeof argsObj?.command === "string" ? argsObj.command : JSON.stringify(req.args ?? {}).replace(/\s+/g, " ").slice(0, 60);
|
|
6867
6911
|
const ruleName = req.riskMetadata?.ruleName?.replace(/^Smart Rule:\s*/i, "") ?? "policy rule";
|
|
6868
6912
|
const recoveryCommand = req.recoveryCommand;
|
|
6869
|
-
const interactiveLines = req.viewOnly ? [` ${GRAY}\u2192 Awaiting decision from interactive terminal...${
|
|
6870
|
-
` ${
|
|
6871
|
-
` ${
|
|
6872
|
-
` ${
|
|
6913
|
+
const interactiveLines = req.viewOnly ? [` ${GRAY}\u2192 Awaiting decision from interactive terminal...${RESET2}`] : [
|
|
6914
|
+
` ${BOLD2}${GREEN}[1]${RESET2} Allow anyway ${GRAY}(override policy)${RESET2}`,
|
|
6915
|
+
` ${BOLD2}${YELLOW}[2]${RESET2} Redirect AI: "Run '${recoveryCommand}' first, then retry"`,
|
|
6916
|
+
` ${BOLD2}${RED}[3]${RESET2} Deny & stop ${GRAY}(hard block)${RESET2}`,
|
|
6873
6917
|
``,
|
|
6874
|
-
` ${GRAY}[Timeout: auto-deny]${
|
|
6918
|
+
` ${GRAY}[Timeout: auto-deny]${RESET2}`,
|
|
6875
6919
|
` Select [1-3]: `
|
|
6876
6920
|
];
|
|
6877
6921
|
return [
|
|
6878
6922
|
``,
|
|
6879
|
-
`${
|
|
6880
|
-
`\u{1F6E1}\uFE0F ${
|
|
6881
|
-
`${YELLOW}\u26A0\uFE0F Rule: ${ruleName}${
|
|
6882
|
-
`${CYAN}${DIVIDER}${
|
|
6883
|
-
...!req.viewOnly ? [`${
|
|
6923
|
+
`${BOLD2}${CYAN}${DIVIDER}${RESET2}`,
|
|
6924
|
+
`\u{1F6E1}\uFE0F ${BOLD2}NODE9 STATE GUARD:${RESET2} '${BOLD2}${command}${RESET2}'`,
|
|
6925
|
+
`${YELLOW}\u26A0\uFE0F Rule: ${ruleName}${RESET2}`,
|
|
6926
|
+
`${CYAN}${DIVIDER}${RESET2}`,
|
|
6927
|
+
...!req.viewOnly ? [`${BOLD2}What would you like to do?${RESET2}`, ``] : [],
|
|
6884
6928
|
...interactiveLines,
|
|
6885
|
-
`${CYAN}${DIVIDER}${
|
|
6929
|
+
`${CYAN}${DIVIDER}${RESET2}`,
|
|
6886
6930
|
``
|
|
6887
6931
|
];
|
|
6888
6932
|
}
|
|
@@ -6912,7 +6956,7 @@ async function startTail(options = {}) {
|
|
|
6912
6956
|
req2.end();
|
|
6913
6957
|
});
|
|
6914
6958
|
if (result.ok) {
|
|
6915
|
-
console.log(
|
|
6959
|
+
console.log(chalk17.green("\u2713 Flight Recorder buffer cleared."));
|
|
6916
6960
|
} else if (result.code === "ECONNREFUSED") {
|
|
6917
6961
|
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
6918
6962
|
} else if (result.code === "ETIMEDOUT") {
|
|
@@ -6931,10 +6975,10 @@ async function startTail(options = {}) {
|
|
|
6931
6975
|
let cancelActiveCard = null;
|
|
6932
6976
|
const localAllowCounts = /* @__PURE__ */ new Map();
|
|
6933
6977
|
const canApprove = process.stdout.isTTY && process.stdin.isTTY;
|
|
6934
|
-
if (canApprove)
|
|
6978
|
+
if (canApprove) readline4.emitKeypressEvents(process.stdin);
|
|
6935
6979
|
function clearCard() {
|
|
6936
6980
|
if (cardLineCount > 0) {
|
|
6937
|
-
|
|
6981
|
+
readline4.moveCursor(process.stdout, 0, -cardLineCount);
|
|
6938
6982
|
process.stdout.write(ERASE_DOWN);
|
|
6939
6983
|
cardLineCount = 0;
|
|
6940
6984
|
}
|
|
@@ -6984,8 +7028,8 @@ async function startTail(options = {}) {
|
|
|
6984
7028
|
localAllowCounts.get(req2.toolName) ?? 0
|
|
6985
7029
|
)
|
|
6986
7030
|
);
|
|
6987
|
-
const decisionStamp = action === "always-allow" ?
|
|
6988
|
-
stampedLines.push(` ${
|
|
7031
|
+
const decisionStamp = action === "always-allow" ? chalk17.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk17.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk17.green("\u2713 ALLOWED") : action === "redirect" ? chalk17.yellow("\u21A9 REDIRECT AI") : chalk17.red("\u2717 DENIED");
|
|
7032
|
+
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
|
|
6989
7033
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
6990
7034
|
process.stdout.write(SHOW_CURSOR);
|
|
6991
7035
|
cardLineCount = 0;
|
|
@@ -7013,7 +7057,7 @@ async function startTail(options = {}) {
|
|
|
7013
7057
|
postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err) => {
|
|
7014
7058
|
try {
|
|
7015
7059
|
fs24.appendFileSync(
|
|
7016
|
-
|
|
7060
|
+
path27.join(os20.homedir(), ".node9", "hook-debug.log"),
|
|
7017
7061
|
`[tail] POST /decision failed: ${String(err)}
|
|
7018
7062
|
`
|
|
7019
7063
|
);
|
|
@@ -7035,8 +7079,8 @@ async function startTail(options = {}) {
|
|
|
7035
7079
|
);
|
|
7036
7080
|
const stampedLines = buildCardLines(req2, priorCount);
|
|
7037
7081
|
if (externalDecision) {
|
|
7038
|
-
const source = externalDecision === "allow" ?
|
|
7039
|
-
stampedLines.push(` ${
|
|
7082
|
+
const source = externalDecision === "allow" ? chalk17.green("\u2713 ALLOWED") : chalk17.red("\u2717 DENIED");
|
|
7083
|
+
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
|
|
7040
7084
|
}
|
|
7041
7085
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
7042
7086
|
process.stdout.write(SHOW_CURSOR);
|
|
@@ -7094,41 +7138,41 @@ async function startTail(options = {}) {
|
|
|
7094
7138
|
}
|
|
7095
7139
|
} catch {
|
|
7096
7140
|
}
|
|
7097
|
-
console.log(
|
|
7098
|
-
\u{1F6F0}\uFE0F Node9 tail `) +
|
|
7141
|
+
console.log(chalk17.cyan.bold(`
|
|
7142
|
+
\u{1F6F0}\uFE0F Node9 tail `) + chalk17.dim(`\u2192 ${dashboardUrl}`));
|
|
7099
7143
|
if (canApprove) {
|
|
7100
7144
|
console.log(
|
|
7101
|
-
|
|
7145
|
+
chalk17.dim("Interactive approvals: [\u21B5/y] Allow [n] Deny [a] Always Allow [t] Trust 30m")
|
|
7102
7146
|
);
|
|
7103
7147
|
}
|
|
7104
7148
|
if (options.history) {
|
|
7105
|
-
console.log(
|
|
7149
|
+
console.log(chalk17.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
|
|
7106
7150
|
} else {
|
|
7107
7151
|
console.log(
|
|
7108
|
-
|
|
7152
|
+
chalk17.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
|
|
7109
7153
|
);
|
|
7110
7154
|
}
|
|
7111
7155
|
process.on("SIGINT", () => {
|
|
7112
7156
|
clearCard();
|
|
7113
7157
|
process.stdout.write(SHOW_CURSOR);
|
|
7114
7158
|
if (process.stdout.isTTY) {
|
|
7115
|
-
|
|
7116
|
-
|
|
7159
|
+
readline4.clearLine(process.stdout, 0);
|
|
7160
|
+
readline4.cursorTo(process.stdout, 0);
|
|
7117
7161
|
}
|
|
7118
|
-
console.log(
|
|
7162
|
+
console.log(chalk17.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
7119
7163
|
process.exit(0);
|
|
7120
7164
|
});
|
|
7121
7165
|
const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
|
|
7122
7166
|
const req = http2.get(sseUrl, (res) => {
|
|
7123
7167
|
if (res.statusCode !== 200) {
|
|
7124
|
-
console.error(
|
|
7168
|
+
console.error(chalk17.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
7125
7169
|
process.exit(1);
|
|
7126
7170
|
}
|
|
7127
7171
|
let currentEvent = "";
|
|
7128
7172
|
let currentData = "";
|
|
7129
7173
|
res.on("error", () => {
|
|
7130
7174
|
});
|
|
7131
|
-
const rl =
|
|
7175
|
+
const rl = readline4.createInterface({ input: res, crlfDelay: Infinity });
|
|
7132
7176
|
rl.on("error", () => {
|
|
7133
7177
|
});
|
|
7134
7178
|
rl.on("line", (line) => {
|
|
@@ -7148,10 +7192,10 @@ async function startTail(options = {}) {
|
|
|
7148
7192
|
clearCard();
|
|
7149
7193
|
process.stdout.write(SHOW_CURSOR);
|
|
7150
7194
|
if (process.stdout.isTTY) {
|
|
7151
|
-
|
|
7152
|
-
|
|
7195
|
+
readline4.clearLine(process.stdout, 0);
|
|
7196
|
+
readline4.cursorTo(process.stdout, 0);
|
|
7153
7197
|
}
|
|
7154
|
-
console.log(
|
|
7198
|
+
console.log(chalk17.red("\n\u274C Daemon disconnected."));
|
|
7155
7199
|
process.exit(1);
|
|
7156
7200
|
});
|
|
7157
7201
|
});
|
|
@@ -7237,19 +7281,19 @@ async function startTail(options = {}) {
|
|
|
7237
7281
|
}
|
|
7238
7282
|
req.on("error", (err) => {
|
|
7239
7283
|
const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
|
|
7240
|
-
console.error(
|
|
7284
|
+
console.error(chalk17.red(`
|
|
7241
7285
|
\u274C ${msg}`));
|
|
7242
7286
|
process.exit(1);
|
|
7243
7287
|
});
|
|
7244
7288
|
}
|
|
7245
|
-
var PID_FILE, ICONS,
|
|
7289
|
+
var PID_FILE, ICONS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, DIVIDER;
|
|
7246
7290
|
var init_tail = __esm({
|
|
7247
7291
|
"src/tui/tail.ts"() {
|
|
7248
7292
|
"use strict";
|
|
7249
7293
|
init_daemon2();
|
|
7250
7294
|
init_daemon();
|
|
7251
7295
|
init_core();
|
|
7252
|
-
PID_FILE =
|
|
7296
|
+
PID_FILE = path27.join(os20.homedir(), ".node9", "daemon.pid");
|
|
7253
7297
|
ICONS = {
|
|
7254
7298
|
bash: "\u{1F4BB}",
|
|
7255
7299
|
shell: "\u{1F4BB}",
|
|
@@ -7267,8 +7311,8 @@ var init_tail = __esm({
|
|
|
7267
7311
|
delete: "\u{1F5D1}\uFE0F",
|
|
7268
7312
|
web: "\u{1F310}"
|
|
7269
7313
|
};
|
|
7270
|
-
|
|
7271
|
-
|
|
7314
|
+
RESET2 = "\x1B[0m";
|
|
7315
|
+
BOLD2 = "\x1B[1m";
|
|
7272
7316
|
RED = "\x1B[31m";
|
|
7273
7317
|
YELLOW = "\x1B[33m";
|
|
7274
7318
|
CYAN = "\x1B[36m";
|
|
@@ -7289,7 +7333,7 @@ __export(hud_exports, {
|
|
|
7289
7333
|
renderEnvironmentLine: () => renderEnvironmentLine
|
|
7290
7334
|
});
|
|
7291
7335
|
import fs25 from "fs";
|
|
7292
|
-
import
|
|
7336
|
+
import path28 from "path";
|
|
7293
7337
|
import os21 from "os";
|
|
7294
7338
|
import http3 from "http";
|
|
7295
7339
|
async function readStdin() {
|
|
@@ -7341,19 +7385,19 @@ function queryDaemon() {
|
|
|
7341
7385
|
});
|
|
7342
7386
|
}
|
|
7343
7387
|
function dim(s) {
|
|
7344
|
-
return `${DIM}${s}${
|
|
7388
|
+
return `${DIM}${s}${RESET3}`;
|
|
7345
7389
|
}
|
|
7346
7390
|
function bold(s) {
|
|
7347
|
-
return `${
|
|
7391
|
+
return `${BOLD3}${s}${RESET3}`;
|
|
7348
7392
|
}
|
|
7349
7393
|
function color(c, s) {
|
|
7350
|
-
return `${c}${s}${
|
|
7394
|
+
return `${c}${s}${RESET3}`;
|
|
7351
7395
|
}
|
|
7352
7396
|
function progressBar(pct, warnAt = 70, critAt = 85) {
|
|
7353
7397
|
const filled = Math.round(Math.min(pct, 100) / 100 * BAR_WIDTH);
|
|
7354
7398
|
const bar = BAR_FILLED.repeat(filled) + BAR_EMPTY.repeat(BAR_WIDTH - filled);
|
|
7355
7399
|
const c = pct >= critAt ? RED2 : pct >= warnAt ? YELLOW2 : GREEN2;
|
|
7356
|
-
return `${c}${bar}${
|
|
7400
|
+
return `${c}${bar}${RESET3}`;
|
|
7357
7401
|
}
|
|
7358
7402
|
function formatTimeLeft(resetsAt) {
|
|
7359
7403
|
if (!resetsAt) return "";
|
|
@@ -7394,7 +7438,7 @@ function countRulesInDir(rulesDir) {
|
|
|
7394
7438
|
try {
|
|
7395
7439
|
for (const entry of fs25.readdirSync(rulesDir, { withFileTypes: true })) {
|
|
7396
7440
|
if (entry.isDirectory()) {
|
|
7397
|
-
count += countRulesInDir(
|
|
7441
|
+
count += countRulesInDir(path28.join(rulesDir, entry.name));
|
|
7398
7442
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
7399
7443
|
count++;
|
|
7400
7444
|
}
|
|
@@ -7405,46 +7449,46 @@ function countRulesInDir(rulesDir) {
|
|
|
7405
7449
|
}
|
|
7406
7450
|
function isSamePath(a, b) {
|
|
7407
7451
|
try {
|
|
7408
|
-
return
|
|
7452
|
+
return path28.resolve(a) === path28.resolve(b);
|
|
7409
7453
|
} catch {
|
|
7410
7454
|
return false;
|
|
7411
7455
|
}
|
|
7412
7456
|
}
|
|
7413
7457
|
function countConfigs(cwd) {
|
|
7414
7458
|
const homeDir2 = os21.homedir();
|
|
7415
|
-
const claudeDir =
|
|
7459
|
+
const claudeDir = path28.join(homeDir2, ".claude");
|
|
7416
7460
|
let claudeMdCount = 0;
|
|
7417
7461
|
let rulesCount = 0;
|
|
7418
7462
|
let hooksCount = 0;
|
|
7419
7463
|
const userMcpServers = /* @__PURE__ */ new Set();
|
|
7420
7464
|
const projectMcpServers = /* @__PURE__ */ new Set();
|
|
7421
|
-
if (fs25.existsSync(
|
|
7422
|
-
rulesCount += countRulesInDir(
|
|
7423
|
-
const userSettings =
|
|
7465
|
+
if (fs25.existsSync(path28.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
7466
|
+
rulesCount += countRulesInDir(path28.join(claudeDir, "rules"));
|
|
7467
|
+
const userSettings = path28.join(claudeDir, "settings.json");
|
|
7424
7468
|
for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
|
|
7425
7469
|
hooksCount += countHooksInFile(userSettings);
|
|
7426
|
-
const userClaudeJson =
|
|
7470
|
+
const userClaudeJson = path28.join(homeDir2, ".claude.json");
|
|
7427
7471
|
for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
|
|
7428
7472
|
for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
|
|
7429
7473
|
userMcpServers.delete(name);
|
|
7430
7474
|
}
|
|
7431
7475
|
if (cwd) {
|
|
7432
|
-
if (fs25.existsSync(
|
|
7433
|
-
if (fs25.existsSync(
|
|
7434
|
-
const projectClaudeDir =
|
|
7476
|
+
if (fs25.existsSync(path28.join(cwd, "CLAUDE.md"))) claudeMdCount++;
|
|
7477
|
+
if (fs25.existsSync(path28.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
|
|
7478
|
+
const projectClaudeDir = path28.join(cwd, ".claude");
|
|
7435
7479
|
const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
|
|
7436
7480
|
if (!overlapsUserScope) {
|
|
7437
|
-
if (fs25.existsSync(
|
|
7438
|
-
rulesCount += countRulesInDir(
|
|
7439
|
-
const projSettings =
|
|
7481
|
+
if (fs25.existsSync(path28.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
7482
|
+
rulesCount += countRulesInDir(path28.join(projectClaudeDir, "rules"));
|
|
7483
|
+
const projSettings = path28.join(projectClaudeDir, "settings.json");
|
|
7440
7484
|
for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
|
|
7441
7485
|
hooksCount += countHooksInFile(projSettings);
|
|
7442
7486
|
}
|
|
7443
|
-
if (fs25.existsSync(
|
|
7444
|
-
const localSettings =
|
|
7487
|
+
if (fs25.existsSync(path28.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
|
|
7488
|
+
const localSettings = path28.join(projectClaudeDir, "settings.local.json");
|
|
7445
7489
|
for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
|
|
7446
7490
|
hooksCount += countHooksInFile(localSettings);
|
|
7447
|
-
const mcpJsonServers = getMcpServerNames(
|
|
7491
|
+
const mcpJsonServers = getMcpServerNames(path28.join(cwd, ".mcp.json"));
|
|
7448
7492
|
const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
|
|
7449
7493
|
for (const name of disabledMcpJson) mcpJsonServers.delete(name);
|
|
7450
7494
|
for (const name of mcpJsonServers) projectMcpServers.add(name);
|
|
@@ -7557,8 +7601,8 @@ async function main() {
|
|
|
7557
7601
|
try {
|
|
7558
7602
|
const cwd = stdin.cwd ?? process.cwd();
|
|
7559
7603
|
for (const configPath of [
|
|
7560
|
-
|
|
7561
|
-
|
|
7604
|
+
path28.join(cwd, "node9.config.json"),
|
|
7605
|
+
path28.join(os21.homedir(), ".node9", "config.json")
|
|
7562
7606
|
]) {
|
|
7563
7607
|
if (!fs25.existsSync(configPath)) continue;
|
|
7564
7608
|
const cfg = JSON.parse(fs25.readFileSync(configPath, "utf-8"));
|
|
@@ -7579,13 +7623,13 @@ async function main() {
|
|
|
7579
7623
|
renderOffline();
|
|
7580
7624
|
}
|
|
7581
7625
|
}
|
|
7582
|
-
var
|
|
7626
|
+
var RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH;
|
|
7583
7627
|
var init_hud = __esm({
|
|
7584
7628
|
"src/cli/hud.ts"() {
|
|
7585
7629
|
"use strict";
|
|
7586
7630
|
init_daemon();
|
|
7587
|
-
|
|
7588
|
-
|
|
7631
|
+
RESET3 = "\x1B[0m";
|
|
7632
|
+
BOLD3 = "\x1B[1m";
|
|
7589
7633
|
DIM = "\x1B[2m";
|
|
7590
7634
|
RED2 = "\x1B[31m";
|
|
7591
7635
|
GREEN2 = "\x1B[32m";
|
|
@@ -7606,7 +7650,7 @@ import { Command } from "commander";
|
|
|
7606
7650
|
|
|
7607
7651
|
// src/setup.ts
|
|
7608
7652
|
import fs11 from "fs";
|
|
7609
|
-
import
|
|
7653
|
+
import path14 from "path";
|
|
7610
7654
|
import os10 from "os";
|
|
7611
7655
|
import chalk from "chalk";
|
|
7612
7656
|
import { confirm } from "@inquirer/prompts";
|
|
@@ -7632,7 +7676,7 @@ function readJson(filePath) {
|
|
|
7632
7676
|
return null;
|
|
7633
7677
|
}
|
|
7634
7678
|
function writeJson(filePath, data) {
|
|
7635
|
-
const dir =
|
|
7679
|
+
const dir = path14.dirname(filePath);
|
|
7636
7680
|
if (!fs11.existsSync(dir)) fs11.mkdirSync(dir, { recursive: true });
|
|
7637
7681
|
fs11.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
7638
7682
|
}
|
|
@@ -7642,8 +7686,8 @@ function isNode9Hook(cmd) {
|
|
|
7642
7686
|
}
|
|
7643
7687
|
function teardownClaude() {
|
|
7644
7688
|
const homeDir2 = os10.homedir();
|
|
7645
|
-
const hooksPath =
|
|
7646
|
-
const mcpPath =
|
|
7689
|
+
const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
|
|
7690
|
+
const mcpPath = path14.join(homeDir2, ".claude.json");
|
|
7647
7691
|
let changed = false;
|
|
7648
7692
|
const settings = readJson(hooksPath);
|
|
7649
7693
|
if (settings?.hooks) {
|
|
@@ -7692,7 +7736,7 @@ function teardownClaude() {
|
|
|
7692
7736
|
}
|
|
7693
7737
|
function teardownGemini() {
|
|
7694
7738
|
const homeDir2 = os10.homedir();
|
|
7695
|
-
const settingsPath =
|
|
7739
|
+
const settingsPath = path14.join(homeDir2, ".gemini", "settings.json");
|
|
7696
7740
|
const settings = readJson(settingsPath);
|
|
7697
7741
|
if (!settings) {
|
|
7698
7742
|
console.log(chalk.blue(" \u2139\uFE0F ~/.gemini/settings.json not found \u2014 nothing to remove"));
|
|
@@ -7731,7 +7775,7 @@ function teardownGemini() {
|
|
|
7731
7775
|
}
|
|
7732
7776
|
function teardownCursor() {
|
|
7733
7777
|
const homeDir2 = os10.homedir();
|
|
7734
|
-
const mcpPath =
|
|
7778
|
+
const mcpPath = path14.join(homeDir2, ".cursor", "mcp.json");
|
|
7735
7779
|
const mcpConfig = readJson(mcpPath);
|
|
7736
7780
|
if (!mcpConfig?.mcpServers) {
|
|
7737
7781
|
console.log(chalk.blue(" \u2139\uFE0F ~/.cursor/mcp.json not found \u2014 nothing to remove"));
|
|
@@ -7758,8 +7802,8 @@ function teardownCursor() {
|
|
|
7758
7802
|
}
|
|
7759
7803
|
async function setupClaude() {
|
|
7760
7804
|
const homeDir2 = os10.homedir();
|
|
7761
|
-
const mcpPath =
|
|
7762
|
-
const hooksPath =
|
|
7805
|
+
const mcpPath = path14.join(homeDir2, ".claude.json");
|
|
7806
|
+
const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
|
|
7763
7807
|
const claudeConfig = readJson(mcpPath) ?? {};
|
|
7764
7808
|
const settings = readJson(hooksPath) ?? {};
|
|
7765
7809
|
const servers = claudeConfig.mcpServers ?? {};
|
|
@@ -7834,7 +7878,7 @@ async function setupClaude() {
|
|
|
7834
7878
|
}
|
|
7835
7879
|
async function setupGemini() {
|
|
7836
7880
|
const homeDir2 = os10.homedir();
|
|
7837
|
-
const settingsPath =
|
|
7881
|
+
const settingsPath = path14.join(homeDir2, ".gemini", "settings.json");
|
|
7838
7882
|
const settings = readJson(settingsPath) ?? {};
|
|
7839
7883
|
const servers = settings.mcpServers ?? {};
|
|
7840
7884
|
let anythingChanged = false;
|
|
@@ -7929,14 +7973,14 @@ function detectAgents(homeDir2 = os10.homedir()) {
|
|
|
7929
7973
|
}
|
|
7930
7974
|
};
|
|
7931
7975
|
return {
|
|
7932
|
-
claude: exists(
|
|
7933
|
-
gemini: exists(
|
|
7934
|
-
cursor: exists(
|
|
7976
|
+
claude: exists(path14.join(homeDir2, ".claude")) || exists(path14.join(homeDir2, ".claude.json")),
|
|
7977
|
+
gemini: exists(path14.join(homeDir2, ".gemini")),
|
|
7978
|
+
cursor: exists(path14.join(homeDir2, ".cursor"))
|
|
7935
7979
|
};
|
|
7936
7980
|
}
|
|
7937
7981
|
async function setupCursor() {
|
|
7938
7982
|
const homeDir2 = os10.homedir();
|
|
7939
|
-
const mcpPath =
|
|
7983
|
+
const mcpPath = path14.join(homeDir2, ".cursor", "mcp.json");
|
|
7940
7984
|
const mcpConfig = readJson(mcpPath) ?? {};
|
|
7941
7985
|
const servers = mcpConfig.mcpServers ?? {};
|
|
7942
7986
|
let anythingChanged = false;
|
|
@@ -7991,7 +8035,7 @@ async function setupCursor() {
|
|
|
7991
8035
|
}
|
|
7992
8036
|
function setupHud() {
|
|
7993
8037
|
const homeDir2 = os10.homedir();
|
|
7994
|
-
const hooksPath =
|
|
8038
|
+
const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
|
|
7995
8039
|
const settings = readJson(hooksPath) ?? {};
|
|
7996
8040
|
const hudCommand = fullPathCommand("hud");
|
|
7997
8041
|
const statusLineObj = { type: "command", command: hudCommand };
|
|
@@ -8018,7 +8062,7 @@ function setupHud() {
|
|
|
8018
8062
|
}
|
|
8019
8063
|
function teardownHud() {
|
|
8020
8064
|
const homeDir2 = os10.homedir();
|
|
8021
|
-
const hooksPath =
|
|
8065
|
+
const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
|
|
8022
8066
|
const settings = readJson(hooksPath);
|
|
8023
8067
|
if (!settings) {
|
|
8024
8068
|
console.log(chalk.blue(" \u2139\uFE0F ~/.claude/settings.json not found \u2014 nothing to remove"));
|
|
@@ -8038,11 +8082,11 @@ function teardownHud() {
|
|
|
8038
8082
|
|
|
8039
8083
|
// src/cli.ts
|
|
8040
8084
|
init_daemon2();
|
|
8041
|
-
import
|
|
8085
|
+
import chalk18 from "chalk";
|
|
8042
8086
|
import fs26 from "fs";
|
|
8043
|
-
import
|
|
8087
|
+
import path29 from "path";
|
|
8044
8088
|
import os22 from "os";
|
|
8045
|
-
import { confirm as
|
|
8089
|
+
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
8046
8090
|
|
|
8047
8091
|
// src/utils/duration.ts
|
|
8048
8092
|
function parseDuration(str) {
|
|
@@ -8233,6 +8277,7 @@ function openBrowserLocal() {
|
|
|
8233
8277
|
}
|
|
8234
8278
|
}
|
|
8235
8279
|
async function autoStartDaemonAndWait() {
|
|
8280
|
+
if (process.env.NODE9_TESTING === "1") return false;
|
|
8236
8281
|
try {
|
|
8237
8282
|
const child = spawn4(process.execPath, [process.argv[1], "daemon"], {
|
|
8238
8283
|
detached: true,
|
|
@@ -8268,17 +8313,17 @@ init_config();
|
|
|
8268
8313
|
init_policy();
|
|
8269
8314
|
import chalk5 from "chalk";
|
|
8270
8315
|
import fs18 from "fs";
|
|
8271
|
-
import
|
|
8316
|
+
import path20 from "path";
|
|
8272
8317
|
import os14 from "os";
|
|
8273
8318
|
|
|
8274
8319
|
// src/undo.ts
|
|
8275
8320
|
import { spawnSync as spawnSync4, spawn as spawn5 } from "child_process";
|
|
8276
8321
|
import crypto2 from "crypto";
|
|
8277
8322
|
import fs17 from "fs";
|
|
8278
|
-
import
|
|
8323
|
+
import path19 from "path";
|
|
8279
8324
|
import os13 from "os";
|
|
8280
|
-
var SNAPSHOT_STACK_PATH =
|
|
8281
|
-
var UNDO_LATEST_PATH =
|
|
8325
|
+
var SNAPSHOT_STACK_PATH = path19.join(os13.homedir(), ".node9", "snapshots.json");
|
|
8326
|
+
var UNDO_LATEST_PATH = path19.join(os13.homedir(), ".node9", "undo_latest.txt");
|
|
8282
8327
|
var MAX_SNAPSHOTS = 10;
|
|
8283
8328
|
var GIT_TIMEOUT = 15e3;
|
|
8284
8329
|
function readStack() {
|
|
@@ -8290,20 +8335,37 @@ function readStack() {
|
|
|
8290
8335
|
return [];
|
|
8291
8336
|
}
|
|
8292
8337
|
function writeStack(stack) {
|
|
8293
|
-
const dir =
|
|
8338
|
+
const dir = path19.dirname(SNAPSHOT_STACK_PATH);
|
|
8294
8339
|
if (!fs17.existsSync(dir)) fs17.mkdirSync(dir, { recursive: true });
|
|
8295
8340
|
fs17.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
8296
8341
|
}
|
|
8342
|
+
function extractFilePath(args) {
|
|
8343
|
+
if (!args || typeof args !== "object") return null;
|
|
8344
|
+
const a = args;
|
|
8345
|
+
const fp = a.file_path ?? a.path ?? a.filename;
|
|
8346
|
+
return typeof fp === "string" ? fp : null;
|
|
8347
|
+
}
|
|
8297
8348
|
function buildArgsSummary(tool, args) {
|
|
8349
|
+
const filePath = extractFilePath(args);
|
|
8350
|
+
if (filePath) return filePath;
|
|
8298
8351
|
if (!args || typeof args !== "object") return "";
|
|
8299
8352
|
const a = args;
|
|
8300
|
-
const filePath = a.file_path ?? a.path ?? a.filename;
|
|
8301
|
-
if (typeof filePath === "string") return filePath;
|
|
8302
8353
|
const cmd = a.command ?? a.cmd;
|
|
8303
8354
|
if (typeof cmd === "string") return cmd.slice(0, 80);
|
|
8304
8355
|
const sql = a.sql ?? a.query;
|
|
8305
8356
|
if (typeof sql === "string") return sql.slice(0, 80);
|
|
8306
|
-
return
|
|
8357
|
+
return "";
|
|
8358
|
+
}
|
|
8359
|
+
function findProjectRoot(filePath) {
|
|
8360
|
+
let dir = path19.dirname(filePath);
|
|
8361
|
+
while (true) {
|
|
8362
|
+
if (fs17.existsSync(path19.join(dir, ".git")) || fs17.existsSync(path19.join(dir, "package.json"))) {
|
|
8363
|
+
return dir;
|
|
8364
|
+
}
|
|
8365
|
+
const parent = path19.dirname(dir);
|
|
8366
|
+
if (parent === dir) return process.cwd();
|
|
8367
|
+
dir = parent;
|
|
8368
|
+
}
|
|
8307
8369
|
}
|
|
8308
8370
|
function normalizeCwdForHash(cwd) {
|
|
8309
8371
|
let normalized;
|
|
@@ -8318,14 +8380,14 @@ function normalizeCwdForHash(cwd) {
|
|
|
8318
8380
|
}
|
|
8319
8381
|
function getShadowRepoDir(cwd) {
|
|
8320
8382
|
const hash = crypto2.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
|
|
8321
|
-
return
|
|
8383
|
+
return path19.join(os13.homedir(), ".node9", "snapshots", hash);
|
|
8322
8384
|
}
|
|
8323
8385
|
function cleanOrphanedIndexFiles(shadowDir) {
|
|
8324
8386
|
try {
|
|
8325
8387
|
const cutoff = Date.now() - 6e4;
|
|
8326
8388
|
for (const f of fs17.readdirSync(shadowDir)) {
|
|
8327
8389
|
if (f.startsWith("index_")) {
|
|
8328
|
-
const fp =
|
|
8390
|
+
const fp = path19.join(shadowDir, f);
|
|
8329
8391
|
try {
|
|
8330
8392
|
if (fs17.statSync(fp).mtimeMs < cutoff) fs17.unlinkSync(fp);
|
|
8331
8393
|
} catch {
|
|
@@ -8339,7 +8401,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
|
|
|
8339
8401
|
const hardcoded = [".git", ".node9"];
|
|
8340
8402
|
const lines = [...hardcoded, ...ignorePaths].join("\n");
|
|
8341
8403
|
try {
|
|
8342
|
-
fs17.writeFileSync(
|
|
8404
|
+
fs17.writeFileSync(path19.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
|
|
8343
8405
|
} catch {
|
|
8344
8406
|
}
|
|
8345
8407
|
}
|
|
@@ -8352,7 +8414,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
8352
8414
|
timeout: 3e3
|
|
8353
8415
|
});
|
|
8354
8416
|
if (check.status === 0) {
|
|
8355
|
-
const ptPath =
|
|
8417
|
+
const ptPath = path19.join(shadowDir, "project-path.txt");
|
|
8356
8418
|
try {
|
|
8357
8419
|
const stored = fs17.readFileSync(ptPath, "utf8").trim();
|
|
8358
8420
|
if (stored === normalizedCwd) return true;
|
|
@@ -8374,12 +8436,12 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
8374
8436
|
} catch {
|
|
8375
8437
|
}
|
|
8376
8438
|
const init = spawnSync4("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
|
|
8377
|
-
if (init.status !== 0) {
|
|
8378
|
-
|
|
8379
|
-
|
|
8439
|
+
if (init.status !== 0 || init.error) {
|
|
8440
|
+
const reason = init.error ? init.error.message : init.stderr?.toString();
|
|
8441
|
+
if (process.env.NODE9_DEBUG === "1") console.error("[Node9] git init --bare failed:", reason);
|
|
8380
8442
|
return false;
|
|
8381
8443
|
}
|
|
8382
|
-
const configFile =
|
|
8444
|
+
const configFile = path19.join(shadowDir, "config");
|
|
8383
8445
|
spawnSync4("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
|
|
8384
8446
|
timeout: 3e3
|
|
8385
8447
|
});
|
|
@@ -8387,7 +8449,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
8387
8449
|
timeout: 3e3
|
|
8388
8450
|
});
|
|
8389
8451
|
try {
|
|
8390
|
-
fs17.writeFileSync(
|
|
8452
|
+
fs17.writeFileSync(path19.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
|
|
8391
8453
|
} catch {
|
|
8392
8454
|
}
|
|
8393
8455
|
return true;
|
|
@@ -8406,11 +8468,13 @@ function buildGitEnv(cwd) {
|
|
|
8406
8468
|
async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = []) {
|
|
8407
8469
|
let indexFile = null;
|
|
8408
8470
|
try {
|
|
8409
|
-
const
|
|
8471
|
+
const rawFilePath = extractFilePath(args);
|
|
8472
|
+
const absFilePath = rawFilePath && path19.isAbsolute(rawFilePath) ? rawFilePath : null;
|
|
8473
|
+
const cwd = absFilePath ? findProjectRoot(absFilePath) : process.cwd();
|
|
8410
8474
|
const shadowDir = getShadowRepoDir(cwd);
|
|
8411
8475
|
if (!ensureShadowRepo(shadowDir, cwd)) return null;
|
|
8412
8476
|
writeShadowExcludes(shadowDir, ignorePaths);
|
|
8413
|
-
indexFile =
|
|
8477
|
+
indexFile = path19.join(shadowDir, `index_${process.pid}_${Date.now()}`);
|
|
8414
8478
|
const shadowEnv = {
|
|
8415
8479
|
...process.env,
|
|
8416
8480
|
GIT_DIR: shadowDir,
|
|
@@ -8429,15 +8493,53 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
8429
8493
|
const commitHash = commitRes.stdout?.toString().trim();
|
|
8430
8494
|
if (!commitHash || commitRes.status !== 0) return null;
|
|
8431
8495
|
const stack = readStack();
|
|
8496
|
+
const prevEntry = [...stack].reverse().find((e) => e.cwd === cwd);
|
|
8497
|
+
let capturedFiles = [];
|
|
8498
|
+
let capturedDiff = null;
|
|
8499
|
+
if (prevEntry) {
|
|
8500
|
+
const filesRes = spawnSync4("git", ["diff", "--name-only", prevEntry.hash, commitHash], {
|
|
8501
|
+
env: shadowEnv,
|
|
8502
|
+
timeout: GIT_TIMEOUT
|
|
8503
|
+
});
|
|
8504
|
+
if (filesRes.status === 0) {
|
|
8505
|
+
capturedFiles = filesRes.stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
8506
|
+
}
|
|
8507
|
+
const diffRes = spawnSync4("git", ["diff", prevEntry.hash, commitHash], {
|
|
8508
|
+
env: shadowEnv,
|
|
8509
|
+
timeout: GIT_TIMEOUT
|
|
8510
|
+
});
|
|
8511
|
+
if (diffRes.status === 0) {
|
|
8512
|
+
capturedDiff = diffRes.stdout?.toString() || null;
|
|
8513
|
+
}
|
|
8514
|
+
} else {
|
|
8515
|
+
const filesRes = spawnSync4("git", ["ls-tree", "-r", "--name-only", commitHash], {
|
|
8516
|
+
env: shadowEnv,
|
|
8517
|
+
timeout: GIT_TIMEOUT
|
|
8518
|
+
});
|
|
8519
|
+
if (filesRes.status === 0) {
|
|
8520
|
+
capturedFiles = filesRes.stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
8521
|
+
}
|
|
8522
|
+
capturedDiff = null;
|
|
8523
|
+
}
|
|
8432
8524
|
stack.push({
|
|
8433
8525
|
hash: commitHash,
|
|
8434
8526
|
tool,
|
|
8435
8527
|
argsSummary: buildArgsSummary(tool, args),
|
|
8528
|
+
files: capturedFiles,
|
|
8529
|
+
diff: capturedDiff,
|
|
8436
8530
|
cwd,
|
|
8437
8531
|
timestamp: Date.now()
|
|
8438
8532
|
});
|
|
8439
8533
|
const shouldGc = stack.length % 5 === 0;
|
|
8440
|
-
|
|
8534
|
+
let cwdCount = 0;
|
|
8535
|
+
let oldestCwdIdx = -1;
|
|
8536
|
+
for (let i = 0; i < stack.length; i++) {
|
|
8537
|
+
if (stack[i].cwd === cwd) {
|
|
8538
|
+
if (oldestCwdIdx === -1) oldestCwdIdx = i;
|
|
8539
|
+
cwdCount++;
|
|
8540
|
+
}
|
|
8541
|
+
}
|
|
8542
|
+
if (cwdCount > MAX_SNAPSHOTS) stack.splice(oldestCwdIdx, 1);
|
|
8441
8543
|
writeStack(stack);
|
|
8442
8544
|
fs17.writeFileSync(UNDO_LATEST_PATH, commitHash);
|
|
8443
8545
|
if (shouldGc) {
|
|
@@ -8493,14 +8595,21 @@ function applyUndo(hash, cwd) {
|
|
|
8493
8595
|
env,
|
|
8494
8596
|
timeout: GIT_TIMEOUT
|
|
8495
8597
|
});
|
|
8496
|
-
if (restore.status !== 0)
|
|
8598
|
+
if (restore.status !== 0 || restore.error) {
|
|
8599
|
+
if (process.env.NODE9_DEBUG === "1") {
|
|
8600
|
+
const msg = restore.error ? restore.error.message : restore.stderr?.toString();
|
|
8601
|
+
console.error("[Node9] git restore failed:", msg);
|
|
8602
|
+
}
|
|
8603
|
+
return false;
|
|
8604
|
+
}
|
|
8497
8605
|
const lsTree = spawnSync4("git", ["ls-tree", "-r", "--name-only", hash], {
|
|
8498
8606
|
cwd: dir,
|
|
8499
8607
|
env,
|
|
8500
8608
|
timeout: GIT_TIMEOUT
|
|
8501
8609
|
});
|
|
8502
8610
|
if (lsTree.status !== 0) {
|
|
8503
|
-
|
|
8611
|
+
const errorMsg = lsTree.stderr?.toString() || "Unknown git error";
|
|
8612
|
+
process.stderr.write(`[Node9] applyUndo: git ls-tree failed for hash ${hash}: ${errorMsg}
|
|
8504
8613
|
`);
|
|
8505
8614
|
return false;
|
|
8506
8615
|
}
|
|
@@ -8519,7 +8628,7 @@ function applyUndo(hash, cwd) {
|
|
|
8519
8628
|
timeout: GIT_TIMEOUT
|
|
8520
8629
|
}).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
8521
8630
|
for (const file of [...tracked, ...untracked]) {
|
|
8522
|
-
const fullPath =
|
|
8631
|
+
const fullPath = path19.join(dir, file);
|
|
8523
8632
|
if (!snapshotFiles.has(file) && fs17.existsSync(fullPath)) {
|
|
8524
8633
|
fs17.unlinkSync(fullPath);
|
|
8525
8634
|
}
|
|
@@ -8545,7 +8654,7 @@ function registerCheckCommand(program2) {
|
|
|
8545
8654
|
} catch (err) {
|
|
8546
8655
|
const tempConfig = getConfig();
|
|
8547
8656
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
8548
|
-
const logPath =
|
|
8657
|
+
const logPath = path20.join(os14.homedir(), ".node9", "hook-debug.log");
|
|
8549
8658
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
8550
8659
|
fs18.appendFileSync(
|
|
8551
8660
|
logPath,
|
|
@@ -8558,9 +8667,9 @@ RAW: ${raw}
|
|
|
8558
8667
|
}
|
|
8559
8668
|
const config = getConfig(payload.cwd || void 0);
|
|
8560
8669
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
8561
|
-
const logPath =
|
|
8562
|
-
if (!fs18.existsSync(
|
|
8563
|
-
fs18.mkdirSync(
|
|
8670
|
+
const logPath = path20.join(os14.homedir(), ".node9", "hook-debug.log");
|
|
8671
|
+
if (!fs18.existsSync(path20.dirname(logPath)))
|
|
8672
|
+
fs18.mkdirSync(path20.dirname(logPath), { recursive: true });
|
|
8564
8673
|
fs18.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
8565
8674
|
`);
|
|
8566
8675
|
}
|
|
@@ -8625,7 +8734,7 @@ RAW: ${raw}
|
|
|
8625
8734
|
if (shouldSnapshot(toolName, toolInput, config)) {
|
|
8626
8735
|
await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
|
|
8627
8736
|
}
|
|
8628
|
-
const safeCwdForAuth = typeof payload.cwd === "string" &&
|
|
8737
|
+
const safeCwdForAuth = typeof payload.cwd === "string" && path20.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
8629
8738
|
const result = await authorizeHeadless(toolName, toolInput, meta, {
|
|
8630
8739
|
cwd: safeCwdForAuth
|
|
8631
8740
|
});
|
|
@@ -8669,7 +8778,7 @@ RAW: ${raw}
|
|
|
8669
8778
|
});
|
|
8670
8779
|
} catch (err) {
|
|
8671
8780
|
if (process.env.NODE9_DEBUG === "1") {
|
|
8672
|
-
const logPath =
|
|
8781
|
+
const logPath = path20.join(os14.homedir(), ".node9", "hook-debug.log");
|
|
8673
8782
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
8674
8783
|
fs18.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
8675
8784
|
`);
|
|
@@ -8709,7 +8818,7 @@ init_audit();
|
|
|
8709
8818
|
init_config();
|
|
8710
8819
|
init_policy();
|
|
8711
8820
|
import fs19 from "fs";
|
|
8712
|
-
import
|
|
8821
|
+
import path21 from "path";
|
|
8713
8822
|
import os15 from "os";
|
|
8714
8823
|
init_daemon();
|
|
8715
8824
|
|
|
@@ -8783,9 +8892,9 @@ function registerLogCommand(program2) {
|
|
|
8783
8892
|
decision: "allowed",
|
|
8784
8893
|
source: "post-hook"
|
|
8785
8894
|
};
|
|
8786
|
-
const logPath =
|
|
8787
|
-
if (!fs19.existsSync(
|
|
8788
|
-
fs19.mkdirSync(
|
|
8895
|
+
const logPath = path21.join(os15.homedir(), ".node9", "audit.log");
|
|
8896
|
+
if (!fs19.existsSync(path21.dirname(logPath)))
|
|
8897
|
+
fs19.mkdirSync(path21.dirname(logPath), { recursive: true });
|
|
8789
8898
|
fs19.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
8790
8899
|
if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
|
|
8791
8900
|
const command = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
|
|
@@ -8811,7 +8920,7 @@ function registerLogCommand(program2) {
|
|
|
8811
8920
|
}
|
|
8812
8921
|
}
|
|
8813
8922
|
}
|
|
8814
|
-
const safeCwd = typeof payload.cwd === "string" &&
|
|
8923
|
+
const safeCwd = typeof payload.cwd === "string" && path21.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
8815
8924
|
const config = getConfig(safeCwd);
|
|
8816
8925
|
if (shouldSnapshot(tool, {}, config)) {
|
|
8817
8926
|
await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
|
|
@@ -8820,7 +8929,7 @@ function registerLogCommand(program2) {
|
|
|
8820
8929
|
const msg = err instanceof Error ? err.message : String(err);
|
|
8821
8930
|
process.stderr.write(`[Node9] audit log error: ${msg}
|
|
8822
8931
|
`);
|
|
8823
|
-
const debugPath =
|
|
8932
|
+
const debugPath = path21.join(os15.homedir(), ".node9", "hook-debug.log");
|
|
8824
8933
|
try {
|
|
8825
8934
|
fs19.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
|
|
8826
8935
|
`);
|
|
@@ -9128,7 +9237,7 @@ function registerConfigShowCommand(program2) {
|
|
|
9128
9237
|
init_daemon();
|
|
9129
9238
|
import chalk7 from "chalk";
|
|
9130
9239
|
import fs20 from "fs";
|
|
9131
|
-
import
|
|
9240
|
+
import path22 from "path";
|
|
9132
9241
|
import os16 from "os";
|
|
9133
9242
|
import { execSync as execSync2 } from "child_process";
|
|
9134
9243
|
function registerDoctorCommand(program2, version2) {
|
|
@@ -9182,7 +9291,7 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9182
9291
|
);
|
|
9183
9292
|
}
|
|
9184
9293
|
section("Configuration");
|
|
9185
|
-
const globalConfigPath =
|
|
9294
|
+
const globalConfigPath = path22.join(homeDir2, ".node9", "config.json");
|
|
9186
9295
|
if (fs20.existsSync(globalConfigPath)) {
|
|
9187
9296
|
try {
|
|
9188
9297
|
JSON.parse(fs20.readFileSync(globalConfigPath, "utf-8"));
|
|
@@ -9193,7 +9302,7 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9193
9302
|
} else {
|
|
9194
9303
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
9195
9304
|
}
|
|
9196
|
-
const projectConfigPath =
|
|
9305
|
+
const projectConfigPath = path22.join(process.cwd(), "node9.config.json");
|
|
9197
9306
|
if (fs20.existsSync(projectConfigPath)) {
|
|
9198
9307
|
try {
|
|
9199
9308
|
JSON.parse(fs20.readFileSync(projectConfigPath, "utf-8"));
|
|
@@ -9205,7 +9314,7 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9205
9314
|
);
|
|
9206
9315
|
}
|
|
9207
9316
|
}
|
|
9208
|
-
const credsPath =
|
|
9317
|
+
const credsPath = path22.join(homeDir2, ".node9", "credentials.json");
|
|
9209
9318
|
if (fs20.existsSync(credsPath)) {
|
|
9210
9319
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
9211
9320
|
} else {
|
|
@@ -9215,7 +9324,7 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9215
9324
|
);
|
|
9216
9325
|
}
|
|
9217
9326
|
section("Agent Hooks");
|
|
9218
|
-
const claudeSettingsPath =
|
|
9327
|
+
const claudeSettingsPath = path22.join(homeDir2, ".claude", "settings.json");
|
|
9219
9328
|
if (fs20.existsSync(claudeSettingsPath)) {
|
|
9220
9329
|
try {
|
|
9221
9330
|
const cs = JSON.parse(fs20.readFileSync(claudeSettingsPath, "utf-8"));
|
|
@@ -9234,7 +9343,7 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9234
9343
|
} else {
|
|
9235
9344
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
9236
9345
|
}
|
|
9237
|
-
const geminiSettingsPath =
|
|
9346
|
+
const geminiSettingsPath = path22.join(homeDir2, ".gemini", "settings.json");
|
|
9238
9347
|
if (fs20.existsSync(geminiSettingsPath)) {
|
|
9239
9348
|
try {
|
|
9240
9349
|
const gs = JSON.parse(fs20.readFileSync(geminiSettingsPath, "utf-8"));
|
|
@@ -9253,7 +9362,7 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9253
9362
|
} else {
|
|
9254
9363
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
9255
9364
|
}
|
|
9256
|
-
const cursorHooksPath =
|
|
9365
|
+
const cursorHooksPath = path22.join(homeDir2, ".cursor", "hooks.json");
|
|
9257
9366
|
if (fs20.existsSync(cursorHooksPath)) {
|
|
9258
9367
|
try {
|
|
9259
9368
|
const cur = JSON.parse(fs20.readFileSync(cursorHooksPath, "utf-8"));
|
|
@@ -9295,7 +9404,7 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9295
9404
|
// src/cli/commands/audit.ts
|
|
9296
9405
|
import chalk8 from "chalk";
|
|
9297
9406
|
import fs21 from "fs";
|
|
9298
|
-
import
|
|
9407
|
+
import path23 from "path";
|
|
9299
9408
|
import os17 from "os";
|
|
9300
9409
|
function formatRelativeTime(timestamp) {
|
|
9301
9410
|
const diff = Date.now() - new Date(timestamp).getTime();
|
|
@@ -9309,7 +9418,7 @@ function formatRelativeTime(timestamp) {
|
|
|
9309
9418
|
}
|
|
9310
9419
|
function registerAuditCommand(program2) {
|
|
9311
9420
|
program2.command("audit").description("View local execution audit log").option("--tail <n>", "Number of entries to show", "20").option("--tool <pattern>", "Filter by tool name (substring match)").option("--deny", "Show only denied actions").option("--json", "Output raw JSON").action((options) => {
|
|
9312
|
-
const logPath =
|
|
9421
|
+
const logPath = path23.join(os17.homedir(), ".node9", "audit.log");
|
|
9313
9422
|
if (!fs21.existsSync(logPath)) {
|
|
9314
9423
|
console.log(
|
|
9315
9424
|
chalk8.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
@@ -9437,7 +9546,7 @@ init_core();
|
|
|
9437
9546
|
init_daemon();
|
|
9438
9547
|
import chalk10 from "chalk";
|
|
9439
9548
|
import fs22 from "fs";
|
|
9440
|
-
import
|
|
9549
|
+
import path24 from "path";
|
|
9441
9550
|
import os18 from "os";
|
|
9442
9551
|
function readJson2(filePath) {
|
|
9443
9552
|
try {
|
|
@@ -9506,8 +9615,8 @@ function registerStatusCommand(program2) {
|
|
|
9506
9615
|
console.log("");
|
|
9507
9616
|
const modeLabel = settings.mode === "audit" ? chalk10.blue("audit") : settings.mode === "strict" ? chalk10.red("strict") : chalk10.white("standard");
|
|
9508
9617
|
console.log(` Mode: ${modeLabel}`);
|
|
9509
|
-
const projectConfig =
|
|
9510
|
-
const globalConfig =
|
|
9618
|
+
const projectConfig = path24.join(process.cwd(), "node9.config.json");
|
|
9619
|
+
const globalConfig = path24.join(os18.homedir(), ".node9", "config.json");
|
|
9511
9620
|
console.log(
|
|
9512
9621
|
` Local: ${fs22.existsSync(projectConfig) ? chalk10.green("Active (node9.config.json)") : chalk10.gray("Not present")}`
|
|
9513
9622
|
);
|
|
@@ -9521,13 +9630,13 @@ function registerStatusCommand(program2) {
|
|
|
9521
9630
|
}
|
|
9522
9631
|
const homeDir2 = os18.homedir();
|
|
9523
9632
|
const claudeSettings = readJson2(
|
|
9524
|
-
|
|
9633
|
+
path24.join(homeDir2, ".claude", "settings.json")
|
|
9525
9634
|
);
|
|
9526
|
-
const claudeConfig = readJson2(
|
|
9635
|
+
const claudeConfig = readJson2(path24.join(homeDir2, ".claude.json"));
|
|
9527
9636
|
const geminiSettings = readJson2(
|
|
9528
|
-
|
|
9637
|
+
path24.join(homeDir2, ".gemini", "settings.json")
|
|
9529
9638
|
);
|
|
9530
|
-
const cursorConfig = readJson2(
|
|
9639
|
+
const cursorConfig = readJson2(path24.join(homeDir2, ".cursor", "mcp.json"));
|
|
9531
9640
|
const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
|
|
9532
9641
|
if (agentFound) {
|
|
9533
9642
|
console.log("");
|
|
@@ -9588,12 +9697,12 @@ function registerStatusCommand(program2) {
|
|
|
9588
9697
|
init_core();
|
|
9589
9698
|
import chalk11 from "chalk";
|
|
9590
9699
|
import fs23 from "fs";
|
|
9591
|
-
import
|
|
9700
|
+
import path25 from "path";
|
|
9592
9701
|
import os19 from "os";
|
|
9593
9702
|
function registerInitCommand(program2) {
|
|
9594
9703
|
program2.command("init").description("Set up Node9: create config and wire all detected AI agents").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").option("--skip-setup", "Only create config \u2014 do not wire AI agents").action(async (options) => {
|
|
9595
9704
|
console.log(chalk11.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
|
|
9596
|
-
const configPath =
|
|
9705
|
+
const configPath = path25.join(os19.homedir(), ".node9", "config.json");
|
|
9597
9706
|
if (fs23.existsSync(configPath) && !options.force) {
|
|
9598
9707
|
console.log(chalk11.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
9599
9708
|
} else {
|
|
@@ -9603,7 +9712,7 @@ function registerInitCommand(program2) {
|
|
|
9603
9712
|
...DEFAULT_CONFIG,
|
|
9604
9713
|
settings: { ...DEFAULT_CONFIG.settings, mode: safeMode }
|
|
9605
9714
|
};
|
|
9606
|
-
const dir =
|
|
9715
|
+
const dir = path25.dirname(configPath);
|
|
9607
9716
|
if (!fs23.existsSync(dir)) fs23.mkdirSync(dir, { recursive: true });
|
|
9608
9717
|
fs23.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
9609
9718
|
console.log(chalk11.green(`\u2705 Config created: ${configPath}`));
|
|
@@ -9634,107 +9743,306 @@ function registerInitCommand(program2) {
|
|
|
9634
9743
|
else if (agent === "cursor") await setupCursor();
|
|
9635
9744
|
console.log("");
|
|
9636
9745
|
}
|
|
9746
|
+
if (detected.claude) {
|
|
9747
|
+
setupHud();
|
|
9748
|
+
console.log(chalk11.green("\u2705 node9 HUD added to Claude Code statusline"));
|
|
9749
|
+
console.log(chalk11.gray(" Restart Claude Code to activate the security statusline."));
|
|
9750
|
+
console.log("");
|
|
9751
|
+
}
|
|
9637
9752
|
console.log(chalk11.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
|
|
9638
9753
|
console.log(chalk11.gray(" Run: node9 daemon start"));
|
|
9639
9754
|
});
|
|
9640
9755
|
}
|
|
9641
9756
|
|
|
9642
9757
|
// src/cli/commands/undo.ts
|
|
9758
|
+
import path26 from "path";
|
|
9759
|
+
import chalk13 from "chalk";
|
|
9760
|
+
|
|
9761
|
+
// src/tui/undo-navigator.ts
|
|
9762
|
+
import readline2 from "readline";
|
|
9643
9763
|
import chalk12 from "chalk";
|
|
9644
|
-
|
|
9764
|
+
var RESET = "\x1B[0m";
|
|
9765
|
+
var BOLD = "\x1B[1m";
|
|
9766
|
+
var CLEAR_SCREEN = "\x1B[2J\x1B[H";
|
|
9767
|
+
var SESSION_GAP_MS = 6e4;
|
|
9768
|
+
function formatAge(timestamp) {
|
|
9769
|
+
const age = Math.round((Date.now() - timestamp) / 1e3);
|
|
9770
|
+
if (age < 60) return `${age}s ago`;
|
|
9771
|
+
if (age < 3600) return `${Math.round(age / 60)}m ago`;
|
|
9772
|
+
if (age < 86400) return `${Math.round(age / 3600)}h ago`;
|
|
9773
|
+
return `${Math.round(age / 86400)}d ago`;
|
|
9774
|
+
}
|
|
9775
|
+
function renderDiff(raw) {
|
|
9776
|
+
const lines = raw.split("\n").filter(
|
|
9777
|
+
(l) => !l.startsWith("diff --git") && !l.startsWith("index ") && !l.startsWith("Binary")
|
|
9778
|
+
);
|
|
9779
|
+
for (const line of lines) {
|
|
9780
|
+
if (line.startsWith("+++") || line.startsWith("---")) {
|
|
9781
|
+
process.stdout.write(chalk12.bold(line) + "\n");
|
|
9782
|
+
} else if (line.startsWith("+")) {
|
|
9783
|
+
process.stdout.write(chalk12.green(line) + "\n");
|
|
9784
|
+
} else if (line.startsWith("-")) {
|
|
9785
|
+
process.stdout.write(chalk12.red(line) + "\n");
|
|
9786
|
+
} else if (line.startsWith("@@")) {
|
|
9787
|
+
process.stdout.write(chalk12.cyan(line) + "\n");
|
|
9788
|
+
} else {
|
|
9789
|
+
process.stdout.write(chalk12.gray(line) + "\n");
|
|
9790
|
+
}
|
|
9791
|
+
}
|
|
9792
|
+
}
|
|
9793
|
+
function isSessionBoundary(entries, idx) {
|
|
9794
|
+
if (idx <= 0) return false;
|
|
9795
|
+
return entries[idx - 1].timestamp - entries[idx].timestamp > SESSION_GAP_MS;
|
|
9796
|
+
}
|
|
9797
|
+
function sessionStart(entries, idx) {
|
|
9798
|
+
let i = idx;
|
|
9799
|
+
while (i > 0 && !isSessionBoundary(entries, i)) i--;
|
|
9800
|
+
return i;
|
|
9801
|
+
}
|
|
9802
|
+
function render(entries, idx) {
|
|
9803
|
+
const entry = entries[idx];
|
|
9804
|
+
const total = entries.length;
|
|
9805
|
+
const step = idx + 1;
|
|
9806
|
+
process.stdout.write(CLEAR_SCREEN);
|
|
9807
|
+
process.stdout.write(
|
|
9808
|
+
chalk12.magenta.bold(`\u23EA Node9 Undo`) + chalk12.gray(` \u2500\u2500 step ${step} of ${total}`) + (entry.files?.length ? chalk12.gray(
|
|
9809
|
+
` \u2500\u2500 ${entry.files.slice(0, 2).join(", ")}${entry.files.length > 2 ? ` +${entry.files.length - 2} more` : ""}`
|
|
9810
|
+
) : "") + "\n\n"
|
|
9811
|
+
);
|
|
9812
|
+
process.stdout.write(
|
|
9813
|
+
` ${BOLD}Tool:${RESET} ${chalk12.cyan(entry.tool)}` + (entry.argsSummary ? chalk12.gray(" \u2192 " + entry.argsSummary) : "") + "\n"
|
|
9814
|
+
);
|
|
9815
|
+
process.stdout.write(` ${BOLD}When:${RESET} ${chalk12.gray(formatAge(entry.timestamp))}
|
|
9816
|
+
`);
|
|
9817
|
+
process.stdout.write(` ${BOLD}Dir: ${RESET} ${chalk12.gray(entry.cwd)}
|
|
9818
|
+
`);
|
|
9819
|
+
if (entry.files && entry.files.length > 0) {
|
|
9820
|
+
process.stdout.write(` ${BOLD}Files:${RESET} ${chalk12.gray(entry.files.join(", "))}
|
|
9821
|
+
`);
|
|
9822
|
+
}
|
|
9823
|
+
if (idx < total - 1 && isSessionBoundary(entries, idx + 1)) {
|
|
9824
|
+
process.stdout.write(chalk12.gray("\n \u2500\u2500 session boundary above \u2500\u2500\n"));
|
|
9825
|
+
}
|
|
9826
|
+
process.stdout.write("\n");
|
|
9827
|
+
const diff = entry.diff ?? computeUndoDiff(entry.hash, entry.cwd);
|
|
9828
|
+
if (diff) {
|
|
9829
|
+
renderDiff(diff);
|
|
9830
|
+
} else {
|
|
9831
|
+
process.stdout.write(
|
|
9832
|
+
chalk12.gray(" (no diff \u2014 working tree may already match this snapshot)\n")
|
|
9833
|
+
);
|
|
9834
|
+
}
|
|
9835
|
+
process.stdout.write("\n");
|
|
9836
|
+
process.stdout.write(
|
|
9837
|
+
chalk12.gray(" ") + (idx < total - 1 ? chalk12.white("[\u2190] older") : chalk12.gray("[\u2190] older")) + chalk12.gray(" ") + (idx > 0 ? chalk12.white("[\u2192] newer") : chalk12.gray("[\u2192] newer")) + chalk12.gray(" ") + chalk12.green("[\u21B5] restore here") + chalk12.gray(" ") + chalk12.yellow("[s] session start") + chalk12.gray(" ") + chalk12.gray("[q] quit") + "\n"
|
|
9838
|
+
);
|
|
9839
|
+
}
|
|
9840
|
+
async function runUndoNavigator(entries) {
|
|
9841
|
+
if (entries.length === 0) return { restored: false };
|
|
9842
|
+
const display = [...entries].reverse();
|
|
9843
|
+
let idx = 0;
|
|
9844
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
9845
|
+
render(display, idx);
|
|
9846
|
+
return { restored: false };
|
|
9847
|
+
}
|
|
9848
|
+
readline2.emitKeypressEvents(process.stdin);
|
|
9849
|
+
return new Promise((resolve) => {
|
|
9850
|
+
let done = false;
|
|
9851
|
+
render(display, idx);
|
|
9852
|
+
try {
|
|
9853
|
+
process.stdin.setRawMode(true);
|
|
9854
|
+
} catch {
|
|
9855
|
+
resolve({ restored: false });
|
|
9856
|
+
return;
|
|
9857
|
+
}
|
|
9858
|
+
process.stdin.resume();
|
|
9859
|
+
const cleanup = () => {
|
|
9860
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
9861
|
+
try {
|
|
9862
|
+
process.stdin.setRawMode(false);
|
|
9863
|
+
} catch {
|
|
9864
|
+
}
|
|
9865
|
+
process.stdin.pause();
|
|
9866
|
+
};
|
|
9867
|
+
const onKeypress = (_str, key) => {
|
|
9868
|
+
if (done) return;
|
|
9869
|
+
const name = key?.name ?? "";
|
|
9870
|
+
if (name === "left" || name === "h") {
|
|
9871
|
+
if (idx < display.length - 1) {
|
|
9872
|
+
idx++;
|
|
9873
|
+
render(display, idx);
|
|
9874
|
+
}
|
|
9875
|
+
} else if (name === "right" || name === "l") {
|
|
9876
|
+
if (idx > 0) {
|
|
9877
|
+
idx--;
|
|
9878
|
+
render(display, idx);
|
|
9879
|
+
}
|
|
9880
|
+
} else if (name === "s") {
|
|
9881
|
+
const start = sessionStart(display, idx);
|
|
9882
|
+
if (start !== idx) {
|
|
9883
|
+
idx = start;
|
|
9884
|
+
render(display, idx);
|
|
9885
|
+
}
|
|
9886
|
+
} else if (name === "return" || name === "y") {
|
|
9887
|
+
done = true;
|
|
9888
|
+
cleanup();
|
|
9889
|
+
process.stdout.write(CLEAR_SCREEN);
|
|
9890
|
+
const entry = display[idx];
|
|
9891
|
+
process.stdout.write(chalk12.magenta.bold("\n\u23EA Restoring snapshot...\n\n"));
|
|
9892
|
+
if (applyUndo(entry.hash, entry.cwd)) {
|
|
9893
|
+
process.stdout.write(chalk12.green("\u2705 Reverted successfully.\n\n"));
|
|
9894
|
+
resolve({ restored: true });
|
|
9895
|
+
} else {
|
|
9896
|
+
process.stdout.write(chalk12.red("\u274C Undo failed.\n\n"));
|
|
9897
|
+
resolve({ restored: false });
|
|
9898
|
+
}
|
|
9899
|
+
} else if (name === "q" || key?.ctrl && name === "c") {
|
|
9900
|
+
done = true;
|
|
9901
|
+
cleanup();
|
|
9902
|
+
process.stdout.write(CLEAR_SCREEN);
|
|
9903
|
+
process.stdout.write(chalk12.gray("\nCancelled.\n\n"));
|
|
9904
|
+
resolve({ restored: false });
|
|
9905
|
+
}
|
|
9906
|
+
};
|
|
9907
|
+
process.stdin.on("keypress", onKeypress);
|
|
9908
|
+
});
|
|
9909
|
+
}
|
|
9910
|
+
|
|
9911
|
+
// src/cli/commands/undo.ts
|
|
9912
|
+
function findMatchingCwd(startDir, history) {
|
|
9913
|
+
const cwds = new Set(history.map((e) => e.cwd));
|
|
9914
|
+
let dir = startDir;
|
|
9915
|
+
while (true) {
|
|
9916
|
+
if (cwds.has(dir)) return dir;
|
|
9917
|
+
const parent = path26.dirname(dir);
|
|
9918
|
+
if (parent === dir) return null;
|
|
9919
|
+
dir = parent;
|
|
9920
|
+
}
|
|
9921
|
+
}
|
|
9922
|
+
function formatAge2(timestamp) {
|
|
9923
|
+
const age = Math.round((Date.now() - timestamp) / 1e3);
|
|
9924
|
+
if (age < 60) return `${age}s ago`;
|
|
9925
|
+
if (age < 3600) return `${Math.round(age / 60)}m ago`;
|
|
9926
|
+
if (age < 86400) return `${Math.round(age / 3600)}h ago`;
|
|
9927
|
+
return `${Math.round(age / 86400)}d ago`;
|
|
9928
|
+
}
|
|
9645
9929
|
function registerUndoCommand(program2) {
|
|
9646
9930
|
program2.command("undo").description(
|
|
9647
|
-
"
|
|
9648
|
-
).option("--steps <n>", "
|
|
9649
|
-
const steps = Math.max(1, parseInt(options.steps, 10) || 1);
|
|
9931
|
+
"Browse and restore pre-AI snapshots. Arrow keys to navigate, Enter to restore. Use --steps N to go back N actions non-interactively, --list to print history."
|
|
9932
|
+
).option("--steps <n>", "Non-interactive: restore N steps back (default: 1)").option("--list", "Print snapshot history as a table and exit").option("--all", "Include snapshots from all directories, not just the current one").action(async (options) => {
|
|
9650
9933
|
const allHistory = getSnapshotHistory();
|
|
9651
|
-
const
|
|
9934
|
+
const matchedCwd = options.all ? null : findMatchingCwd(process.cwd(), allHistory);
|
|
9935
|
+
const history = options.all ? allHistory : allHistory.filter((s) => s.cwd === matchedCwd);
|
|
9652
9936
|
if (history.length === 0) {
|
|
9653
9937
|
if (!options.all && allHistory.length > 0) {
|
|
9654
9938
|
console.log(
|
|
9655
|
-
|
|
9939
|
+
chalk13.yellow(
|
|
9656
9940
|
`
|
|
9657
9941
|
\u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
|
|
9658
|
-
Run ${
|
|
9942
|
+
Run ${chalk13.cyan("node9 undo --all")} to see snapshots from all projects.
|
|
9659
9943
|
`
|
|
9660
9944
|
)
|
|
9661
9945
|
);
|
|
9662
9946
|
} else {
|
|
9663
|
-
console.log(
|
|
9947
|
+
console.log(chalk13.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
|
|
9664
9948
|
}
|
|
9665
9949
|
return;
|
|
9666
9950
|
}
|
|
9667
|
-
|
|
9668
|
-
|
|
9951
|
+
if (options.list) {
|
|
9952
|
+
console.log(chalk13.magenta.bold("\n\u23EA Snapshot History\n"));
|
|
9669
9953
|
console.log(
|
|
9670
|
-
|
|
9671
|
-
`
|
|
9672
|
-
\u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
|
|
9673
|
-
`
|
|
9954
|
+
chalk13.gray(
|
|
9955
|
+
` ${"#".padEnd(3)} ${"File / Command".padEnd(30)} ${"Tool".padEnd(8)} ${"When".padEnd(10)} Dir`
|
|
9674
9956
|
)
|
|
9675
9957
|
);
|
|
9958
|
+
console.log(chalk13.gray(" " + "\u2500".repeat(80)));
|
|
9959
|
+
const display = [...history].reverse();
|
|
9960
|
+
let prevTs = null;
|
|
9961
|
+
for (let i = 0; i < display.length; i++) {
|
|
9962
|
+
const e = display[i];
|
|
9963
|
+
const isGap = prevTs !== null && prevTs - e.timestamp > 6e4;
|
|
9964
|
+
if (isGap) console.log(chalk13.gray(" \u2500\u2500 earlier \u2500\u2500"));
|
|
9965
|
+
const label = (e.argsSummary || e.files?.[0] || "\u2014").slice(0, 30).padEnd(30);
|
|
9966
|
+
const tool = e.tool.slice(0, 8).padEnd(8);
|
|
9967
|
+
const when = formatAge2(e.timestamp).padEnd(10);
|
|
9968
|
+
const dir = e.cwd.length > 30 ? "\u2026" + e.cwd.slice(-29) : e.cwd;
|
|
9969
|
+
console.log(
|
|
9970
|
+
chalk13.white(
|
|
9971
|
+
` ${String(i + 1).padEnd(3)} ${label} ${chalk13.cyan(tool)} ${chalk13.gray(when)} ${chalk13.gray(dir)}`
|
|
9972
|
+
)
|
|
9973
|
+
);
|
|
9974
|
+
prevTs = e.timestamp;
|
|
9975
|
+
}
|
|
9976
|
+
console.log("");
|
|
9676
9977
|
return;
|
|
9677
9978
|
}
|
|
9678
|
-
|
|
9679
|
-
|
|
9680
|
-
|
|
9681
|
-
|
|
9682
|
-
|
|
9979
|
+
if (options.steps !== void 0) {
|
|
9980
|
+
const steps = Math.max(1, parseInt(options.steps, 10) || 1);
|
|
9981
|
+
const idx = history.length - steps;
|
|
9982
|
+
if (idx < 0) {
|
|
9983
|
+
console.log(
|
|
9984
|
+
chalk13.yellow(
|
|
9985
|
+
`
|
|
9986
|
+
\u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
|
|
9987
|
+
`
|
|
9988
|
+
)
|
|
9989
|
+
);
|
|
9990
|
+
return;
|
|
9991
|
+
}
|
|
9992
|
+
const snapshot = history[idx];
|
|
9993
|
+
const ageStr = formatAge2(snapshot.timestamp);
|
|
9994
|
+
console.log(
|
|
9995
|
+
chalk13.magenta.bold(`
|
|
9683
9996
|
\u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`)
|
|
9684
|
-
|
|
9685
|
-
console.log(
|
|
9686
|
-
chalk12.white(
|
|
9687
|
-
` Tool: ${chalk12.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk12.gray(" \u2192 " + snapshot.argsSummary) : ""}`
|
|
9688
|
-
)
|
|
9689
|
-
);
|
|
9690
|
-
console.log(chalk12.white(` When: ${chalk12.gray(ageStr)}`));
|
|
9691
|
-
console.log(chalk12.white(` Dir: ${chalk12.gray(snapshot.cwd)}`));
|
|
9692
|
-
if (steps > 1)
|
|
9997
|
+
);
|
|
9693
9998
|
console.log(
|
|
9694
|
-
|
|
9999
|
+
chalk13.white(
|
|
10000
|
+
` Tool: ${chalk13.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk13.gray(" \u2192 " + snapshot.argsSummary) : ""}`
|
|
10001
|
+
)
|
|
9695
10002
|
);
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
9699
|
-
|
|
9700
|
-
|
|
9701
|
-
|
|
9702
|
-
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
|
|
9707
|
-
|
|
9708
|
-
console.log(
|
|
9709
|
-
|
|
9710
|
-
console.log(
|
|
10003
|
+
console.log(chalk13.white(` When: ${chalk13.gray(ageStr)}`));
|
|
10004
|
+
console.log(chalk13.white(` Dir: ${chalk13.gray(snapshot.cwd)}`));
|
|
10005
|
+
if (steps > 1)
|
|
10006
|
+
console.log(
|
|
10007
|
+
chalk13.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
|
|
10008
|
+
);
|
|
10009
|
+
console.log("");
|
|
10010
|
+
const diff = snapshot.diff ?? computeUndoDiff(snapshot.hash, snapshot.cwd);
|
|
10011
|
+
if (diff) {
|
|
10012
|
+
const lines = diff.split("\n").filter((l) => !l.startsWith("diff --git") && !l.startsWith("index "));
|
|
10013
|
+
for (const line of lines) {
|
|
10014
|
+
if (line.startsWith("+++") || line.startsWith("---")) console.log(chalk13.bold(line));
|
|
10015
|
+
else if (line.startsWith("+")) console.log(chalk13.green(line));
|
|
10016
|
+
else if (line.startsWith("-")) console.log(chalk13.red(line));
|
|
10017
|
+
else if (line.startsWith("@@")) console.log(chalk13.cyan(line));
|
|
10018
|
+
else console.log(chalk13.gray(line));
|
|
9711
10019
|
}
|
|
10020
|
+
console.log("");
|
|
10021
|
+
} else {
|
|
10022
|
+
console.log(
|
|
10023
|
+
chalk13.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
|
|
10024
|
+
);
|
|
9712
10025
|
}
|
|
9713
|
-
|
|
9714
|
-
|
|
9715
|
-
|
|
9716
|
-
|
|
9717
|
-
|
|
9718
|
-
|
|
9719
|
-
|
|
9720
|
-
|
|
9721
|
-
default: false
|
|
9722
|
-
});
|
|
9723
|
-
if (proceed) {
|
|
9724
|
-
if (applyUndo(snapshot.hash, snapshot.cwd)) {
|
|
9725
|
-
console.log(chalk12.green("\n\u2705 Reverted successfully.\n"));
|
|
10026
|
+
const { confirm: confirm3 } = await import("@inquirer/prompts");
|
|
10027
|
+
const proceed = await confirm3({ message: `Revert to this snapshot?`, default: false });
|
|
10028
|
+
if (proceed) {
|
|
10029
|
+
if (applyUndo(snapshot.hash, snapshot.cwd)) {
|
|
10030
|
+
console.log(chalk13.green("\n\u2705 Reverted successfully.\n"));
|
|
10031
|
+
} else {
|
|
10032
|
+
console.error(chalk13.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
|
|
10033
|
+
}
|
|
9726
10034
|
} else {
|
|
9727
|
-
console.
|
|
10035
|
+
console.log(chalk13.gray("\nCancelled.\n"));
|
|
9728
10036
|
}
|
|
9729
|
-
|
|
9730
|
-
console.log(chalk12.gray("\nCancelled.\n"));
|
|
10037
|
+
return;
|
|
9731
10038
|
}
|
|
10039
|
+
await runUndoNavigator(history);
|
|
9732
10040
|
});
|
|
9733
10041
|
}
|
|
9734
10042
|
|
|
9735
10043
|
// src/cli/commands/watch.ts
|
|
9736
10044
|
init_daemon();
|
|
9737
|
-
import
|
|
10045
|
+
import chalk14 from "chalk";
|
|
9738
10046
|
import { spawn as spawn7, spawnSync as spawnSync5 } from "child_process";
|
|
9739
10047
|
function registerWatchCommand(program2) {
|
|
9740
10048
|
program2.command("watch").description("Run a command under Node9 watch mode (daemon stays alive for the session)").argument("<command>", "Command to run").argument("[args...]", "Arguments for the command").action(async (cmd, args) => {
|
|
@@ -9750,7 +10058,7 @@ function registerWatchCommand(program2) {
|
|
|
9750
10058
|
throw new Error("not running");
|
|
9751
10059
|
}
|
|
9752
10060
|
} catch {
|
|
9753
|
-
console.error(
|
|
10061
|
+
console.error(chalk14.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
|
|
9754
10062
|
const child = spawn7(process.execPath, [process.argv[1], "daemon"], {
|
|
9755
10063
|
detached: true,
|
|
9756
10064
|
stdio: "ignore",
|
|
@@ -9772,12 +10080,12 @@ function registerWatchCommand(program2) {
|
|
|
9772
10080
|
}
|
|
9773
10081
|
}
|
|
9774
10082
|
if (!ready) {
|
|
9775
|
-
console.error(
|
|
10083
|
+
console.error(chalk14.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
9776
10084
|
process.exit(1);
|
|
9777
10085
|
}
|
|
9778
10086
|
}
|
|
9779
10087
|
console.error(
|
|
9780
|
-
|
|
10088
|
+
chalk14.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + chalk14.dim(` \u2192 localhost:${port}`) + chalk14.dim(
|
|
9781
10089
|
"\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
|
|
9782
10090
|
)
|
|
9783
10091
|
);
|
|
@@ -9786,7 +10094,7 @@ function registerWatchCommand(program2) {
|
|
|
9786
10094
|
env: { ...process.env, NODE9_WATCH_MODE: "1" }
|
|
9787
10095
|
});
|
|
9788
10096
|
if (result.error) {
|
|
9789
|
-
console.error(
|
|
10097
|
+
console.error(chalk14.red(`\u274C Failed to run command: ${result.error.message}`));
|
|
9790
10098
|
process.exit(1);
|
|
9791
10099
|
}
|
|
9792
10100
|
process.exit(result.status ?? 0);
|
|
@@ -9795,8 +10103,8 @@ function registerWatchCommand(program2) {
|
|
|
9795
10103
|
|
|
9796
10104
|
// src/mcp-gateway/index.ts
|
|
9797
10105
|
init_orchestrator();
|
|
9798
|
-
import
|
|
9799
|
-
import
|
|
10106
|
+
import readline3 from "readline";
|
|
10107
|
+
import chalk15 from "chalk";
|
|
9800
10108
|
import { spawn as spawn8 } from "child_process";
|
|
9801
10109
|
import { execa as execa2 } from "execa";
|
|
9802
10110
|
init_provenance();
|
|
@@ -9859,13 +10167,13 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
9859
10167
|
const prov = checkProvenance(executable);
|
|
9860
10168
|
if (prov.trustLevel === "suspect") {
|
|
9861
10169
|
console.error(
|
|
9862
|
-
|
|
10170
|
+
chalk15.red(
|
|
9863
10171
|
`\u26A0\uFE0F Node9: Upstream MCP server binary is suspect \u2014 ${prov.reason} (${prov.resolvedPath})`
|
|
9864
10172
|
)
|
|
9865
10173
|
);
|
|
9866
|
-
console.error(
|
|
10174
|
+
console.error(chalk15.red(" Verify this binary is trusted before proceeding."));
|
|
9867
10175
|
}
|
|
9868
|
-
console.error(
|
|
10176
|
+
console.error(chalk15.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
|
|
9869
10177
|
const UPSTREAM_INJECTOR_VARS = /* @__PURE__ */ new Set([
|
|
9870
10178
|
"NODE_OPTIONS",
|
|
9871
10179
|
"NODE_PATH",
|
|
@@ -9893,7 +10201,7 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
9893
10201
|
let authPending = false;
|
|
9894
10202
|
let deferredExitCode = null;
|
|
9895
10203
|
let deferredStdinEnd = false;
|
|
9896
|
-
const agentIn =
|
|
10204
|
+
const agentIn = readline3.createInterface({ input: process.stdin, terminal: false });
|
|
9897
10205
|
agentIn.on("line", async (line) => {
|
|
9898
10206
|
let message;
|
|
9899
10207
|
try {
|
|
@@ -9929,10 +10237,10 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
9929
10237
|
mcpServer
|
|
9930
10238
|
});
|
|
9931
10239
|
if (!result.approved) {
|
|
9932
|
-
console.error(
|
|
10240
|
+
console.error(chalk15.red(`
|
|
9933
10241
|
\u{1F6D1} Node9 MCP Gateway: Action Blocked`));
|
|
9934
|
-
console.error(
|
|
9935
|
-
console.error(
|
|
10242
|
+
console.error(chalk15.gray(` Tool: ${toolName}`));
|
|
10243
|
+
console.error(chalk15.gray(` Reason: ${result.reason ?? "Security Policy"}
|
|
9936
10244
|
`));
|
|
9937
10245
|
const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
|
|
9938
10246
|
const isHumanDecision = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
|
|
@@ -10006,7 +10314,7 @@ function registerMcpGatewayCommand(program2) {
|
|
|
10006
10314
|
|
|
10007
10315
|
// src/cli/commands/trust.ts
|
|
10008
10316
|
init_trusted_hosts();
|
|
10009
|
-
import
|
|
10317
|
+
import chalk16 from "chalk";
|
|
10010
10318
|
function isValidHost(host) {
|
|
10011
10319
|
return /^(\*\.)?[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/.test(host);
|
|
10012
10320
|
}
|
|
@@ -10016,44 +10324,44 @@ function registerTrustCommand(program2) {
|
|
|
10016
10324
|
const normalized = normalizeHost(host.trim());
|
|
10017
10325
|
if (!isValidHost(normalized)) {
|
|
10018
10326
|
console.error(
|
|
10019
|
-
|
|
10327
|
+
chalk16.red(`
|
|
10020
10328
|
\u274C Invalid host: "${host}"
|
|
10021
|
-
`) +
|
|
10329
|
+
`) + chalk16.gray(" Use an FQDN like api.mycompany.com or *.mycompany.com\n")
|
|
10022
10330
|
);
|
|
10023
10331
|
process.exit(1);
|
|
10024
10332
|
}
|
|
10025
10333
|
addTrustedHost(normalized);
|
|
10026
|
-
console.log(
|
|
10334
|
+
console.log(chalk16.green(`
|
|
10027
10335
|
\u2705 ${normalized} added to trusted hosts.`));
|
|
10028
10336
|
console.log(
|
|
10029
|
-
|
|
10337
|
+
chalk16.gray(" Pipe-chain blocks to this host: critical \u2192 review, high \u2192 allow\n")
|
|
10030
10338
|
);
|
|
10031
10339
|
});
|
|
10032
10340
|
trustCmd.command("remove <host>").description("Remove a trusted host").action((host) => {
|
|
10033
10341
|
const normalized = normalizeHost(host.trim());
|
|
10034
10342
|
const removed = removeTrustedHost(normalized);
|
|
10035
10343
|
if (!removed) {
|
|
10036
|
-
console.error(
|
|
10344
|
+
console.error(chalk16.yellow(`
|
|
10037
10345
|
\u26A0\uFE0F "${normalized}" is not in the trusted hosts list.
|
|
10038
10346
|
`));
|
|
10039
10347
|
process.exit(1);
|
|
10040
10348
|
}
|
|
10041
|
-
console.log(
|
|
10349
|
+
console.log(chalk16.green(`
|
|
10042
10350
|
\u2705 ${normalized} removed from trusted hosts.
|
|
10043
10351
|
`));
|
|
10044
10352
|
});
|
|
10045
10353
|
trustCmd.command("list").description("Show all trusted hosts").action(() => {
|
|
10046
10354
|
const hosts = readTrustedHosts();
|
|
10047
10355
|
if (hosts.length === 0) {
|
|
10048
|
-
console.log(
|
|
10049
|
-
console.log(` Add one: ${
|
|
10356
|
+
console.log(chalk16.gray("\n No trusted hosts configured.\n"));
|
|
10357
|
+
console.log(` Add one: ${chalk16.cyan("node9 trust add api.mycompany.com")}
|
|
10050
10358
|
`);
|
|
10051
10359
|
return;
|
|
10052
10360
|
}
|
|
10053
|
-
console.log(
|
|
10361
|
+
console.log(chalk16.bold("\n\u{1F513} Trusted Hosts\n"));
|
|
10054
10362
|
for (const entry of hosts) {
|
|
10055
10363
|
const date = new Date(entry.addedAt).toLocaleDateString();
|
|
10056
|
-
console.log(` ${
|
|
10364
|
+
console.log(` ${chalk16.cyan(entry.host.padEnd(40))} ${chalk16.gray(`added ${date}`)}`);
|
|
10057
10365
|
}
|
|
10058
10366
|
console.log("");
|
|
10059
10367
|
});
|
|
@@ -10061,15 +10369,15 @@ function registerTrustCommand(program2) {
|
|
|
10061
10369
|
|
|
10062
10370
|
// src/cli.ts
|
|
10063
10371
|
var { version } = JSON.parse(
|
|
10064
|
-
fs26.readFileSync(
|
|
10372
|
+
fs26.readFileSync(path29.join(__dirname, "../package.json"), "utf-8")
|
|
10065
10373
|
);
|
|
10066
10374
|
var program = new Command();
|
|
10067
10375
|
program.name("node9").description("The Sudo Command for AI Agents").version(version);
|
|
10068
10376
|
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) => {
|
|
10069
10377
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
10070
|
-
const credPath =
|
|
10071
|
-
if (!fs26.existsSync(
|
|
10072
|
-
fs26.mkdirSync(
|
|
10378
|
+
const credPath = path29.join(os22.homedir(), ".node9", "credentials.json");
|
|
10379
|
+
if (!fs26.existsSync(path29.dirname(credPath)))
|
|
10380
|
+
fs26.mkdirSync(path29.dirname(credPath), { recursive: true });
|
|
10073
10381
|
const profileName = options.profile || "default";
|
|
10074
10382
|
let existingCreds = {};
|
|
10075
10383
|
try {
|
|
@@ -10088,7 +10396,7 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
10088
10396
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
10089
10397
|
fs26.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
10090
10398
|
if (profileName === "default") {
|
|
10091
|
-
const configPath =
|
|
10399
|
+
const configPath = path29.join(os22.homedir(), ".node9", "config.json");
|
|
10092
10400
|
let config = {};
|
|
10093
10401
|
try {
|
|
10094
10402
|
if (fs26.existsSync(configPath))
|
|
@@ -10107,19 +10415,19 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
10107
10415
|
approvers.cloud = false;
|
|
10108
10416
|
}
|
|
10109
10417
|
s.approvers = approvers;
|
|
10110
|
-
if (!fs26.existsSync(
|
|
10111
|
-
fs26.mkdirSync(
|
|
10418
|
+
if (!fs26.existsSync(path29.dirname(configPath)))
|
|
10419
|
+
fs26.mkdirSync(path29.dirname(configPath), { recursive: true });
|
|
10112
10420
|
fs26.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
10113
10421
|
}
|
|
10114
10422
|
if (options.profile && profileName !== "default") {
|
|
10115
|
-
console.log(
|
|
10116
|
-
console.log(
|
|
10423
|
+
console.log(chalk18.green(`\u2705 Profile "${profileName}" saved`));
|
|
10424
|
+
console.log(chalk18.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
10117
10425
|
} else if (options.local) {
|
|
10118
|
-
console.log(
|
|
10119
|
-
console.log(
|
|
10426
|
+
console.log(chalk18.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
10427
|
+
console.log(chalk18.gray(` All decisions stay on this machine.`));
|
|
10120
10428
|
} else {
|
|
10121
|
-
console.log(
|
|
10122
|
-
console.log(
|
|
10429
|
+
console.log(chalk18.green(`\u2705 Logged in \u2014 agent mode`));
|
|
10430
|
+
console.log(chalk18.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
10123
10431
|
}
|
|
10124
10432
|
});
|
|
10125
10433
|
program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor hud").argument("<target>", "The agent to protect: claude | gemini | cursor | hud").action(async (target) => {
|
|
@@ -10127,19 +10435,19 @@ program.command("addto").description("Integrate Node9 with an AI agent").addHelp
|
|
|
10127
10435
|
if (target === "claude") return await setupClaude();
|
|
10128
10436
|
if (target === "cursor") return await setupCursor();
|
|
10129
10437
|
if (target === "hud") return setupHud();
|
|
10130
|
-
console.error(
|
|
10438
|
+
console.error(chalk18.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
|
|
10131
10439
|
process.exit(1);
|
|
10132
10440
|
});
|
|
10133
10441
|
program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor hud").argument("[target]", "The agent to protect: claude | gemini | cursor | hud").action(async (target) => {
|
|
10134
10442
|
if (!target) {
|
|
10135
|
-
console.log(
|
|
10136
|
-
console.log(" Usage: " +
|
|
10443
|
+
console.log(chalk18.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
10444
|
+
console.log(" Usage: " + chalk18.white("node9 setup <target>") + "\n");
|
|
10137
10445
|
console.log(" Targets:");
|
|
10138
|
-
console.log(" " +
|
|
10139
|
-
console.log(" " +
|
|
10140
|
-
console.log(" " +
|
|
10446
|
+
console.log(" " + chalk18.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
10447
|
+
console.log(" " + chalk18.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
10448
|
+
console.log(" " + chalk18.green("cursor") + " \u2014 Cursor (hook mode)");
|
|
10141
10449
|
process.stdout.write(
|
|
10142
|
-
" " +
|
|
10450
|
+
" " + chalk18.green("hud") + " \u2014 Claude Code security statusline\n"
|
|
10143
10451
|
);
|
|
10144
10452
|
console.log("");
|
|
10145
10453
|
return;
|
|
@@ -10149,7 +10457,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
10149
10457
|
if (t === "claude") return await setupClaude();
|
|
10150
10458
|
if (t === "cursor") return await setupCursor();
|
|
10151
10459
|
if (t === "hud") return setupHud();
|
|
10152
|
-
console.error(
|
|
10460
|
+
console.error(chalk18.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
|
|
10153
10461
|
process.exit(1);
|
|
10154
10462
|
});
|
|
10155
10463
|
program.command("removefrom").description("Remove Node9 hooks from an AI agent configuration").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to remove from: claude | gemini | cursor").action((target) => {
|
|
@@ -10160,31 +10468,31 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
|
|
|
10160
10468
|
else if (target === "hud") fn = teardownHud;
|
|
10161
10469
|
else {
|
|
10162
10470
|
console.error(
|
|
10163
|
-
|
|
10471
|
+
chalk18.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`)
|
|
10164
10472
|
);
|
|
10165
10473
|
process.exit(1);
|
|
10166
10474
|
}
|
|
10167
|
-
console.log(
|
|
10475
|
+
console.log(chalk18.cyan(`
|
|
10168
10476
|
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
10169
10477
|
`));
|
|
10170
10478
|
try {
|
|
10171
10479
|
fn();
|
|
10172
10480
|
} catch (err) {
|
|
10173
|
-
console.error(
|
|
10481
|
+
console.error(chalk18.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
10174
10482
|
process.exit(1);
|
|
10175
10483
|
}
|
|
10176
|
-
console.log(
|
|
10484
|
+
console.log(chalk18.gray("\n Restart the agent for changes to take effect."));
|
|
10177
10485
|
});
|
|
10178
10486
|
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) => {
|
|
10179
|
-
console.log(
|
|
10180
|
-
console.log(
|
|
10487
|
+
console.log(chalk18.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
10488
|
+
console.log(chalk18.bold("Stopping daemon..."));
|
|
10181
10489
|
try {
|
|
10182
10490
|
stopDaemon();
|
|
10183
|
-
console.log(
|
|
10491
|
+
console.log(chalk18.green(" \u2705 Daemon stopped"));
|
|
10184
10492
|
} catch {
|
|
10185
|
-
console.log(
|
|
10493
|
+
console.log(chalk18.blue(" \u2139\uFE0F Daemon was not running"));
|
|
10186
10494
|
}
|
|
10187
|
-
console.log(
|
|
10495
|
+
console.log(chalk18.bold("\nRemoving hooks..."));
|
|
10188
10496
|
let teardownFailed = false;
|
|
10189
10497
|
for (const [label, fn] of [
|
|
10190
10498
|
["Claude", teardownClaude],
|
|
@@ -10196,16 +10504,16 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
10196
10504
|
} catch (err) {
|
|
10197
10505
|
teardownFailed = true;
|
|
10198
10506
|
console.error(
|
|
10199
|
-
|
|
10507
|
+
chalk18.red(
|
|
10200
10508
|
` \u26A0\uFE0F Failed to remove ${label} hooks: ${err instanceof Error ? err.message : String(err)}`
|
|
10201
10509
|
)
|
|
10202
10510
|
);
|
|
10203
10511
|
}
|
|
10204
10512
|
}
|
|
10205
10513
|
if (options.purge) {
|
|
10206
|
-
const node9Dir =
|
|
10514
|
+
const node9Dir = path29.join(os22.homedir(), ".node9");
|
|
10207
10515
|
if (fs26.existsSync(node9Dir)) {
|
|
10208
|
-
const confirmed = await
|
|
10516
|
+
const confirmed = await confirm2({
|
|
10209
10517
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
10210
10518
|
default: false
|
|
10211
10519
|
});
|
|
@@ -10213,28 +10521,28 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
10213
10521
|
fs26.rmSync(node9Dir, { recursive: true });
|
|
10214
10522
|
if (fs26.existsSync(node9Dir)) {
|
|
10215
10523
|
console.error(
|
|
10216
|
-
|
|
10524
|
+
chalk18.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
10217
10525
|
);
|
|
10218
10526
|
} else {
|
|
10219
|
-
console.log(
|
|
10527
|
+
console.log(chalk18.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
10220
10528
|
}
|
|
10221
10529
|
} else {
|
|
10222
|
-
console.log(
|
|
10530
|
+
console.log(chalk18.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
10223
10531
|
}
|
|
10224
10532
|
} else {
|
|
10225
|
-
console.log(
|
|
10533
|
+
console.log(chalk18.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
10226
10534
|
}
|
|
10227
10535
|
} else {
|
|
10228
10536
|
console.log(
|
|
10229
|
-
|
|
10537
|
+
chalk18.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
10230
10538
|
);
|
|
10231
10539
|
}
|
|
10232
10540
|
if (teardownFailed) {
|
|
10233
|
-
console.error(
|
|
10541
|
+
console.error(chalk18.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
10234
10542
|
process.exit(1);
|
|
10235
10543
|
}
|
|
10236
|
-
console.log(
|
|
10237
|
-
console.log(
|
|
10544
|
+
console.log(chalk18.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
|
|
10545
|
+
console.log(chalk18.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
|
|
10238
10546
|
});
|
|
10239
10547
|
registerDoctorCommand(program, version);
|
|
10240
10548
|
program.command("explain").description(
|
|
@@ -10247,7 +10555,7 @@ program.command("explain").description(
|
|
|
10247
10555
|
try {
|
|
10248
10556
|
args = JSON.parse(trimmed);
|
|
10249
10557
|
} catch {
|
|
10250
|
-
console.error(
|
|
10558
|
+
console.error(chalk18.red(`
|
|
10251
10559
|
\u274C Invalid JSON: ${trimmed}
|
|
10252
10560
|
`));
|
|
10253
10561
|
process.exit(1);
|
|
@@ -10258,54 +10566,54 @@ program.command("explain").description(
|
|
|
10258
10566
|
}
|
|
10259
10567
|
const result = await explainPolicy(tool, args);
|
|
10260
10568
|
console.log("");
|
|
10261
|
-
console.log(
|
|
10569
|
+
console.log(chalk18.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
10262
10570
|
console.log("");
|
|
10263
|
-
console.log(` ${
|
|
10571
|
+
console.log(` ${chalk18.bold("Tool:")} ${chalk18.white(result.tool)}`);
|
|
10264
10572
|
if (argsRaw) {
|
|
10265
10573
|
const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
10266
|
-
console.log(` ${
|
|
10574
|
+
console.log(` ${chalk18.bold("Input:")} ${chalk18.gray(preview)}`);
|
|
10267
10575
|
}
|
|
10268
10576
|
console.log("");
|
|
10269
|
-
console.log(
|
|
10577
|
+
console.log(chalk18.bold("Config Sources (Waterfall):"));
|
|
10270
10578
|
for (const tier of result.waterfall) {
|
|
10271
|
-
const num =
|
|
10579
|
+
const num = chalk18.gray(` ${tier.tier}.`);
|
|
10272
10580
|
const label = tier.label.padEnd(16);
|
|
10273
10581
|
let statusStr;
|
|
10274
10582
|
if (tier.tier === 1) {
|
|
10275
|
-
statusStr =
|
|
10583
|
+
statusStr = chalk18.gray(tier.note ?? "");
|
|
10276
10584
|
} else if (tier.status === "active") {
|
|
10277
|
-
const loc = tier.path ?
|
|
10278
|
-
const note = tier.note ?
|
|
10279
|
-
statusStr =
|
|
10585
|
+
const loc = tier.path ? chalk18.gray(tier.path) : "";
|
|
10586
|
+
const note = tier.note ? chalk18.gray(`(${tier.note})`) : "";
|
|
10587
|
+
statusStr = chalk18.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
|
|
10280
10588
|
} else {
|
|
10281
|
-
statusStr =
|
|
10589
|
+
statusStr = chalk18.gray("\u25CB " + (tier.note ?? "not found"));
|
|
10282
10590
|
}
|
|
10283
|
-
console.log(`${num} ${
|
|
10591
|
+
console.log(`${num} ${chalk18.white(label)} ${statusStr}`);
|
|
10284
10592
|
}
|
|
10285
10593
|
console.log("");
|
|
10286
|
-
console.log(
|
|
10594
|
+
console.log(chalk18.bold("Policy Evaluation:"));
|
|
10287
10595
|
for (const step of result.steps) {
|
|
10288
10596
|
const isFinal = step.isFinal;
|
|
10289
10597
|
let icon;
|
|
10290
|
-
if (step.outcome === "allow") icon =
|
|
10291
|
-
else if (step.outcome === "review") icon =
|
|
10292
|
-
else if (step.outcome === "skip") icon =
|
|
10293
|
-
else icon =
|
|
10598
|
+
if (step.outcome === "allow") icon = chalk18.green(" \u2705");
|
|
10599
|
+
else if (step.outcome === "review") icon = chalk18.red(" \u{1F534}");
|
|
10600
|
+
else if (step.outcome === "skip") icon = chalk18.gray(" \u2500 ");
|
|
10601
|
+
else icon = chalk18.gray(" \u25CB ");
|
|
10294
10602
|
const name = step.name.padEnd(18);
|
|
10295
|
-
const nameStr = isFinal ?
|
|
10296
|
-
const detail = isFinal ?
|
|
10297
|
-
const arrow = isFinal ?
|
|
10603
|
+
const nameStr = isFinal ? chalk18.white.bold(name) : chalk18.white(name);
|
|
10604
|
+
const detail = isFinal ? chalk18.white(step.detail) : chalk18.gray(step.detail);
|
|
10605
|
+
const arrow = isFinal ? chalk18.yellow(" \u2190 STOP") : "";
|
|
10298
10606
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
10299
10607
|
}
|
|
10300
10608
|
console.log("");
|
|
10301
10609
|
if (result.decision === "allow") {
|
|
10302
|
-
console.log(
|
|
10610
|
+
console.log(chalk18.green.bold(" Decision: \u2705 ALLOW") + chalk18.gray(" \u2014 no approval needed"));
|
|
10303
10611
|
} else {
|
|
10304
10612
|
console.log(
|
|
10305
|
-
|
|
10613
|
+
chalk18.red.bold(" Decision: \u{1F534} REVIEW") + chalk18.gray(" \u2014 human approval required")
|
|
10306
10614
|
);
|
|
10307
10615
|
if (result.blockedByLabel) {
|
|
10308
|
-
console.log(
|
|
10616
|
+
console.log(chalk18.gray(` Reason: ${result.blockedByLabel}`));
|
|
10309
10617
|
}
|
|
10310
10618
|
}
|
|
10311
10619
|
console.log("");
|
|
@@ -10319,7 +10627,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
|
|
|
10319
10627
|
try {
|
|
10320
10628
|
await startTail2(options);
|
|
10321
10629
|
} catch (err) {
|
|
10322
|
-
console.error(
|
|
10630
|
+
console.error(chalk18.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
|
|
10323
10631
|
process.exit(1);
|
|
10324
10632
|
}
|
|
10325
10633
|
});
|
|
@@ -10335,7 +10643,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
10335
10643
|
const ms = parseDuration(options.duration);
|
|
10336
10644
|
if (ms === null) {
|
|
10337
10645
|
console.error(
|
|
10338
|
-
|
|
10646
|
+
chalk18.red(`
|
|
10339
10647
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
10340
10648
|
`)
|
|
10341
10649
|
);
|
|
@@ -10343,20 +10651,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
10343
10651
|
}
|
|
10344
10652
|
pauseNode9(ms, options.duration);
|
|
10345
10653
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
10346
|
-
console.log(
|
|
10654
|
+
console.log(chalk18.yellow(`
|
|
10347
10655
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
10348
|
-
console.log(
|
|
10349
|
-
console.log(
|
|
10656
|
+
console.log(chalk18.gray(` All tool calls will be allowed without review.`));
|
|
10657
|
+
console.log(chalk18.gray(` Run "node9 resume" to re-enable early.
|
|
10350
10658
|
`));
|
|
10351
10659
|
});
|
|
10352
10660
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
10353
10661
|
const { paused } = checkPause();
|
|
10354
10662
|
if (!paused) {
|
|
10355
|
-
console.log(
|
|
10663
|
+
console.log(chalk18.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
10356
10664
|
return;
|
|
10357
10665
|
}
|
|
10358
10666
|
resumeNode9();
|
|
10359
|
-
console.log(
|
|
10667
|
+
console.log(chalk18.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
10360
10668
|
});
|
|
10361
10669
|
var HOOK_BASED_AGENTS = {
|
|
10362
10670
|
claude: "claude",
|
|
@@ -10369,15 +10677,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
10369
10677
|
if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
|
|
10370
10678
|
const target = HOOK_BASED_AGENTS[firstArg2];
|
|
10371
10679
|
console.error(
|
|
10372
|
-
|
|
10680
|
+
chalk18.yellow(`
|
|
10373
10681
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
10374
10682
|
);
|
|
10375
|
-
console.error(
|
|
10683
|
+
console.error(chalk18.white(`
|
|
10376
10684
|
"${target}" uses its own hook system. Use:`));
|
|
10377
10685
|
console.error(
|
|
10378
|
-
|
|
10686
|
+
chalk18.green(` node9 addto ${target} `) + chalk18.gray("# one-time setup")
|
|
10379
10687
|
);
|
|
10380
|
-
console.error(
|
|
10688
|
+
console.error(chalk18.green(` ${target} `) + chalk18.gray("# run normally"));
|
|
10381
10689
|
process.exit(1);
|
|
10382
10690
|
}
|
|
10383
10691
|
const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
|
|
@@ -10394,12 +10702,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
10394
10702
|
}
|
|
10395
10703
|
);
|
|
10396
10704
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
10397
|
-
console.error(
|
|
10705
|
+
console.error(chalk18.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
10398
10706
|
const daemonReady = await autoStartDaemonAndWait();
|
|
10399
10707
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
10400
10708
|
}
|
|
10401
10709
|
if (result.noApprovalMechanism && process.stdout.isTTY) {
|
|
10402
|
-
const approved = await
|
|
10710
|
+
const approved = await confirm2({
|
|
10403
10711
|
message: `\u{1F6E1}\uFE0F Node9: Allow "${fullCommand}"?`,
|
|
10404
10712
|
default: false
|
|
10405
10713
|
});
|
|
@@ -10407,12 +10715,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
10407
10715
|
}
|
|
10408
10716
|
if (!result.approved) {
|
|
10409
10717
|
console.error(
|
|
10410
|
-
|
|
10718
|
+
chalk18.red(`
|
|
10411
10719
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
10412
10720
|
);
|
|
10413
10721
|
process.exit(1);
|
|
10414
10722
|
}
|
|
10415
|
-
console.error(
|
|
10723
|
+
console.error(chalk18.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
10416
10724
|
await runProxy(fullCommand);
|
|
10417
10725
|
} else {
|
|
10418
10726
|
program.help();
|
|
@@ -10427,7 +10735,7 @@ if (process.argv[2] !== "daemon") {
|
|
|
10427
10735
|
const isCheckHook = process.argv[2] === "check";
|
|
10428
10736
|
if (isCheckHook) {
|
|
10429
10737
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
10430
|
-
const logPath =
|
|
10738
|
+
const logPath = path29.join(os22.homedir(), ".node9", "hook-debug.log");
|
|
10431
10739
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
10432
10740
|
fs26.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
10433
10741
|
`);
|