@node9/proxy 1.5.2 → 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 +115 -20
- package/dist/cli.js +1408 -345
- package/dist/cli.mjs +1406 -343
- package/dist/index.js +201 -49
- package/dist/index.mjs +201 -49
- 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,
|
|
@@ -191,12 +191,21 @@ var init_config_schema = __esm({
|
|
|
191
191
|
verdict: z.enum(["allow", "review", "block"], {
|
|
192
192
|
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
193
193
|
}),
|
|
194
|
-
reason: z.string().optional()
|
|
194
|
+
reason: z.string().optional(),
|
|
195
|
+
// Unknown predicate names are filtered out rather than failing the whole rule.
|
|
196
|
+
// Failing the whole z.array() would cause sanitizeConfig to drop the entire
|
|
197
|
+
// `policy` top-level key, silently disabling ALL smart rules in the config.
|
|
198
|
+
dependsOnState: z.array(z.string()).transform(
|
|
199
|
+
(arr) => arr.filter(
|
|
200
|
+
(p) => p === "no_test_passed_since_last_edit"
|
|
201
|
+
)
|
|
202
|
+
).optional(),
|
|
203
|
+
recoveryCommand: z.string().optional()
|
|
195
204
|
});
|
|
196
205
|
ConfigFileSchema = z.object({
|
|
197
206
|
version: z.string().optional(),
|
|
198
207
|
settings: z.object({
|
|
199
|
-
mode: z.enum(["standard", "strict", "audit"]).optional(),
|
|
208
|
+
mode: z.enum(["standard", "strict", "audit", "observe"]).optional(),
|
|
200
209
|
autoStartDaemon: z.boolean().optional(),
|
|
201
210
|
enableUndo: z.boolean().optional(),
|
|
202
211
|
enableHookLogDebug: z.boolean().optional(),
|
|
@@ -626,12 +635,17 @@ function getConfig(cwd) {
|
|
|
626
635
|
if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
|
|
627
636
|
mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
|
|
628
637
|
if (s.environment !== void 0) mergedSettings.environment = s.environment;
|
|
638
|
+
if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
|
|
629
639
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
630
640
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
631
641
|
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
632
642
|
if (p.toolInspection)
|
|
633
643
|
mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
634
|
-
if (p.smartRules)
|
|
644
|
+
if (p.smartRules) {
|
|
645
|
+
const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
|
|
646
|
+
const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
|
|
647
|
+
mergedPolicy.smartRules = [...defaultBlocks, ...p.smartRules, ...defaultNonBlocks];
|
|
648
|
+
}
|
|
635
649
|
if (p.snapshot) {
|
|
636
650
|
const s2 = p.snapshot;
|
|
637
651
|
if (s2.tools) mergedPolicy.snapshot.tools.push(...s2.tools);
|
|
@@ -827,7 +841,9 @@ var init_config = __esm({
|
|
|
827
841
|
{
|
|
828
842
|
field: "command",
|
|
829
843
|
op: "matches",
|
|
830
|
-
|
|
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|$)"
|
|
831
847
|
},
|
|
832
848
|
{
|
|
833
849
|
field: "command",
|
|
@@ -1741,9 +1757,9 @@ function matchesPattern(text, patterns) {
|
|
|
1741
1757
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1742
1758
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1743
1759
|
}
|
|
1744
|
-
function getNestedValue(obj,
|
|
1760
|
+
function getNestedValue(obj, path30) {
|
|
1745
1761
|
if (!obj || typeof obj !== "object") return null;
|
|
1746
|
-
return
|
|
1762
|
+
return path30.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1747
1763
|
}
|
|
1748
1764
|
function shouldSnapshot(toolName, args, config) {
|
|
1749
1765
|
if (!config.settings.enableUndo) return false;
|
|
@@ -1893,7 +1909,13 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1893
1909
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
1894
1910
|
reason: matchedRule.reason,
|
|
1895
1911
|
tier: 2,
|
|
1896
|
-
ruleName: matchedRule.name ?? matchedRule.tool
|
|
1912
|
+
ruleName: matchedRule.name ?? matchedRule.tool,
|
|
1913
|
+
...matchedRule.verdict === "block" && matchedRule.dependsOnState?.length && {
|
|
1914
|
+
dependsOnStatePredicates: matchedRule.dependsOnState
|
|
1915
|
+
},
|
|
1916
|
+
...matchedRule.verdict === "block" && matchedRule.recoveryCommand && {
|
|
1917
|
+
recoveryCommand: matchedRule.recoveryCommand
|
|
1918
|
+
}
|
|
1897
1919
|
};
|
|
1898
1920
|
}
|
|
1899
1921
|
}
|
|
@@ -2416,9 +2438,38 @@ var init_state = __esm({
|
|
|
2416
2438
|
|
|
2417
2439
|
// src/auth/daemon.ts
|
|
2418
2440
|
import fs9 from "fs";
|
|
2441
|
+
import net from "net";
|
|
2419
2442
|
import path10 from "path";
|
|
2420
2443
|
import os8 from "os";
|
|
2421
2444
|
import { spawnSync } from "child_process";
|
|
2445
|
+
function notifyActivitySocket(data) {
|
|
2446
|
+
return new Promise((resolve) => {
|
|
2447
|
+
try {
|
|
2448
|
+
const payload = JSON.stringify(data);
|
|
2449
|
+
const sock = net.createConnection(ACTIVITY_SOCKET_PATH);
|
|
2450
|
+
sock.on("connect", () => {
|
|
2451
|
+
sock.on("close", resolve);
|
|
2452
|
+
sock.end(payload);
|
|
2453
|
+
});
|
|
2454
|
+
sock.on("error", resolve);
|
|
2455
|
+
} catch {
|
|
2456
|
+
resolve();
|
|
2457
|
+
}
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
async function checkStatePredicates(predicates) {
|
|
2461
|
+
if (predicates.length === 0) return {};
|
|
2462
|
+
try {
|
|
2463
|
+
const qs = predicates.map(encodeURIComponent).join(",");
|
|
2464
|
+
const res = await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/state/check?predicates=${qs}`, {
|
|
2465
|
+
signal: AbortSignal.timeout(100)
|
|
2466
|
+
});
|
|
2467
|
+
if (!res.ok) return null;
|
|
2468
|
+
return await res.json();
|
|
2469
|
+
} catch {
|
|
2470
|
+
return null;
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2422
2473
|
function getInternalToken() {
|
|
2423
2474
|
try {
|
|
2424
2475
|
const pidFile = path10.join(os8.homedir(), ".node9", "daemon.pid");
|
|
@@ -2452,7 +2503,7 @@ function isDaemonRunning() {
|
|
|
2452
2503
|
return false;
|
|
2453
2504
|
}
|
|
2454
2505
|
}
|
|
2455
|
-
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd) {
|
|
2506
|
+
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly) {
|
|
2456
2507
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2457
2508
|
const ctrl = new AbortController();
|
|
2458
2509
|
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
@@ -2470,7 +2521,10 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
2470
2521
|
// activity-result as the CLI used for the pending activity event.
|
|
2471
2522
|
activityId,
|
|
2472
2523
|
...riskMetadata && { riskMetadata },
|
|
2473
|
-
...cwd && { cwd }
|
|
2524
|
+
...cwd && { cwd },
|
|
2525
|
+
...recoveryCommand && { recoveryCommand },
|
|
2526
|
+
...skipBackgroundAuth && { skipBackgroundAuth: true },
|
|
2527
|
+
...viewOnly && { viewOnly: true }
|
|
2474
2528
|
}),
|
|
2475
2529
|
signal: ctrl.signal
|
|
2476
2530
|
});
|
|
@@ -2490,10 +2544,10 @@ async function waitForDaemonDecision(id, signal) {
|
|
|
2490
2544
|
try {
|
|
2491
2545
|
const waitRes = await fetch(`${base}/wait/${id}`, { signal: waitCtrl.signal });
|
|
2492
2546
|
if (!waitRes.ok) return { decision: "deny" };
|
|
2493
|
-
const { decision, source } = await waitRes.json();
|
|
2547
|
+
const { decision, source, reason } = await waitRes.json();
|
|
2494
2548
|
if (decision === "allow") return { decision: "allow", source };
|
|
2495
2549
|
if (decision === "abandoned") return { decision: "abandoned", source };
|
|
2496
|
-
return { decision: "deny", source };
|
|
2550
|
+
return { decision: "deny", source, reason };
|
|
2497
2551
|
} finally {
|
|
2498
2552
|
clearTimeout(waitTimer);
|
|
2499
2553
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
@@ -2579,10 +2633,11 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
|
2579
2633
|
signal: AbortSignal.timeout(3e3)
|
|
2580
2634
|
});
|
|
2581
2635
|
}
|
|
2582
|
-
var DAEMON_PORT, DAEMON_HOST;
|
|
2636
|
+
var ACTIVITY_SOCKET_PATH, DAEMON_PORT, DAEMON_HOST;
|
|
2583
2637
|
var init_daemon = __esm({
|
|
2584
2638
|
"src/auth/daemon.ts"() {
|
|
2585
2639
|
"use strict";
|
|
2640
|
+
ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path10.join(os8.tmpdir(), "node9-activity.sock");
|
|
2586
2641
|
DAEMON_PORT = 7391;
|
|
2587
2642
|
DAEMON_HOST = "127.0.0.1";
|
|
2588
2643
|
}
|
|
@@ -2922,6 +2977,7 @@ var init_native = __esm({
|
|
|
2922
2977
|
// src/auth/cloud.ts
|
|
2923
2978
|
import fs10 from "fs";
|
|
2924
2979
|
import os9 from "os";
|
|
2980
|
+
import path13 from "path";
|
|
2925
2981
|
function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
2926
2982
|
return fetch(`${creds.apiUrl}/audit`, {
|
|
2927
2983
|
method: "POST",
|
|
@@ -2946,6 +3002,33 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
2946
3002
|
async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
2947
3003
|
const controller = new AbortController();
|
|
2948
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
|
+
}
|
|
2949
3032
|
try {
|
|
2950
3033
|
const response = await fetch(creds.apiUrl, {
|
|
2951
3034
|
method: "POST",
|
|
@@ -2960,7 +3043,8 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
|
2960
3043
|
cwd: process.cwd(),
|
|
2961
3044
|
platform: os9.platform()
|
|
2962
3045
|
},
|
|
2963
|
-
...riskMetadata && { riskMetadata }
|
|
3046
|
+
...riskMetadata && { riskMetadata },
|
|
3047
|
+
...ciContext && { ciContext }
|
|
2964
3048
|
}),
|
|
2965
3049
|
signal: controller.signal
|
|
2966
3050
|
});
|
|
@@ -2986,12 +3070,17 @@ async function pollNode9SaaS(requestId, creds, signal) {
|
|
|
2986
3070
|
});
|
|
2987
3071
|
clearTimeout(pollTimer);
|
|
2988
3072
|
if (!statusRes.ok) continue;
|
|
2989
|
-
const
|
|
3073
|
+
const statusBody = await statusRes.json();
|
|
3074
|
+
const { status } = statusBody;
|
|
2990
3075
|
if (status === "APPROVED") {
|
|
2991
|
-
return { approved: true, reason };
|
|
3076
|
+
return { approved: true, reason: statusBody.reason };
|
|
2992
3077
|
}
|
|
2993
3078
|
if (status === "DENIED" || status === "AUTO_BLOCKED" || status === "TIMED_OUT") {
|
|
2994
|
-
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 };
|
|
2995
3084
|
}
|
|
2996
3085
|
} catch {
|
|
2997
3086
|
}
|
|
@@ -3036,9 +3125,6 @@ var init_cloud = __esm({
|
|
|
3036
3125
|
});
|
|
3037
3126
|
|
|
3038
3127
|
// src/auth/orchestrator.ts
|
|
3039
|
-
import net from "net";
|
|
3040
|
-
import path13 from "path";
|
|
3041
|
-
import os10 from "os";
|
|
3042
3128
|
import { randomUUID } from "crypto";
|
|
3043
3129
|
function isWriteTool(toolName) {
|
|
3044
3130
|
const t = toolName.toLowerCase().replace(/[^a-z_]/g, "_");
|
|
@@ -3075,19 +3161,7 @@ function isNetworkTool(toolName, args) {
|
|
|
3075
3161
|
return false;
|
|
3076
3162
|
}
|
|
3077
3163
|
function notifyActivity(data) {
|
|
3078
|
-
return
|
|
3079
|
-
try {
|
|
3080
|
-
const payload = JSON.stringify(data);
|
|
3081
|
-
const sock = net.createConnection(ACTIVITY_SOCKET_PATH);
|
|
3082
|
-
sock.on("connect", () => {
|
|
3083
|
-
sock.on("close", resolve);
|
|
3084
|
-
sock.end(payload);
|
|
3085
|
-
});
|
|
3086
|
-
sock.on("error", resolve);
|
|
3087
|
-
} catch {
|
|
3088
|
-
resolve();
|
|
3089
|
-
}
|
|
3090
|
-
});
|
|
3164
|
+
return notifyActivitySocket(data);
|
|
3091
3165
|
}
|
|
3092
3166
|
async function authorizeHeadless(toolName, args, meta, options) {
|
|
3093
3167
|
if (!options?.calledFromDaemon) {
|
|
@@ -3104,7 +3178,9 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
3104
3178
|
tool: toolName,
|
|
3105
3179
|
ts: actTs,
|
|
3106
3180
|
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
|
|
3107
|
-
label: result.blockedByLabel
|
|
3181
|
+
label: result.blockedByLabel,
|
|
3182
|
+
ruleHit: result.ruleHit,
|
|
3183
|
+
observeWouldBlock: result.observeWouldBlock
|
|
3108
3184
|
});
|
|
3109
3185
|
}
|
|
3110
3186
|
return result;
|
|
@@ -3131,10 +3207,12 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3131
3207
|
appendHookDebug(toolName, args, meta, hashAuditArgs);
|
|
3132
3208
|
}
|
|
3133
3209
|
const isManual = meta?.agent === "Terminal";
|
|
3210
|
+
const isObserveMode = config.settings.mode === "observe";
|
|
3134
3211
|
let explainableLabel = "Local Config";
|
|
3135
3212
|
let policyMatchedField;
|
|
3136
3213
|
let policyMatchedWord;
|
|
3137
3214
|
let riskMetadata;
|
|
3215
|
+
let statefulRecoveryCommand;
|
|
3138
3216
|
let taintWarning = null;
|
|
3139
3217
|
if (isNetworkTool(toolName, args)) {
|
|
3140
3218
|
const filePaths = extractFilePaths(toolName, args);
|
|
@@ -3155,10 +3233,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3155
3233
|
if (dlpMatch) {
|
|
3156
3234
|
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
3157
3235
|
if (dlpMatch.severity === "block") {
|
|
3158
|
-
if (!isManual)
|
|
3236
|
+
if (!isManual)
|
|
3237
|
+
appendLocalAudit(
|
|
3238
|
+
toolName,
|
|
3239
|
+
args,
|
|
3240
|
+
"deny",
|
|
3241
|
+
isObserveMode ? "observe-mode-dlp-would-block" : "dlp-block",
|
|
3242
|
+
meta,
|
|
3243
|
+
true
|
|
3244
|
+
);
|
|
3159
3245
|
if (isWriteTool(toolName) && filePath) {
|
|
3160
3246
|
await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`);
|
|
3161
3247
|
}
|
|
3248
|
+
if (isObserveMode) {
|
|
3249
|
+
return {
|
|
3250
|
+
approved: true,
|
|
3251
|
+
checkedBy: "audit",
|
|
3252
|
+
observeWouldBlock: true,
|
|
3253
|
+
blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
|
|
3254
|
+
};
|
|
3255
|
+
}
|
|
3162
3256
|
return {
|
|
3163
3257
|
approved: false,
|
|
3164
3258
|
reason: dlpReason,
|
|
@@ -3171,6 +3265,31 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3171
3265
|
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
3172
3266
|
}
|
|
3173
3267
|
}
|
|
3268
|
+
if (isObserveMode) {
|
|
3269
|
+
if (!isIgnoredTool(toolName)) {
|
|
3270
|
+
const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
|
|
3271
|
+
const wouldBlock = policyResult.decision === "block";
|
|
3272
|
+
if (!isManual)
|
|
3273
|
+
appendLocalAudit(
|
|
3274
|
+
toolName,
|
|
3275
|
+
args,
|
|
3276
|
+
"allow",
|
|
3277
|
+
wouldBlock ? "observe-mode-would-block" : "observe-mode",
|
|
3278
|
+
meta,
|
|
3279
|
+
hashAuditArgs
|
|
3280
|
+
);
|
|
3281
|
+
return {
|
|
3282
|
+
approved: true,
|
|
3283
|
+
checkedBy: "audit",
|
|
3284
|
+
...wouldBlock && {
|
|
3285
|
+
observeWouldBlock: true,
|
|
3286
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
3287
|
+
ruleHit: policyResult.ruleName
|
|
3288
|
+
}
|
|
3289
|
+
};
|
|
3290
|
+
}
|
|
3291
|
+
return { approved: true, checkedBy: "audit" };
|
|
3292
|
+
}
|
|
3174
3293
|
if (config.settings.mode === "audit") {
|
|
3175
3294
|
if (!isIgnoredTool(toolName)) {
|
|
3176
3295
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
|
|
@@ -3193,19 +3312,46 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3193
3312
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
3194
3313
|
if (policyResult.decision === "allow") {
|
|
3195
3314
|
if (approvers.cloud && creds?.apiKey)
|
|
3196
|
-
auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
3315
|
+
await auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
3197
3316
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta, hashAuditArgs);
|
|
3198
3317
|
return { approved: true, checkedBy: "local-policy" };
|
|
3199
3318
|
}
|
|
3200
3319
|
if (policyResult.decision === "block") {
|
|
3201
|
-
if (
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3320
|
+
if (policyResult.dependsOnStatePredicates?.length) {
|
|
3321
|
+
const stateResults = await checkStatePredicates(policyResult.dependsOnStatePredicates);
|
|
3322
|
+
const predicatesMet = stateResults !== null && policyResult.dependsOnStatePredicates.every((p) => stateResults[p] === true);
|
|
3323
|
+
if (stateResults === null && !isManual) {
|
|
3324
|
+
appendToLog(HOOK_DEBUG_LOG, {
|
|
3325
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3326
|
+
event: "state-check-fail-open",
|
|
3327
|
+
tool: toolName,
|
|
3328
|
+
rule: policyResult.ruleName,
|
|
3329
|
+
predicates: policyResult.dependsOnStatePredicates,
|
|
3330
|
+
reason: "daemon unreachable or /state/check timed out \u2014 block rule downgraded to review"
|
|
3331
|
+
});
|
|
3332
|
+
}
|
|
3333
|
+
if (predicatesMet && policyResult.recoveryCommand) {
|
|
3334
|
+
statefulRecoveryCommand = policyResult.recoveryCommand;
|
|
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);
|
|
3341
|
+
} else {
|
|
3342
|
+
if (!isManual)
|
|
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);
|
|
3346
|
+
return {
|
|
3347
|
+
approved: false,
|
|
3348
|
+
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
3349
|
+
blockedBy: "local-config",
|
|
3350
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
3351
|
+
ruleHit: policyResult.ruleName,
|
|
3352
|
+
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand }
|
|
3353
|
+
};
|
|
3354
|
+
}
|
|
3209
3355
|
}
|
|
3210
3356
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
3211
3357
|
policyMatchedField = policyResult.matchedField;
|
|
@@ -3312,7 +3458,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3312
3458
|
meta,
|
|
3313
3459
|
riskMetadata,
|
|
3314
3460
|
options?.activityId,
|
|
3315
|
-
options?.cwd
|
|
3461
|
+
options?.cwd,
|
|
3462
|
+
statefulRecoveryCommand
|
|
3316
3463
|
);
|
|
3317
3464
|
daemonEntryId = entry.id;
|
|
3318
3465
|
daemonAllowCount = entry.allowCount;
|
|
@@ -3373,20 +3520,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3373
3520
|
if (daemonEntryId && (approvers.browser || approvers.terminal)) {
|
|
3374
3521
|
racePromises.push(
|
|
3375
3522
|
(async () => {
|
|
3376
|
-
const {
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3523
|
+
const {
|
|
3524
|
+
decision: daemonDecision,
|
|
3525
|
+
source: decisionSource,
|
|
3526
|
+
reason: daemonReason
|
|
3527
|
+
} = await waitForDaemonDecision(daemonEntryId, signal);
|
|
3380
3528
|
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
3381
3529
|
const isApproved = daemonDecision === "allow";
|
|
3382
|
-
const
|
|
3530
|
+
const isRedirect = decisionSource === "terminal-redirect";
|
|
3531
|
+
const src = decisionSource === "terminal" || decisionSource === "terminal-redirect" || decisionSource === "browser" ? decisionSource === "browser" ? "browser" : "terminal" : approvers.browser ? "browser" : "terminal";
|
|
3383
3532
|
const via = src === "terminal" ? "Terminal (node9 tail)" : "Browser Dashboard";
|
|
3384
3533
|
return {
|
|
3385
3534
|
approved: isApproved,
|
|
3386
|
-
reason: isApproved ? void 0 :
|
|
3535
|
+
reason: isApproved ? void 0 : (
|
|
3536
|
+
// Use the redirect reason from the tail when choice [2] was selected;
|
|
3537
|
+
// otherwise fall back to the generic rejection message.
|
|
3538
|
+
isRedirect && daemonReason || `The human user rejected this action via the Node9 ${via}.`
|
|
3539
|
+
),
|
|
3387
3540
|
checkedBy: isApproved ? "daemon" : void 0,
|
|
3388
3541
|
blockedBy: isApproved ? void 0 : "local-decision",
|
|
3389
|
-
blockedByLabel: `User Decision (${via})`,
|
|
3542
|
+
blockedByLabel: isRedirect ? "Steered Redirect (Terminal)" : `User Decision (${via})`,
|
|
3390
3543
|
decisionSource: src
|
|
3391
3544
|
};
|
|
3392
3545
|
})()
|
|
@@ -3461,7 +3614,7 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
3461
3614
|
}
|
|
3462
3615
|
return finalResult;
|
|
3463
3616
|
}
|
|
3464
|
-
var WRITE_TOOLS
|
|
3617
|
+
var WRITE_TOOLS;
|
|
3465
3618
|
var init_orchestrator = __esm({
|
|
3466
3619
|
"src/auth/orchestrator.ts"() {
|
|
3467
3620
|
"use strict";
|
|
@@ -3485,7 +3638,6 @@ var init_orchestrator = __esm({
|
|
|
3485
3638
|
"notebook_edit",
|
|
3486
3639
|
"notebookedit"
|
|
3487
3640
|
]);
|
|
3488
|
-
ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path13.join(os10.tmpdir(), "node9-activity.sock");
|
|
3489
3641
|
}
|
|
3490
3642
|
});
|
|
3491
3643
|
|
|
@@ -5291,11 +5443,112 @@ var init_taint_store = __esm({
|
|
|
5291
5443
|
}
|
|
5292
5444
|
});
|
|
5293
5445
|
|
|
5446
|
+
// src/daemon/session-counters.ts
|
|
5447
|
+
var SessionCounters, sessionCounters;
|
|
5448
|
+
var init_session_counters = __esm({
|
|
5449
|
+
"src/daemon/session-counters.ts"() {
|
|
5450
|
+
"use strict";
|
|
5451
|
+
SessionCounters = class {
|
|
5452
|
+
_allowed = 0;
|
|
5453
|
+
_blocked = 0;
|
|
5454
|
+
_dlpHits = 0;
|
|
5455
|
+
_wouldBlock = 0;
|
|
5456
|
+
_lastRuleHit = null;
|
|
5457
|
+
_lastBlockedTool = null;
|
|
5458
|
+
incrementAllowed() {
|
|
5459
|
+
this._allowed++;
|
|
5460
|
+
}
|
|
5461
|
+
incrementBlocked() {
|
|
5462
|
+
this._blocked++;
|
|
5463
|
+
}
|
|
5464
|
+
incrementDlpHits() {
|
|
5465
|
+
this._dlpHits++;
|
|
5466
|
+
}
|
|
5467
|
+
incrementWouldBlock() {
|
|
5468
|
+
this._wouldBlock++;
|
|
5469
|
+
}
|
|
5470
|
+
recordRuleHit(label) {
|
|
5471
|
+
this._lastRuleHit = label;
|
|
5472
|
+
}
|
|
5473
|
+
recordBlockedTool(toolName) {
|
|
5474
|
+
this._lastBlockedTool = toolName;
|
|
5475
|
+
}
|
|
5476
|
+
get() {
|
|
5477
|
+
return {
|
|
5478
|
+
allowed: this._allowed,
|
|
5479
|
+
blocked: this._blocked,
|
|
5480
|
+
dlpHits: this._dlpHits,
|
|
5481
|
+
wouldBlock: this._wouldBlock,
|
|
5482
|
+
lastRuleHit: this._lastRuleHit,
|
|
5483
|
+
lastBlockedTool: this._lastBlockedTool
|
|
5484
|
+
};
|
|
5485
|
+
}
|
|
5486
|
+
reset() {
|
|
5487
|
+
this._allowed = 0;
|
|
5488
|
+
this._blocked = 0;
|
|
5489
|
+
this._dlpHits = 0;
|
|
5490
|
+
this._wouldBlock = 0;
|
|
5491
|
+
this._lastRuleHit = null;
|
|
5492
|
+
this._lastBlockedTool = null;
|
|
5493
|
+
}
|
|
5494
|
+
};
|
|
5495
|
+
sessionCounters = new SessionCounters();
|
|
5496
|
+
}
|
|
5497
|
+
});
|
|
5498
|
+
|
|
5499
|
+
// src/daemon/session-history.ts
|
|
5500
|
+
var SessionHistory, sessionHistory;
|
|
5501
|
+
var init_session_history = __esm({
|
|
5502
|
+
"src/daemon/session-history.ts"() {
|
|
5503
|
+
"use strict";
|
|
5504
|
+
SessionHistory = class {
|
|
5505
|
+
lastEditAt = null;
|
|
5506
|
+
lastTestPassAt = null;
|
|
5507
|
+
lastTestFailAt = null;
|
|
5508
|
+
recordEdit(ts = Date.now()) {
|
|
5509
|
+
this.lastEditAt = ts;
|
|
5510
|
+
}
|
|
5511
|
+
recordTestPass(ts = Date.now()) {
|
|
5512
|
+
this.lastTestPassAt = ts;
|
|
5513
|
+
}
|
|
5514
|
+
recordTestFail(ts = Date.now()) {
|
|
5515
|
+
this.lastTestFailAt = ts;
|
|
5516
|
+
}
|
|
5517
|
+
/**
|
|
5518
|
+
* Returns true when the named predicate is currently satisfied.
|
|
5519
|
+
* Unknown predicates always return false (fail-open: don't block on unknown state).
|
|
5520
|
+
*/
|
|
5521
|
+
checkPredicate(name) {
|
|
5522
|
+
switch (name) {
|
|
5523
|
+
case "no_test_passed_since_last_edit":
|
|
5524
|
+
if (this.lastEditAt === null) return false;
|
|
5525
|
+
return this.lastTestPassAt === null || this.lastTestPassAt < this.lastEditAt;
|
|
5526
|
+
default:
|
|
5527
|
+
return false;
|
|
5528
|
+
}
|
|
5529
|
+
}
|
|
5530
|
+
getSnapshot() {
|
|
5531
|
+
return {
|
|
5532
|
+
lastEditAt: this.lastEditAt,
|
|
5533
|
+
lastTestPassAt: this.lastTestPassAt,
|
|
5534
|
+
lastTestFailAt: this.lastTestFailAt
|
|
5535
|
+
};
|
|
5536
|
+
}
|
|
5537
|
+
reset() {
|
|
5538
|
+
this.lastEditAt = null;
|
|
5539
|
+
this.lastTestPassAt = null;
|
|
5540
|
+
this.lastTestFailAt = null;
|
|
5541
|
+
}
|
|
5542
|
+
};
|
|
5543
|
+
sessionHistory = new SessionHistory();
|
|
5544
|
+
}
|
|
5545
|
+
});
|
|
5546
|
+
|
|
5294
5547
|
// src/daemon/state.ts
|
|
5295
5548
|
import net2 from "net";
|
|
5296
5549
|
import fs13 from "fs";
|
|
5297
5550
|
import path16 from "path";
|
|
5298
|
-
import
|
|
5551
|
+
import os11 from "os";
|
|
5299
5552
|
import { spawn as spawn2 } from "child_process";
|
|
5300
5553
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
5301
5554
|
function loadInsightCounts() {
|
|
@@ -5458,6 +5711,7 @@ function readBody(req) {
|
|
|
5458
5711
|
});
|
|
5459
5712
|
}
|
|
5460
5713
|
function openBrowser(url) {
|
|
5714
|
+
if (process.env.NODE9_TESTING === "1") return;
|
|
5461
5715
|
try {
|
|
5462
5716
|
const args = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
|
|
5463
5717
|
spawn2(args[0], args.slice(1), { detached: true, stdio: "ignore" }).unref();
|
|
@@ -5529,6 +5783,14 @@ function startActivitySocket() {
|
|
|
5529
5783
|
socket.on("end", () => {
|
|
5530
5784
|
try {
|
|
5531
5785
|
const data = JSON.parse(Buffer.concat(chunks).toString());
|
|
5786
|
+
if (data.status === "test_pass") {
|
|
5787
|
+
sessionHistory.recordTestPass(data.ts);
|
|
5788
|
+
return;
|
|
5789
|
+
}
|
|
5790
|
+
if (data.status === "test_fail") {
|
|
5791
|
+
sessionHistory.recordTestFail(data.ts);
|
|
5792
|
+
return;
|
|
5793
|
+
}
|
|
5532
5794
|
if (data.status === "pending") {
|
|
5533
5795
|
broadcast("activity", {
|
|
5534
5796
|
id: data.id,
|
|
@@ -5538,6 +5800,24 @@ function startActivitySocket() {
|
|
|
5538
5800
|
status: "pending"
|
|
5539
5801
|
});
|
|
5540
5802
|
} else {
|
|
5803
|
+
if (data.status === "allow") {
|
|
5804
|
+
sessionCounters.incrementAllowed();
|
|
5805
|
+
if (data.observeWouldBlock) sessionCounters.incrementWouldBlock();
|
|
5806
|
+
if (WRITE_TOOL_NAMES.has(data.tool.toLowerCase().replace(/[^a-z_]/g, "_"))) {
|
|
5807
|
+
sessionHistory.recordEdit(data.ts);
|
|
5808
|
+
}
|
|
5809
|
+
} else if (data.status === "block") {
|
|
5810
|
+
sessionCounters.incrementBlocked();
|
|
5811
|
+
sessionCounters.recordBlockedTool(data.tool);
|
|
5812
|
+
if (data.ruleHit) sessionCounters.recordRuleHit(data.ruleHit);
|
|
5813
|
+
} else if (data.status === "dlp") {
|
|
5814
|
+
sessionCounters.incrementBlocked();
|
|
5815
|
+
sessionCounters.incrementDlpHits();
|
|
5816
|
+
sessionCounters.recordBlockedTool(data.tool);
|
|
5817
|
+
} else if (data.status === "taint") {
|
|
5818
|
+
sessionCounters.incrementBlocked();
|
|
5819
|
+
sessionCounters.recordBlockedTool(data.tool);
|
|
5820
|
+
}
|
|
5541
5821
|
broadcast("activity-result", {
|
|
5542
5822
|
id: data.id,
|
|
5543
5823
|
status: data.status,
|
|
@@ -5558,14 +5838,16 @@ function startActivitySocket() {
|
|
|
5558
5838
|
}
|
|
5559
5839
|
});
|
|
5560
5840
|
}
|
|
5561
|
-
var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE;
|
|
5841
|
+
var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE, WRITE_TOOL_NAMES;
|
|
5562
5842
|
var init_state2 = __esm({
|
|
5563
5843
|
"src/daemon/state.ts"() {
|
|
5564
5844
|
"use strict";
|
|
5565
5845
|
init_daemon();
|
|
5566
5846
|
init_suggestion_tracker();
|
|
5567
5847
|
init_taint_store();
|
|
5568
|
-
|
|
5848
|
+
init_session_counters();
|
|
5849
|
+
init_session_history();
|
|
5850
|
+
homeDir = os11.homedir();
|
|
5569
5851
|
DAEMON_PID_FILE = path16.join(homeDir, ".node9", "daemon.pid");
|
|
5570
5852
|
DECISIONS_FILE = path16.join(homeDir, ".node9", "decisions.json");
|
|
5571
5853
|
AUDIT_LOG_FILE = path16.join(homeDir, ".node9", "audit.log");
|
|
@@ -5590,17 +5872,28 @@ var init_state2 = __esm({
|
|
|
5590
5872
|
"2h": 2 * 60 * 6e4
|
|
5591
5873
|
};
|
|
5592
5874
|
autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
5593
|
-
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path16.join(
|
|
5875
|
+
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path16.join(os11.tmpdir(), "node9-activity.sock");
|
|
5594
5876
|
ACTIVITY_RING_SIZE = 100;
|
|
5595
5877
|
activityRing = [];
|
|
5596
5878
|
SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
5879
|
+
WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
5880
|
+
"write",
|
|
5881
|
+
"write_file",
|
|
5882
|
+
"create_file",
|
|
5883
|
+
"edit",
|
|
5884
|
+
"multiedit",
|
|
5885
|
+
"str_replace_based_edit_tool",
|
|
5886
|
+
"replace",
|
|
5887
|
+
"notebook_edit",
|
|
5888
|
+
"notebookedit"
|
|
5889
|
+
]);
|
|
5597
5890
|
}
|
|
5598
5891
|
});
|
|
5599
5892
|
|
|
5600
5893
|
// src/config/patch.ts
|
|
5601
5894
|
import fs14 from "fs";
|
|
5602
5895
|
import path17 from "path";
|
|
5603
|
-
import
|
|
5896
|
+
import os12 from "os";
|
|
5604
5897
|
function patchConfig(configPath, patch) {
|
|
5605
5898
|
let config = {};
|
|
5606
5899
|
try {
|
|
@@ -5650,7 +5943,7 @@ var GLOBAL_CONFIG_PATH;
|
|
|
5650
5943
|
var init_patch = __esm({
|
|
5651
5944
|
"src/config/patch.ts"() {
|
|
5652
5945
|
"use strict";
|
|
5653
|
-
GLOBAL_CONFIG_PATH = path17.join(
|
|
5946
|
+
GLOBAL_CONFIG_PATH = path17.join(os12.homedir(), ".node9", "config.json");
|
|
5654
5947
|
}
|
|
5655
5948
|
});
|
|
5656
5949
|
|
|
@@ -5723,6 +6016,8 @@ data: ${JSON.stringify({
|
|
|
5723
6016
|
toolName: e.toolName,
|
|
5724
6017
|
args: e.args,
|
|
5725
6018
|
riskMetadata: e.riskMetadata,
|
|
6019
|
+
...e.recoveryCommand && { recoveryCommand: e.recoveryCommand },
|
|
6020
|
+
...e.viewOnly && { viewOnly: true },
|
|
5726
6021
|
slackDelegated: e.slackDelegated,
|
|
5727
6022
|
timestamp: e.timestamp,
|
|
5728
6023
|
agent: e.agent,
|
|
@@ -5788,6 +6083,9 @@ data: ${JSON.stringify(item.data)}
|
|
|
5788
6083
|
agent,
|
|
5789
6084
|
mcpServer,
|
|
5790
6085
|
riskMetadata,
|
|
6086
|
+
recoveryCommand,
|
|
6087
|
+
skipBackgroundAuth = false,
|
|
6088
|
+
viewOnly = false,
|
|
5791
6089
|
fromCLI = false,
|
|
5792
6090
|
activityId,
|
|
5793
6091
|
cwd
|
|
@@ -5798,6 +6096,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
5798
6096
|
toolName,
|
|
5799
6097
|
args,
|
|
5800
6098
|
riskMetadata: riskMetadata ?? void 0,
|
|
6099
|
+
...typeof recoveryCommand === "string" && recoveryCommand && { recoveryCommand },
|
|
6100
|
+
...viewOnly && { viewOnly: true },
|
|
5801
6101
|
agent: typeof agent === "string" ? agent : void 0,
|
|
5802
6102
|
mcpServer: typeof mcpServer === "string" ? mcpServer : void 0,
|
|
5803
6103
|
slackDelegated: !!slackDelegated,
|
|
@@ -5842,6 +6142,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
5842
6142
|
toolName,
|
|
5843
6143
|
args,
|
|
5844
6144
|
riskMetadata: entry.riskMetadata,
|
|
6145
|
+
...entry.recoveryCommand && { recoveryCommand: entry.recoveryCommand },
|
|
6146
|
+
...entry.viewOnly && { viewOnly: true },
|
|
5845
6147
|
slackDelegated: entry.slackDelegated,
|
|
5846
6148
|
agent: entry.agent,
|
|
5847
6149
|
mcpServer: entry.mcpServer,
|
|
@@ -5858,7 +6160,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5858
6160
|
}
|
|
5859
6161
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5860
6162
|
res.end(JSON.stringify({ id, allowCount: (insightCounts.get(toolName) ?? 0) + 1 }));
|
|
5861
|
-
if (slackDelegated) return;
|
|
6163
|
+
if (slackDelegated || skipBackgroundAuth) return;
|
|
5862
6164
|
authorizeHeadless(
|
|
5863
6165
|
toolName,
|
|
5864
6166
|
args,
|
|
@@ -5997,7 +6299,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5997
6299
|
saveInsightCounts();
|
|
5998
6300
|
suggestionTracker.resetTool(entry.toolName);
|
|
5999
6301
|
}
|
|
6000
|
-
const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
|
|
6302
|
+
const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native", "terminal-redirect"]);
|
|
6001
6303
|
if (source && VALID_SOURCES.has(source)) entry.decisionSource = source;
|
|
6002
6304
|
if (entry.waiter) {
|
|
6003
6305
|
entry.waiter(resolvedDecision, reason);
|
|
@@ -6026,6 +6328,41 @@ data: ${JSON.stringify(item.data)}
|
|
|
6026
6328
|
return res.end(JSON.stringify({ error: "internal" }));
|
|
6027
6329
|
}
|
|
6028
6330
|
}
|
|
6331
|
+
if (req.method === "GET" && pathname === "/status") {
|
|
6332
|
+
try {
|
|
6333
|
+
const s = getGlobalSettings();
|
|
6334
|
+
const counters = sessionCounters.get();
|
|
6335
|
+
const mode = s.mode ?? "standard";
|
|
6336
|
+
const status = {
|
|
6337
|
+
mode,
|
|
6338
|
+
session: {
|
|
6339
|
+
allowed: counters.allowed,
|
|
6340
|
+
blocked: counters.blocked,
|
|
6341
|
+
dlpHits: counters.dlpHits,
|
|
6342
|
+
wouldBlock: counters.wouldBlock
|
|
6343
|
+
},
|
|
6344
|
+
taintedCount: taintStore.list().length,
|
|
6345
|
+
lastRuleHit: counters.lastRuleHit,
|
|
6346
|
+
lastBlockedTool: counters.lastBlockedTool
|
|
6347
|
+
};
|
|
6348
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6349
|
+
return res.end(JSON.stringify(status));
|
|
6350
|
+
} catch (err) {
|
|
6351
|
+
console.error(chalk2.red("[node9 daemon] GET /status failed:"), err);
|
|
6352
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
6353
|
+
return res.end(JSON.stringify({ error: "internal" }));
|
|
6354
|
+
}
|
|
6355
|
+
}
|
|
6356
|
+
if (req.method === "GET" && pathname === "/state/check") {
|
|
6357
|
+
const predicatesParam = reqUrl.searchParams.get("predicates") ?? "";
|
|
6358
|
+
const predicates = predicatesParam.split(",").filter(Boolean);
|
|
6359
|
+
const results = {};
|
|
6360
|
+
for (const p of predicates) {
|
|
6361
|
+
results[p] = sessionHistory.checkPredicate(p);
|
|
6362
|
+
}
|
|
6363
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6364
|
+
return res.end(JSON.stringify(results));
|
|
6365
|
+
}
|
|
6029
6366
|
if (req.method === "POST" && pathname === "/settings") {
|
|
6030
6367
|
if (!validToken(req)) return res.writeHead(403).end();
|
|
6031
6368
|
try {
|
|
@@ -6429,11 +6766,11 @@ __export(tail_exports, {
|
|
|
6429
6766
|
startTail: () => startTail
|
|
6430
6767
|
});
|
|
6431
6768
|
import http2 from "http";
|
|
6432
|
-
import
|
|
6769
|
+
import chalk17 from "chalk";
|
|
6433
6770
|
import fs24 from "fs";
|
|
6434
|
-
import
|
|
6435
|
-
import
|
|
6436
|
-
import
|
|
6771
|
+
import os20 from "os";
|
|
6772
|
+
import path27 from "path";
|
|
6773
|
+
import readline4 from "readline";
|
|
6437
6774
|
import { spawn as spawn9, execSync as execSync3 } from "child_process";
|
|
6438
6775
|
function getIcon(tool) {
|
|
6439
6776
|
const t = tool.toLowerCase();
|
|
@@ -6448,27 +6785,27 @@ function formatBase(activity) {
|
|
|
6448
6785
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
6449
6786
|
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
|
|
6450
6787
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
6451
|
-
return `${
|
|
6788
|
+
return `${chalk17.gray(time)} ${icon} ${chalk17.white.bold(toolName)} ${chalk17.dim(argsPreview)}`;
|
|
6452
6789
|
}
|
|
6453
6790
|
function renderResult(activity, result) {
|
|
6454
6791
|
const base = formatBase(activity);
|
|
6455
6792
|
let status;
|
|
6456
6793
|
if (result.status === "allow") {
|
|
6457
|
-
status =
|
|
6794
|
+
status = chalk17.green("\u2713 ALLOW");
|
|
6458
6795
|
} else if (result.status === "dlp") {
|
|
6459
|
-
status =
|
|
6796
|
+
status = chalk17.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
6460
6797
|
} else {
|
|
6461
|
-
status =
|
|
6798
|
+
status = chalk17.red("\u2717 BLOCK");
|
|
6462
6799
|
}
|
|
6463
6800
|
if (process.stdout.isTTY) {
|
|
6464
|
-
|
|
6465
|
-
|
|
6801
|
+
readline4.clearLine(process.stdout, 0);
|
|
6802
|
+
readline4.cursorTo(process.stdout, 0);
|
|
6466
6803
|
}
|
|
6467
6804
|
console.log(`${base} ${status}`);
|
|
6468
6805
|
}
|
|
6469
6806
|
function renderPending(activity) {
|
|
6470
6807
|
if (!process.stdout.isTTY) return;
|
|
6471
|
-
process.stdout.write(`${formatBase(activity)} ${
|
|
6808
|
+
process.stdout.write(`${formatBase(activity)} ${chalk17.yellow("\u25CF \u2026")}\r`);
|
|
6472
6809
|
}
|
|
6473
6810
|
async function ensureDaemon() {
|
|
6474
6811
|
let pidPort = null;
|
|
@@ -6477,7 +6814,7 @@ async function ensureDaemon() {
|
|
|
6477
6814
|
const { port } = JSON.parse(fs24.readFileSync(PID_FILE, "utf-8"));
|
|
6478
6815
|
pidPort = port;
|
|
6479
6816
|
} catch {
|
|
6480
|
-
console.error(
|
|
6817
|
+
console.error(chalk17.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
6481
6818
|
}
|
|
6482
6819
|
}
|
|
6483
6820
|
const checkPort = pidPort ?? DAEMON_PORT;
|
|
@@ -6488,7 +6825,7 @@ async function ensureDaemon() {
|
|
|
6488
6825
|
if (res.ok) return checkPort;
|
|
6489
6826
|
} catch {
|
|
6490
6827
|
}
|
|
6491
|
-
console.log(
|
|
6828
|
+
console.log(chalk17.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
6492
6829
|
const child = spawn9(process.execPath, [process.argv[1], "daemon"], {
|
|
6493
6830
|
detached: true,
|
|
6494
6831
|
stdio: "ignore",
|
|
@@ -6505,14 +6842,15 @@ async function ensureDaemon() {
|
|
|
6505
6842
|
} catch {
|
|
6506
6843
|
}
|
|
6507
6844
|
}
|
|
6508
|
-
console.error(
|
|
6845
|
+
console.error(chalk17.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
6509
6846
|
process.exit(1);
|
|
6510
6847
|
}
|
|
6511
6848
|
function postDecisionHttp(id, decision, csrfToken, port, opts) {
|
|
6512
6849
|
return new Promise((resolve, reject) => {
|
|
6513
|
-
const bodyObj = { decision, source: "terminal" };
|
|
6850
|
+
const bodyObj = { decision, source: opts?.source ?? "terminal" };
|
|
6514
6851
|
if (opts?.persist) bodyObj.persist = true;
|
|
6515
6852
|
if (opts?.trustDuration) bodyObj.trustDuration = opts.trustDuration;
|
|
6853
|
+
if (opts?.reason) bodyObj.reason = opts.reason;
|
|
6516
6854
|
const body = JSON.stringify(bodyObj);
|
|
6517
6855
|
const req = http2.request(
|
|
6518
6856
|
{
|
|
@@ -6537,33 +6875,61 @@ function postDecisionHttp(id, decision, csrfToken, port, opts) {
|
|
|
6537
6875
|
});
|
|
6538
6876
|
}
|
|
6539
6877
|
function buildCardLines(req, localCount = 0) {
|
|
6878
|
+
if (req.recoveryCommand) {
|
|
6879
|
+
return buildRecoveryCardLines(req);
|
|
6880
|
+
}
|
|
6540
6881
|
const argsStr = JSON.stringify(req.args ?? {}).replace(/\s+/g, " ");
|
|
6541
6882
|
const argsPreview = argsStr.length > 60 ? argsStr.slice(0, 60) + "\u2026" : argsStr;
|
|
6542
6883
|
const tierLabel = req.riskMetadata?.tier != null ? req.riskMetadata.tier <= 2 ? `${YELLOW}\u26A0 Tier ${req.riskMetadata.tier}` : `${RED}\u{1F6D1} Tier ${req.riskMetadata.tier}` : `${YELLOW}\u26A0 Review`;
|
|
6543
6884
|
const blockedBy = req.riskMetadata?.blockedByLabel ?? "Policy rule";
|
|
6544
6885
|
const lines = [
|
|
6545
6886
|
``,
|
|
6546
|
-
`${
|
|
6547
|
-
`${CYAN}\u2551${
|
|
6548
|
-
`${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}`
|
|
6549
6890
|
];
|
|
6550
6891
|
if (req.riskMetadata?.ruleName && blockedBy.includes("Taint")) {
|
|
6551
|
-
lines.push(`${CYAN}\u2551${
|
|
6892
|
+
lines.push(`${CYAN}\u2551${RESET2} ${YELLOW}\u26A0 ${req.riskMetadata.ruleName}${RESET2}`);
|
|
6552
6893
|
}
|
|
6553
|
-
lines.push(`${CYAN}\u2551${
|
|
6894
|
+
lines.push(`${CYAN}\u2551${RESET2} Args: ${GRAY}${argsPreview}${RESET2}`);
|
|
6554
6895
|
if (localCount >= 2) {
|
|
6555
6896
|
lines.push(
|
|
6556
|
-
`${CYAN}\u2551${
|
|
6897
|
+
`${CYAN}\u2551${RESET2} ${YELLOW}\u{1F4A1}${RESET2} Approved ${localCount}\xD7 before \u2014 ${BOLD2}[a]${RESET2}${YELLOW} creates a permanent rule${RESET2}`
|
|
6557
6898
|
);
|
|
6558
6899
|
}
|
|
6559
6900
|
lines.push(
|
|
6560
|
-
`${CYAN}\u255A${
|
|
6901
|
+
`${CYAN}\u255A${RESET2}`,
|
|
6561
6902
|
``,
|
|
6562
|
-
` ${
|
|
6903
|
+
` ${BOLD2}${GREEN}[\u21B5/y]${RESET2} Allow ${BOLD2}${RED}[n]${RESET2} Deny ${BOLD2}${YELLOW}[a]${RESET2} Always Allow ${BOLD2}${CYAN}[t]${RESET2} Trust 30m`,
|
|
6563
6904
|
``
|
|
6564
6905
|
);
|
|
6565
6906
|
return lines;
|
|
6566
6907
|
}
|
|
6908
|
+
function buildRecoveryCardLines(req) {
|
|
6909
|
+
const argsObj = req.args;
|
|
6910
|
+
const command = typeof argsObj?.command === "string" ? argsObj.command : JSON.stringify(req.args ?? {}).replace(/\s+/g, " ").slice(0, 60);
|
|
6911
|
+
const ruleName = req.riskMetadata?.ruleName?.replace(/^Smart Rule:\s*/i, "") ?? "policy rule";
|
|
6912
|
+
const recoveryCommand = req.recoveryCommand;
|
|
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}`,
|
|
6917
|
+
``,
|
|
6918
|
+
` ${GRAY}[Timeout: auto-deny]${RESET2}`,
|
|
6919
|
+
` Select [1-3]: `
|
|
6920
|
+
];
|
|
6921
|
+
return [
|
|
6922
|
+
``,
|
|
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}`, ``] : [],
|
|
6928
|
+
...interactiveLines,
|
|
6929
|
+
`${CYAN}${DIVIDER}${RESET2}`,
|
|
6930
|
+
``
|
|
6931
|
+
];
|
|
6932
|
+
}
|
|
6567
6933
|
async function startTail(options = {}) {
|
|
6568
6934
|
const port = await ensureDaemon();
|
|
6569
6935
|
if (options.clear) {
|
|
@@ -6590,7 +6956,7 @@ async function startTail(options = {}) {
|
|
|
6590
6956
|
req2.end();
|
|
6591
6957
|
});
|
|
6592
6958
|
if (result.ok) {
|
|
6593
|
-
console.log(
|
|
6959
|
+
console.log(chalk17.green("\u2713 Flight Recorder buffer cleared."));
|
|
6594
6960
|
} else if (result.code === "ECONNREFUSED") {
|
|
6595
6961
|
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
6596
6962
|
} else if (result.code === "ETIMEDOUT") {
|
|
@@ -6609,10 +6975,10 @@ async function startTail(options = {}) {
|
|
|
6609
6975
|
let cancelActiveCard = null;
|
|
6610
6976
|
const localAllowCounts = /* @__PURE__ */ new Map();
|
|
6611
6977
|
const canApprove = process.stdout.isTTY && process.stdin.isTTY;
|
|
6612
|
-
if (canApprove)
|
|
6978
|
+
if (canApprove) readline4.emitKeypressEvents(process.stdin);
|
|
6613
6979
|
function clearCard() {
|
|
6614
6980
|
if (cardLineCount > 0) {
|
|
6615
|
-
|
|
6981
|
+
readline4.moveCursor(process.stdout, 0, -cardLineCount);
|
|
6616
6982
|
process.stdout.write(ERASE_DOWN);
|
|
6617
6983
|
cardLineCount = 0;
|
|
6618
6984
|
}
|
|
@@ -6662,14 +7028,14 @@ async function startTail(options = {}) {
|
|
|
6662
7028
|
localAllowCounts.get(req2.toolName) ?? 0
|
|
6663
7029
|
)
|
|
6664
7030
|
);
|
|
6665
|
-
const decisionStamp = action === "always-allow" ?
|
|
6666
|
-
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}`, ``);
|
|
6667
7033
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
6668
7034
|
process.stdout.write(SHOW_CURSOR);
|
|
6669
7035
|
cardLineCount = 0;
|
|
6670
7036
|
if (action === "allow" || action === "always-allow" || action === "trust") {
|
|
6671
7037
|
localAllowCounts.set(req2.toolName, (localAllowCounts.get(req2.toolName) ?? 0) + 1);
|
|
6672
|
-
} else if (action === "deny") {
|
|
7038
|
+
} else if (action === "deny" || action === "redirect") {
|
|
6673
7039
|
localAllowCounts.delete(req2.toolName);
|
|
6674
7040
|
}
|
|
6675
7041
|
let httpDecision;
|
|
@@ -6680,13 +7046,18 @@ async function startTail(options = {}) {
|
|
|
6680
7046
|
} else if (action === "trust") {
|
|
6681
7047
|
httpDecision = "trust";
|
|
6682
7048
|
httpOpts = { trustDuration: "30m" };
|
|
7049
|
+
} else if (action === "redirect") {
|
|
7050
|
+
httpDecision = "deny";
|
|
7051
|
+
const recoveryCommand = req2.recoveryCommand ?? "the required pre-condition";
|
|
7052
|
+
const redirectReason = `USER INTERVENTION: I am blocking this ${req2.toolName} because the required pre-condition has not been met. Please execute \`${recoveryCommand}\`. If it passes, you are then authorized to run \`${req2.toolName}\`.`;
|
|
7053
|
+
httpOpts = { reason: redirectReason, source: "terminal-redirect" };
|
|
6683
7054
|
} else {
|
|
6684
7055
|
httpDecision = action;
|
|
6685
7056
|
}
|
|
6686
7057
|
postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err) => {
|
|
6687
7058
|
try {
|
|
6688
7059
|
fs24.appendFileSync(
|
|
6689
|
-
|
|
7060
|
+
path27.join(os20.homedir(), ".node9", "hook-debug.log"),
|
|
6690
7061
|
`[tail] POST /decision failed: ${String(err)}
|
|
6691
7062
|
`
|
|
6692
7063
|
);
|
|
@@ -6708,8 +7079,8 @@ async function startTail(options = {}) {
|
|
|
6708
7079
|
);
|
|
6709
7080
|
const stampedLines = buildCardLines(req2, priorCount);
|
|
6710
7081
|
if (externalDecision) {
|
|
6711
|
-
const source = externalDecision === "allow" ?
|
|
6712
|
-
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}`, ``);
|
|
6713
7084
|
}
|
|
6714
7085
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
6715
7086
|
process.stdout.write(SHOW_CURSOR);
|
|
@@ -6718,17 +7089,34 @@ async function startTail(options = {}) {
|
|
|
6718
7089
|
cardActive = false;
|
|
6719
7090
|
showNextCard();
|
|
6720
7091
|
};
|
|
7092
|
+
if (req2.viewOnly) {
|
|
7093
|
+
process.stdin.resume();
|
|
7094
|
+
onKeypress = () => {
|
|
7095
|
+
};
|
|
7096
|
+
process.stdin.on("keypress", onKeypress);
|
|
7097
|
+
return;
|
|
7098
|
+
}
|
|
6721
7099
|
process.stdin.resume();
|
|
6722
7100
|
onKeypress = (_str, key) => {
|
|
6723
7101
|
const name = key?.name ?? "";
|
|
6724
|
-
if (
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
7102
|
+
if (req2.recoveryCommand) {
|
|
7103
|
+
if (name === "1") {
|
|
7104
|
+
settle("allow");
|
|
7105
|
+
} else if (name === "2") {
|
|
7106
|
+
settle("redirect");
|
|
7107
|
+
} else if (name === "3" || key?.ctrl && name === "c") {
|
|
7108
|
+
settle("deny");
|
|
7109
|
+
}
|
|
7110
|
+
} else {
|
|
7111
|
+
if (name === "y" || name === "return") {
|
|
7112
|
+
settle("allow");
|
|
7113
|
+
} else if (name === "n" || name === "d" || key?.ctrl && name === "c") {
|
|
7114
|
+
settle("deny");
|
|
7115
|
+
} else if (name === "a") {
|
|
7116
|
+
settle("always-allow");
|
|
7117
|
+
} else if (name === "t") {
|
|
7118
|
+
settle("trust");
|
|
7119
|
+
}
|
|
6732
7120
|
}
|
|
6733
7121
|
};
|
|
6734
7122
|
process.stdin.on("keypress", onKeypress);
|
|
@@ -6750,41 +7138,41 @@ async function startTail(options = {}) {
|
|
|
6750
7138
|
}
|
|
6751
7139
|
} catch {
|
|
6752
7140
|
}
|
|
6753
|
-
console.log(
|
|
6754
|
-
\u{1F6F0}\uFE0F Node9 tail `) +
|
|
7141
|
+
console.log(chalk17.cyan.bold(`
|
|
7142
|
+
\u{1F6F0}\uFE0F Node9 tail `) + chalk17.dim(`\u2192 ${dashboardUrl}`));
|
|
6755
7143
|
if (canApprove) {
|
|
6756
7144
|
console.log(
|
|
6757
|
-
|
|
7145
|
+
chalk17.dim("Interactive approvals: [\u21B5/y] Allow [n] Deny [a] Always Allow [t] Trust 30m")
|
|
6758
7146
|
);
|
|
6759
7147
|
}
|
|
6760
7148
|
if (options.history) {
|
|
6761
|
-
console.log(
|
|
7149
|
+
console.log(chalk17.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
|
|
6762
7150
|
} else {
|
|
6763
7151
|
console.log(
|
|
6764
|
-
|
|
7152
|
+
chalk17.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
|
|
6765
7153
|
);
|
|
6766
7154
|
}
|
|
6767
7155
|
process.on("SIGINT", () => {
|
|
6768
7156
|
clearCard();
|
|
6769
7157
|
process.stdout.write(SHOW_CURSOR);
|
|
6770
7158
|
if (process.stdout.isTTY) {
|
|
6771
|
-
|
|
6772
|
-
|
|
7159
|
+
readline4.clearLine(process.stdout, 0);
|
|
7160
|
+
readline4.cursorTo(process.stdout, 0);
|
|
6773
7161
|
}
|
|
6774
|
-
console.log(
|
|
7162
|
+
console.log(chalk17.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
6775
7163
|
process.exit(0);
|
|
6776
7164
|
});
|
|
6777
7165
|
const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
|
|
6778
7166
|
const req = http2.get(sseUrl, (res) => {
|
|
6779
7167
|
if (res.statusCode !== 200) {
|
|
6780
|
-
console.error(
|
|
7168
|
+
console.error(chalk17.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
6781
7169
|
process.exit(1);
|
|
6782
7170
|
}
|
|
6783
7171
|
let currentEvent = "";
|
|
6784
7172
|
let currentData = "";
|
|
6785
7173
|
res.on("error", () => {
|
|
6786
7174
|
});
|
|
6787
|
-
const rl =
|
|
7175
|
+
const rl = readline4.createInterface({ input: res, crlfDelay: Infinity });
|
|
6788
7176
|
rl.on("error", () => {
|
|
6789
7177
|
});
|
|
6790
7178
|
rl.on("line", (line) => {
|
|
@@ -6804,10 +7192,10 @@ async function startTail(options = {}) {
|
|
|
6804
7192
|
clearCard();
|
|
6805
7193
|
process.stdout.write(SHOW_CURSOR);
|
|
6806
7194
|
if (process.stdout.isTTY) {
|
|
6807
|
-
|
|
6808
|
-
|
|
7195
|
+
readline4.clearLine(process.stdout, 0);
|
|
7196
|
+
readline4.cursorTo(process.stdout, 0);
|
|
6809
7197
|
}
|
|
6810
|
-
console.log(
|
|
7198
|
+
console.log(chalk17.red("\n\u274C Daemon disconnected."));
|
|
6811
7199
|
process.exit(1);
|
|
6812
7200
|
});
|
|
6813
7201
|
});
|
|
@@ -6893,19 +7281,19 @@ async function startTail(options = {}) {
|
|
|
6893
7281
|
}
|
|
6894
7282
|
req.on("error", (err) => {
|
|
6895
7283
|
const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
|
|
6896
|
-
console.error(
|
|
7284
|
+
console.error(chalk17.red(`
|
|
6897
7285
|
\u274C ${msg}`));
|
|
6898
7286
|
process.exit(1);
|
|
6899
7287
|
});
|
|
6900
7288
|
}
|
|
6901
|
-
var PID_FILE, ICONS,
|
|
7289
|
+
var PID_FILE, ICONS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, DIVIDER;
|
|
6902
7290
|
var init_tail = __esm({
|
|
6903
7291
|
"src/tui/tail.ts"() {
|
|
6904
7292
|
"use strict";
|
|
6905
7293
|
init_daemon2();
|
|
6906
7294
|
init_daemon();
|
|
6907
7295
|
init_core();
|
|
6908
|
-
PID_FILE =
|
|
7296
|
+
PID_FILE = path27.join(os20.homedir(), ".node9", "daemon.pid");
|
|
6909
7297
|
ICONS = {
|
|
6910
7298
|
bash: "\u{1F4BB}",
|
|
6911
7299
|
shell: "\u{1F4BB}",
|
|
@@ -6923,8 +7311,8 @@ var init_tail = __esm({
|
|
|
6923
7311
|
delete: "\u{1F5D1}\uFE0F",
|
|
6924
7312
|
web: "\u{1F310}"
|
|
6925
7313
|
};
|
|
6926
|
-
|
|
6927
|
-
|
|
7314
|
+
RESET2 = "\x1B[0m";
|
|
7315
|
+
BOLD2 = "\x1B[1m";
|
|
6928
7316
|
RED = "\x1B[31m";
|
|
6929
7317
|
YELLOW = "\x1B[33m";
|
|
6930
7318
|
CYAN = "\x1B[36m";
|
|
@@ -6933,6 +7321,326 @@ var init_tail = __esm({
|
|
|
6933
7321
|
HIDE_CURSOR = "\x1B[?25l";
|
|
6934
7322
|
SHOW_CURSOR = "\x1B[?25h";
|
|
6935
7323
|
ERASE_DOWN = "\x1B[J";
|
|
7324
|
+
DIVIDER = "\u2500".repeat(60);
|
|
7325
|
+
}
|
|
7326
|
+
});
|
|
7327
|
+
|
|
7328
|
+
// src/cli/hud.ts
|
|
7329
|
+
var hud_exports = {};
|
|
7330
|
+
__export(hud_exports, {
|
|
7331
|
+
countConfigs: () => countConfigs,
|
|
7332
|
+
main: () => main,
|
|
7333
|
+
renderEnvironmentLine: () => renderEnvironmentLine
|
|
7334
|
+
});
|
|
7335
|
+
import fs25 from "fs";
|
|
7336
|
+
import path28 from "path";
|
|
7337
|
+
import os21 from "os";
|
|
7338
|
+
import http3 from "http";
|
|
7339
|
+
async function readStdin() {
|
|
7340
|
+
const chunks = [];
|
|
7341
|
+
for await (const chunk of process.stdin) {
|
|
7342
|
+
chunks.push(chunk);
|
|
7343
|
+
}
|
|
7344
|
+
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
7345
|
+
if (!raw) return {};
|
|
7346
|
+
try {
|
|
7347
|
+
return JSON.parse(raw);
|
|
7348
|
+
} catch {
|
|
7349
|
+
return {};
|
|
7350
|
+
}
|
|
7351
|
+
}
|
|
7352
|
+
function queryDaemon() {
|
|
7353
|
+
return new Promise((resolve) => {
|
|
7354
|
+
const timeout = setTimeout(() => resolve(null), 50);
|
|
7355
|
+
try {
|
|
7356
|
+
const req = http3.get(
|
|
7357
|
+
`http://${DAEMON_HOST}:${DAEMON_PORT}/status`,
|
|
7358
|
+
{ timeout: 50 },
|
|
7359
|
+
(res) => {
|
|
7360
|
+
const chunks = [];
|
|
7361
|
+
res.on("data", (c) => chunks.push(c));
|
|
7362
|
+
res.on("end", () => {
|
|
7363
|
+
clearTimeout(timeout);
|
|
7364
|
+
try {
|
|
7365
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString()));
|
|
7366
|
+
} catch {
|
|
7367
|
+
resolve(null);
|
|
7368
|
+
}
|
|
7369
|
+
});
|
|
7370
|
+
}
|
|
7371
|
+
);
|
|
7372
|
+
req.on("error", () => {
|
|
7373
|
+
clearTimeout(timeout);
|
|
7374
|
+
resolve(null);
|
|
7375
|
+
});
|
|
7376
|
+
req.on("timeout", () => {
|
|
7377
|
+
clearTimeout(timeout);
|
|
7378
|
+
req.destroy();
|
|
7379
|
+
resolve(null);
|
|
7380
|
+
});
|
|
7381
|
+
} catch {
|
|
7382
|
+
clearTimeout(timeout);
|
|
7383
|
+
resolve(null);
|
|
7384
|
+
}
|
|
7385
|
+
});
|
|
7386
|
+
}
|
|
7387
|
+
function dim(s) {
|
|
7388
|
+
return `${DIM}${s}${RESET3}`;
|
|
7389
|
+
}
|
|
7390
|
+
function bold(s) {
|
|
7391
|
+
return `${BOLD3}${s}${RESET3}`;
|
|
7392
|
+
}
|
|
7393
|
+
function color(c, s) {
|
|
7394
|
+
return `${c}${s}${RESET3}`;
|
|
7395
|
+
}
|
|
7396
|
+
function progressBar(pct, warnAt = 70, critAt = 85) {
|
|
7397
|
+
const filled = Math.round(Math.min(pct, 100) / 100 * BAR_WIDTH);
|
|
7398
|
+
const bar = BAR_FILLED.repeat(filled) + BAR_EMPTY.repeat(BAR_WIDTH - filled);
|
|
7399
|
+
const c = pct >= critAt ? RED2 : pct >= warnAt ? YELLOW2 : GREEN2;
|
|
7400
|
+
return `${c}${bar}${RESET3}`;
|
|
7401
|
+
}
|
|
7402
|
+
function formatTimeLeft(resetsAt) {
|
|
7403
|
+
if (!resetsAt) return "";
|
|
7404
|
+
const ms = new Date(resetsAt).getTime() - Date.now();
|
|
7405
|
+
if (ms <= 0) return "";
|
|
7406
|
+
const totalMin = Math.ceil(ms / 6e4);
|
|
7407
|
+
const h = Math.floor(totalMin / 60);
|
|
7408
|
+
const m = totalMin % 60;
|
|
7409
|
+
if (h > 0) return ` (${h}h ${m}m left)`;
|
|
7410
|
+
return ` (${m}m left)`;
|
|
7411
|
+
}
|
|
7412
|
+
function safeReadJson(filePath) {
|
|
7413
|
+
if (!fs25.existsSync(filePath)) return null;
|
|
7414
|
+
try {
|
|
7415
|
+
return JSON.parse(fs25.readFileSync(filePath, "utf-8"));
|
|
7416
|
+
} catch {
|
|
7417
|
+
return null;
|
|
7418
|
+
}
|
|
7419
|
+
}
|
|
7420
|
+
function getMcpServerNames(filePath) {
|
|
7421
|
+
const cfg = safeReadJson(filePath);
|
|
7422
|
+
if (!cfg || typeof cfg.mcpServers !== "object" || cfg.mcpServers === null) return /* @__PURE__ */ new Set();
|
|
7423
|
+
return new Set(Object.keys(cfg.mcpServers));
|
|
7424
|
+
}
|
|
7425
|
+
function getDisabledMcpServers(filePath, key) {
|
|
7426
|
+
const cfg = safeReadJson(filePath);
|
|
7427
|
+
if (!cfg || !Array.isArray(cfg[key])) return /* @__PURE__ */ new Set();
|
|
7428
|
+
return new Set(cfg[key].filter((s) => typeof s === "string"));
|
|
7429
|
+
}
|
|
7430
|
+
function countHooksInFile(filePath) {
|
|
7431
|
+
const cfg = safeReadJson(filePath);
|
|
7432
|
+
if (!cfg || typeof cfg.hooks !== "object" || cfg.hooks === null) return 0;
|
|
7433
|
+
return Object.keys(cfg.hooks).length;
|
|
7434
|
+
}
|
|
7435
|
+
function countRulesInDir(rulesDir) {
|
|
7436
|
+
if (!fs25.existsSync(rulesDir)) return 0;
|
|
7437
|
+
let count = 0;
|
|
7438
|
+
try {
|
|
7439
|
+
for (const entry of fs25.readdirSync(rulesDir, { withFileTypes: true })) {
|
|
7440
|
+
if (entry.isDirectory()) {
|
|
7441
|
+
count += countRulesInDir(path28.join(rulesDir, entry.name));
|
|
7442
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
7443
|
+
count++;
|
|
7444
|
+
}
|
|
7445
|
+
}
|
|
7446
|
+
} catch {
|
|
7447
|
+
}
|
|
7448
|
+
return count;
|
|
7449
|
+
}
|
|
7450
|
+
function isSamePath(a, b) {
|
|
7451
|
+
try {
|
|
7452
|
+
return path28.resolve(a) === path28.resolve(b);
|
|
7453
|
+
} catch {
|
|
7454
|
+
return false;
|
|
7455
|
+
}
|
|
7456
|
+
}
|
|
7457
|
+
function countConfigs(cwd) {
|
|
7458
|
+
const homeDir2 = os21.homedir();
|
|
7459
|
+
const claudeDir = path28.join(homeDir2, ".claude");
|
|
7460
|
+
let claudeMdCount = 0;
|
|
7461
|
+
let rulesCount = 0;
|
|
7462
|
+
let hooksCount = 0;
|
|
7463
|
+
const userMcpServers = /* @__PURE__ */ new Set();
|
|
7464
|
+
const projectMcpServers = /* @__PURE__ */ new Set();
|
|
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");
|
|
7468
|
+
for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
|
|
7469
|
+
hooksCount += countHooksInFile(userSettings);
|
|
7470
|
+
const userClaudeJson = path28.join(homeDir2, ".claude.json");
|
|
7471
|
+
for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
|
|
7472
|
+
for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
|
|
7473
|
+
userMcpServers.delete(name);
|
|
7474
|
+
}
|
|
7475
|
+
if (cwd) {
|
|
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");
|
|
7479
|
+
const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
|
|
7480
|
+
if (!overlapsUserScope) {
|
|
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");
|
|
7484
|
+
for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
|
|
7485
|
+
hooksCount += countHooksInFile(projSettings);
|
|
7486
|
+
}
|
|
7487
|
+
if (fs25.existsSync(path28.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
|
|
7488
|
+
const localSettings = path28.join(projectClaudeDir, "settings.local.json");
|
|
7489
|
+
for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
|
|
7490
|
+
hooksCount += countHooksInFile(localSettings);
|
|
7491
|
+
const mcpJsonServers = getMcpServerNames(path28.join(cwd, ".mcp.json"));
|
|
7492
|
+
const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
|
|
7493
|
+
for (const name of disabledMcpJson) mcpJsonServers.delete(name);
|
|
7494
|
+
for (const name of mcpJsonServers) projectMcpServers.add(name);
|
|
7495
|
+
}
|
|
7496
|
+
return {
|
|
7497
|
+
claudeMdCount,
|
|
7498
|
+
rulesCount,
|
|
7499
|
+
mcpCount: userMcpServers.size + projectMcpServers.size,
|
|
7500
|
+
hooksCount
|
|
7501
|
+
};
|
|
7502
|
+
}
|
|
7503
|
+
function renderEnvironmentLine(counts) {
|
|
7504
|
+
const { claudeMdCount, rulesCount, mcpCount, hooksCount } = counts;
|
|
7505
|
+
if (claudeMdCount === 0 && rulesCount === 0 && mcpCount === 0 && hooksCount === 0) return null;
|
|
7506
|
+
const parts = [
|
|
7507
|
+
`${claudeMdCount} CLAUDE.md`,
|
|
7508
|
+
`${rulesCount} rules`,
|
|
7509
|
+
`${mcpCount} MCPs`,
|
|
7510
|
+
`${hooksCount} hooks`
|
|
7511
|
+
];
|
|
7512
|
+
return color(DIM, parts.join(` ${dim("|")} `));
|
|
7513
|
+
}
|
|
7514
|
+
function renderOffline() {
|
|
7515
|
+
process.stdout.write(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")} ${dim("|")} ${dim("offline")}
|
|
7516
|
+
`);
|
|
7517
|
+
}
|
|
7518
|
+
function renderSecurityLine(status) {
|
|
7519
|
+
const parts = [];
|
|
7520
|
+
parts.push(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")}`);
|
|
7521
|
+
const modeColors = {
|
|
7522
|
+
standard: GREEN2,
|
|
7523
|
+
strict: RED2,
|
|
7524
|
+
observe: MAGENTA,
|
|
7525
|
+
audit: YELLOW2
|
|
7526
|
+
};
|
|
7527
|
+
const modeIcon = {
|
|
7528
|
+
standard: "",
|
|
7529
|
+
strict: "",
|
|
7530
|
+
observe: "\u{1F441} ",
|
|
7531
|
+
audit: ""
|
|
7532
|
+
};
|
|
7533
|
+
const mc = modeColors[status.mode] ?? WHITE;
|
|
7534
|
+
parts.push(`${dim("|")} ${color(mc, modeIcon[status.mode] ?? "")}${color(mc, status.mode)}`);
|
|
7535
|
+
if (status.mode === "observe") {
|
|
7536
|
+
parts.push(`${dim("|")} ${color(GREEN2, `\u2705 ${status.session.allowed} passed`)}`);
|
|
7537
|
+
if (status.session.wouldBlock > 0) {
|
|
7538
|
+
parts.push(color(YELLOW2, `\u26A0 ${status.session.wouldBlock} would-block`));
|
|
7539
|
+
}
|
|
7540
|
+
} else {
|
|
7541
|
+
parts.push(`${dim("|")} ${color(GREEN2, `\u2705 ${status.session.allowed} allowed`)}`);
|
|
7542
|
+
if (status.session.blocked > 0) {
|
|
7543
|
+
parts.push(color(RED2, `\u{1F6D1} ${status.session.blocked} blocked`));
|
|
7544
|
+
}
|
|
7545
|
+
if (status.session.dlpHits > 0) {
|
|
7546
|
+
parts.push(color(RED2, `\u{1F6A8} ${status.session.dlpHits} dlp`));
|
|
7547
|
+
}
|
|
7548
|
+
}
|
|
7549
|
+
if (status.taintedCount > 0) {
|
|
7550
|
+
parts.push(color(YELLOW2, `\u{1F4A7} ${status.taintedCount} tainted`));
|
|
7551
|
+
}
|
|
7552
|
+
if (status.lastRuleHit) {
|
|
7553
|
+
const ruleName = status.lastRuleHit.replace(/^Smart Rule:\s*/i, "");
|
|
7554
|
+
parts.push(color(CYAN2, `\u26A1 ${ruleName}`));
|
|
7555
|
+
}
|
|
7556
|
+
return parts.join(" ");
|
|
7557
|
+
}
|
|
7558
|
+
function renderContextLine(stdin) {
|
|
7559
|
+
const cw = stdin.context_window;
|
|
7560
|
+
if (!cw) return null;
|
|
7561
|
+
const parts = [];
|
|
7562
|
+
const modelName = typeof stdin.model === "string" ? stdin.model : stdin.model?.display_name ?? "";
|
|
7563
|
+
if (modelName) {
|
|
7564
|
+
parts.push(color(CYAN2, modelName));
|
|
7565
|
+
}
|
|
7566
|
+
const usedPct = cw.used_percentage ?? (cw.current_usage && cw.context_window_size ? Math.round(
|
|
7567
|
+
((cw.current_usage.input_tokens ?? 0) + (cw.current_usage.output_tokens ?? 0)) / cw.context_window_size * 100
|
|
7568
|
+
) : null);
|
|
7569
|
+
if (usedPct !== null) {
|
|
7570
|
+
const bar = progressBar(usedPct);
|
|
7571
|
+
parts.push(`${dim("\u2502")} ctx ${bar} ${usedPct}%`);
|
|
7572
|
+
}
|
|
7573
|
+
const rl = stdin.rate_limits;
|
|
7574
|
+
if (rl?.five_hour?.used_percentage !== void 0) {
|
|
7575
|
+
const pct = Math.round(rl.five_hour.used_percentage);
|
|
7576
|
+
const bar = progressBar(pct, 60, 80);
|
|
7577
|
+
const left = formatTimeLeft(rl.five_hour.resets_at);
|
|
7578
|
+
parts.push(`${dim("\u2502")} 5h ${bar} ${pct}%${left}`);
|
|
7579
|
+
}
|
|
7580
|
+
if (rl?.seven_day?.used_percentage !== void 0) {
|
|
7581
|
+
const pct = Math.round(rl.seven_day.used_percentage);
|
|
7582
|
+
const bar = progressBar(pct, 60, 80);
|
|
7583
|
+
parts.push(`${dim("\u2502")} 7d ${bar} ${pct}%`);
|
|
7584
|
+
}
|
|
7585
|
+
if (parts.length === 0) return null;
|
|
7586
|
+
return parts.join(" ");
|
|
7587
|
+
}
|
|
7588
|
+
async function main() {
|
|
7589
|
+
try {
|
|
7590
|
+
const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
|
|
7591
|
+
if (!daemonStatus2) {
|
|
7592
|
+
renderOffline();
|
|
7593
|
+
return;
|
|
7594
|
+
}
|
|
7595
|
+
process.stdout.write(renderSecurityLine(daemonStatus2) + "\n");
|
|
7596
|
+
const ctxLine = renderContextLine(stdin);
|
|
7597
|
+
if (ctxLine) {
|
|
7598
|
+
process.stdout.write(ctxLine + "\n");
|
|
7599
|
+
}
|
|
7600
|
+
const showEnvCounts = (() => {
|
|
7601
|
+
try {
|
|
7602
|
+
const cwd = stdin.cwd ?? process.cwd();
|
|
7603
|
+
for (const configPath of [
|
|
7604
|
+
path28.join(cwd, "node9.config.json"),
|
|
7605
|
+
path28.join(os21.homedir(), ".node9", "config.json")
|
|
7606
|
+
]) {
|
|
7607
|
+
if (!fs25.existsSync(configPath)) continue;
|
|
7608
|
+
const cfg = JSON.parse(fs25.readFileSync(configPath, "utf-8"));
|
|
7609
|
+
const hud = cfg.settings?.hud;
|
|
7610
|
+
if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
|
|
7611
|
+
}
|
|
7612
|
+
} catch {
|
|
7613
|
+
}
|
|
7614
|
+
return true;
|
|
7615
|
+
})();
|
|
7616
|
+
if (showEnvCounts) {
|
|
7617
|
+
const envLine = renderEnvironmentLine(countConfigs(stdin.cwd));
|
|
7618
|
+
if (envLine) {
|
|
7619
|
+
process.stdout.write(envLine + "\n");
|
|
7620
|
+
}
|
|
7621
|
+
}
|
|
7622
|
+
} catch {
|
|
7623
|
+
renderOffline();
|
|
7624
|
+
}
|
|
7625
|
+
}
|
|
7626
|
+
var RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH;
|
|
7627
|
+
var init_hud = __esm({
|
|
7628
|
+
"src/cli/hud.ts"() {
|
|
7629
|
+
"use strict";
|
|
7630
|
+
init_daemon();
|
|
7631
|
+
RESET3 = "\x1B[0m";
|
|
7632
|
+
BOLD3 = "\x1B[1m";
|
|
7633
|
+
DIM = "\x1B[2m";
|
|
7634
|
+
RED2 = "\x1B[31m";
|
|
7635
|
+
GREEN2 = "\x1B[32m";
|
|
7636
|
+
YELLOW2 = "\x1B[33m";
|
|
7637
|
+
BLUE = "\x1B[34m";
|
|
7638
|
+
MAGENTA = "\x1B[35m";
|
|
7639
|
+
CYAN2 = "\x1B[36m";
|
|
7640
|
+
WHITE = "\x1B[37m";
|
|
7641
|
+
BAR_FILLED = "\u2588";
|
|
7642
|
+
BAR_EMPTY = "\u2591";
|
|
7643
|
+
BAR_WIDTH = 10;
|
|
6936
7644
|
}
|
|
6937
7645
|
});
|
|
6938
7646
|
|
|
@@ -6943,7 +7651,7 @@ import { Command } from "commander";
|
|
|
6943
7651
|
// src/setup.ts
|
|
6944
7652
|
import fs11 from "fs";
|
|
6945
7653
|
import path14 from "path";
|
|
6946
|
-
import
|
|
7654
|
+
import os10 from "os";
|
|
6947
7655
|
import chalk from "chalk";
|
|
6948
7656
|
import { confirm } from "@inquirer/prompts";
|
|
6949
7657
|
function printDaemonTip() {
|
|
@@ -6977,7 +7685,7 @@ function isNode9Hook(cmd) {
|
|
|
6977
7685
|
return /(?:^|[\s/\\])node9 (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js (?:check|log)/.test(cmd);
|
|
6978
7686
|
}
|
|
6979
7687
|
function teardownClaude() {
|
|
6980
|
-
const homeDir2 =
|
|
7688
|
+
const homeDir2 = os10.homedir();
|
|
6981
7689
|
const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
|
|
6982
7690
|
const mcpPath = path14.join(homeDir2, ".claude.json");
|
|
6983
7691
|
let changed = false;
|
|
@@ -7027,7 +7735,7 @@ function teardownClaude() {
|
|
|
7027
7735
|
}
|
|
7028
7736
|
}
|
|
7029
7737
|
function teardownGemini() {
|
|
7030
|
-
const homeDir2 =
|
|
7738
|
+
const homeDir2 = os10.homedir();
|
|
7031
7739
|
const settingsPath = path14.join(homeDir2, ".gemini", "settings.json");
|
|
7032
7740
|
const settings = readJson(settingsPath);
|
|
7033
7741
|
if (!settings) {
|
|
@@ -7066,7 +7774,7 @@ function teardownGemini() {
|
|
|
7066
7774
|
}
|
|
7067
7775
|
}
|
|
7068
7776
|
function teardownCursor() {
|
|
7069
|
-
const homeDir2 =
|
|
7777
|
+
const homeDir2 = os10.homedir();
|
|
7070
7778
|
const mcpPath = path14.join(homeDir2, ".cursor", "mcp.json");
|
|
7071
7779
|
const mcpConfig = readJson(mcpPath);
|
|
7072
7780
|
if (!mcpConfig?.mcpServers) {
|
|
@@ -7093,7 +7801,7 @@ function teardownCursor() {
|
|
|
7093
7801
|
}
|
|
7094
7802
|
}
|
|
7095
7803
|
async function setupClaude() {
|
|
7096
|
-
const homeDir2 =
|
|
7804
|
+
const homeDir2 = os10.homedir();
|
|
7097
7805
|
const mcpPath = path14.join(homeDir2, ".claude.json");
|
|
7098
7806
|
const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
|
|
7099
7807
|
const claudeConfig = readJson(mcpPath) ?? {};
|
|
@@ -7169,7 +7877,7 @@ async function setupClaude() {
|
|
|
7169
7877
|
}
|
|
7170
7878
|
}
|
|
7171
7879
|
async function setupGemini() {
|
|
7172
|
-
const homeDir2 =
|
|
7880
|
+
const homeDir2 = os10.homedir();
|
|
7173
7881
|
const settingsPath = path14.join(homeDir2, ".gemini", "settings.json");
|
|
7174
7882
|
const settings = readJson(settingsPath) ?? {};
|
|
7175
7883
|
const servers = settings.mcpServers ?? {};
|
|
@@ -7251,7 +7959,7 @@ async function setupGemini() {
|
|
|
7251
7959
|
printDaemonTip();
|
|
7252
7960
|
}
|
|
7253
7961
|
}
|
|
7254
|
-
function detectAgents(homeDir2 =
|
|
7962
|
+
function detectAgents(homeDir2 = os10.homedir()) {
|
|
7255
7963
|
const exists = (p) => {
|
|
7256
7964
|
try {
|
|
7257
7965
|
return fs11.existsSync(p);
|
|
@@ -7271,7 +7979,7 @@ function detectAgents(homeDir2 = os11.homedir()) {
|
|
|
7271
7979
|
};
|
|
7272
7980
|
}
|
|
7273
7981
|
async function setupCursor() {
|
|
7274
|
-
const homeDir2 =
|
|
7982
|
+
const homeDir2 = os10.homedir();
|
|
7275
7983
|
const mcpPath = path14.join(homeDir2, ".cursor", "mcp.json");
|
|
7276
7984
|
const mcpConfig = readJson(mcpPath) ?? {};
|
|
7277
7985
|
const servers = mcpConfig.mcpServers ?? {};
|
|
@@ -7325,14 +8033,60 @@ async function setupCursor() {
|
|
|
7325
8033
|
printDaemonTip();
|
|
7326
8034
|
}
|
|
7327
8035
|
}
|
|
8036
|
+
function setupHud() {
|
|
8037
|
+
const homeDir2 = os10.homedir();
|
|
8038
|
+
const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
|
|
8039
|
+
const settings = readJson(hooksPath) ?? {};
|
|
8040
|
+
const hudCommand = fullPathCommand("hud");
|
|
8041
|
+
const statusLineObj = { type: "command", command: hudCommand };
|
|
8042
|
+
const existing = settings.statusLine;
|
|
8043
|
+
const existingCommand = typeof existing === "object" ? existing?.command : existing;
|
|
8044
|
+
if (existingCommand === hudCommand) {
|
|
8045
|
+
console.log(chalk.blue("\u2139\uFE0F node9 HUD is already configured in ~/.claude/settings.json"));
|
|
8046
|
+
console.log(chalk.gray(" Restart Claude Code to activate."));
|
|
8047
|
+
return;
|
|
8048
|
+
}
|
|
8049
|
+
if (existing && existingCommand !== hudCommand) {
|
|
8050
|
+
console.log(
|
|
8051
|
+
chalk.yellow(
|
|
8052
|
+
` \u26A0\uFE0F statusLine is already set to: "${existingCommand}"
|
|
8053
|
+
Overwriting with node9 HUD.`
|
|
8054
|
+
)
|
|
8055
|
+
);
|
|
8056
|
+
}
|
|
8057
|
+
settings.statusLine = statusLineObj;
|
|
8058
|
+
writeJson(hooksPath, settings);
|
|
8059
|
+
console.log(chalk.green.bold("\u2705 node9 HUD added to Claude Code statusline"));
|
|
8060
|
+
console.log(chalk.gray(" Settings: ~/.claude/settings.json"));
|
|
8061
|
+
console.log(chalk.gray(" Restart Claude Code to activate."));
|
|
8062
|
+
}
|
|
8063
|
+
function teardownHud() {
|
|
8064
|
+
const homeDir2 = os10.homedir();
|
|
8065
|
+
const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
|
|
8066
|
+
const settings = readJson(hooksPath);
|
|
8067
|
+
if (!settings) {
|
|
8068
|
+
console.log(chalk.blue(" \u2139\uFE0F ~/.claude/settings.json not found \u2014 nothing to remove"));
|
|
8069
|
+
return;
|
|
8070
|
+
}
|
|
8071
|
+
const existing = settings.statusLine;
|
|
8072
|
+
const existingCommand = typeof existing === "object" ? existing?.command : existing;
|
|
8073
|
+
if (!existingCommand || !String(existingCommand).includes("node9")) {
|
|
8074
|
+
console.log(chalk.blue(" \u2139\uFE0F node9 HUD not found in ~/.claude/settings.json"));
|
|
8075
|
+
return;
|
|
8076
|
+
}
|
|
8077
|
+
delete settings.statusLine;
|
|
8078
|
+
writeJson(hooksPath, settings);
|
|
8079
|
+
console.log(chalk.green(" \u2705 node9 HUD removed from ~/.claude/settings.json"));
|
|
8080
|
+
console.log(chalk.gray(" Restart Claude Code for changes to take effect."));
|
|
8081
|
+
}
|
|
7328
8082
|
|
|
7329
8083
|
// src/cli.ts
|
|
7330
8084
|
init_daemon2();
|
|
7331
|
-
import
|
|
7332
|
-
import
|
|
7333
|
-
import
|
|
8085
|
+
import chalk18 from "chalk";
|
|
8086
|
+
import fs26 from "fs";
|
|
8087
|
+
import path29 from "path";
|
|
7334
8088
|
import os22 from "os";
|
|
7335
|
-
import { confirm as
|
|
8089
|
+
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
7336
8090
|
|
|
7337
8091
|
// src/utils/duration.ts
|
|
7338
8092
|
function parseDuration(str) {
|
|
@@ -7362,7 +8116,7 @@ import { execa } from "execa";
|
|
|
7362
8116
|
import { parseCommandString } from "execa";
|
|
7363
8117
|
|
|
7364
8118
|
// src/policy/negotiation.ts
|
|
7365
|
-
function buildNegotiationMessage(blockedByLabel, isHumanDecision, humanReason) {
|
|
8119
|
+
function buildNegotiationMessage(blockedByLabel, isHumanDecision, humanReason, recoveryCommand) {
|
|
7366
8120
|
if (isHumanDecision) {
|
|
7367
8121
|
return `NODE9: The human user rejected this action.
|
|
7368
8122
|
REASON: ${humanReason || "No specific reason provided."}
|
|
@@ -7418,10 +8172,11 @@ INSTRUCTION: Inform the user this action is pending approval. Wait for them to a
|
|
|
7418
8172
|
INSTRUCTION: Do NOT use "${rule}". Find a read-only or non-destructive alternative.
|
|
7419
8173
|
Do NOT attempt to bypass this rule.`;
|
|
7420
8174
|
}
|
|
8175
|
+
const recovery = recoveryCommand ? `
|
|
8176
|
+
REQUIRED ACTION: Run \`${recoveryCommand}\` first, then retry your original command.` : "\n- Pivot to a non-destructive or read-only alternative.";
|
|
7421
8177
|
return `NODE9: Action blocked by security policy [${blockedByLabel}].
|
|
7422
8178
|
INSTRUCTIONS:
|
|
7423
|
-
- Do NOT retry this exact command or attempt to bypass the rule
|
|
7424
|
-
- Pivot to a non-destructive or read-only alternative.
|
|
8179
|
+
- Do NOT retry this exact command or attempt to bypass the rule.${recovery}
|
|
7425
8180
|
- Inform the user which security rule was triggered and ask how to proceed.`;
|
|
7426
8181
|
}
|
|
7427
8182
|
|
|
@@ -7522,6 +8277,7 @@ function openBrowserLocal() {
|
|
|
7522
8277
|
}
|
|
7523
8278
|
}
|
|
7524
8279
|
async function autoStartDaemonAndWait() {
|
|
8280
|
+
if (process.env.NODE9_TESTING === "1") return false;
|
|
7525
8281
|
try {
|
|
7526
8282
|
const child = spawn4(process.execPath, [process.argv[1], "daemon"], {
|
|
7527
8283
|
detached: true,
|
|
@@ -7558,16 +8314,16 @@ init_policy();
|
|
|
7558
8314
|
import chalk5 from "chalk";
|
|
7559
8315
|
import fs18 from "fs";
|
|
7560
8316
|
import path20 from "path";
|
|
7561
|
-
import
|
|
8317
|
+
import os14 from "os";
|
|
7562
8318
|
|
|
7563
8319
|
// src/undo.ts
|
|
7564
8320
|
import { spawnSync as spawnSync4, spawn as spawn5 } from "child_process";
|
|
7565
8321
|
import crypto2 from "crypto";
|
|
7566
8322
|
import fs17 from "fs";
|
|
7567
8323
|
import path19 from "path";
|
|
7568
|
-
import
|
|
7569
|
-
var SNAPSHOT_STACK_PATH = path19.join(
|
|
7570
|
-
var UNDO_LATEST_PATH = path19.join(
|
|
8324
|
+
import os13 from "os";
|
|
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");
|
|
7571
8327
|
var MAX_SNAPSHOTS = 10;
|
|
7572
8328
|
var GIT_TIMEOUT = 15e3;
|
|
7573
8329
|
function readStack() {
|
|
@@ -7583,16 +8339,33 @@ function writeStack(stack) {
|
|
|
7583
8339
|
if (!fs17.existsSync(dir)) fs17.mkdirSync(dir, { recursive: true });
|
|
7584
8340
|
fs17.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
7585
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
|
+
}
|
|
7586
8348
|
function buildArgsSummary(tool, args) {
|
|
8349
|
+
const filePath = extractFilePath(args);
|
|
8350
|
+
if (filePath) return filePath;
|
|
7587
8351
|
if (!args || typeof args !== "object") return "";
|
|
7588
8352
|
const a = args;
|
|
7589
|
-
const filePath = a.file_path ?? a.path ?? a.filename;
|
|
7590
|
-
if (typeof filePath === "string") return filePath;
|
|
7591
8353
|
const cmd = a.command ?? a.cmd;
|
|
7592
8354
|
if (typeof cmd === "string") return cmd.slice(0, 80);
|
|
7593
8355
|
const sql = a.sql ?? a.query;
|
|
7594
8356
|
if (typeof sql === "string") return sql.slice(0, 80);
|
|
7595
|
-
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
|
+
}
|
|
7596
8369
|
}
|
|
7597
8370
|
function normalizeCwdForHash(cwd) {
|
|
7598
8371
|
let normalized;
|
|
@@ -7607,7 +8380,7 @@ function normalizeCwdForHash(cwd) {
|
|
|
7607
8380
|
}
|
|
7608
8381
|
function getShadowRepoDir(cwd) {
|
|
7609
8382
|
const hash = crypto2.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
|
|
7610
|
-
return path19.join(
|
|
8383
|
+
return path19.join(os13.homedir(), ".node9", "snapshots", hash);
|
|
7611
8384
|
}
|
|
7612
8385
|
function cleanOrphanedIndexFiles(shadowDir) {
|
|
7613
8386
|
try {
|
|
@@ -7663,9 +8436,9 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
7663
8436
|
} catch {
|
|
7664
8437
|
}
|
|
7665
8438
|
const init = spawnSync4("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
|
|
7666
|
-
if (init.status !== 0) {
|
|
7667
|
-
|
|
7668
|
-
|
|
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);
|
|
7669
8442
|
return false;
|
|
7670
8443
|
}
|
|
7671
8444
|
const configFile = path19.join(shadowDir, "config");
|
|
@@ -7695,7 +8468,9 @@ function buildGitEnv(cwd) {
|
|
|
7695
8468
|
async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = []) {
|
|
7696
8469
|
let indexFile = null;
|
|
7697
8470
|
try {
|
|
7698
|
-
const
|
|
8471
|
+
const rawFilePath = extractFilePath(args);
|
|
8472
|
+
const absFilePath = rawFilePath && path19.isAbsolute(rawFilePath) ? rawFilePath : null;
|
|
8473
|
+
const cwd = absFilePath ? findProjectRoot(absFilePath) : process.cwd();
|
|
7699
8474
|
const shadowDir = getShadowRepoDir(cwd);
|
|
7700
8475
|
if (!ensureShadowRepo(shadowDir, cwd)) return null;
|
|
7701
8476
|
writeShadowExcludes(shadowDir, ignorePaths);
|
|
@@ -7718,15 +8493,53 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
7718
8493
|
const commitHash = commitRes.stdout?.toString().trim();
|
|
7719
8494
|
if (!commitHash || commitRes.status !== 0) return null;
|
|
7720
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
|
+
}
|
|
7721
8524
|
stack.push({
|
|
7722
8525
|
hash: commitHash,
|
|
7723
8526
|
tool,
|
|
7724
8527
|
argsSummary: buildArgsSummary(tool, args),
|
|
8528
|
+
files: capturedFiles,
|
|
8529
|
+
diff: capturedDiff,
|
|
7725
8530
|
cwd,
|
|
7726
8531
|
timestamp: Date.now()
|
|
7727
8532
|
});
|
|
7728
8533
|
const shouldGc = stack.length % 5 === 0;
|
|
7729
|
-
|
|
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);
|
|
7730
8543
|
writeStack(stack);
|
|
7731
8544
|
fs17.writeFileSync(UNDO_LATEST_PATH, commitHash);
|
|
7732
8545
|
if (shouldGc) {
|
|
@@ -7782,14 +8595,21 @@ function applyUndo(hash, cwd) {
|
|
|
7782
8595
|
env,
|
|
7783
8596
|
timeout: GIT_TIMEOUT
|
|
7784
8597
|
});
|
|
7785
|
-
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
|
+
}
|
|
7786
8605
|
const lsTree = spawnSync4("git", ["ls-tree", "-r", "--name-only", hash], {
|
|
7787
8606
|
cwd: dir,
|
|
7788
8607
|
env,
|
|
7789
8608
|
timeout: GIT_TIMEOUT
|
|
7790
8609
|
});
|
|
7791
8610
|
if (lsTree.status !== 0) {
|
|
7792
|
-
|
|
8611
|
+
const errorMsg = lsTree.stderr?.toString() || "Unknown git error";
|
|
8612
|
+
process.stderr.write(`[Node9] applyUndo: git ls-tree failed for hash ${hash}: ${errorMsg}
|
|
7793
8613
|
`);
|
|
7794
8614
|
return false;
|
|
7795
8615
|
}
|
|
@@ -7834,7 +8654,7 @@ function registerCheckCommand(program2) {
|
|
|
7834
8654
|
} catch (err) {
|
|
7835
8655
|
const tempConfig = getConfig();
|
|
7836
8656
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
7837
|
-
const logPath = path20.join(
|
|
8657
|
+
const logPath = path20.join(os14.homedir(), ".node9", "hook-debug.log");
|
|
7838
8658
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
7839
8659
|
fs18.appendFileSync(
|
|
7840
8660
|
logPath,
|
|
@@ -7847,7 +8667,7 @@ RAW: ${raw}
|
|
|
7847
8667
|
}
|
|
7848
8668
|
const config = getConfig(payload.cwd || void 0);
|
|
7849
8669
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
7850
|
-
const logPath = path20.join(
|
|
8670
|
+
const logPath = path20.join(os14.homedir(), ".node9", "hook-debug.log");
|
|
7851
8671
|
if (!fs18.existsSync(path20.dirname(logPath)))
|
|
7852
8672
|
fs18.mkdirSync(path20.dirname(logPath), { recursive: true });
|
|
7853
8673
|
fs18.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
@@ -7875,6 +8695,8 @@ RAW: ${raw}
|
|
|
7875
8695
|
}
|
|
7876
8696
|
writeTty(chalk5.gray(` Triggered by: ${blockedByContext}`));
|
|
7877
8697
|
if (result2?.changeHint) writeTty(chalk5.cyan(` To change: ${result2.changeHint}`));
|
|
8698
|
+
if (result2?.recoveryCommand)
|
|
8699
|
+
writeTty(chalk5.green(` \u{1F4A1} Run: ${result2.recoveryCommand}`));
|
|
7878
8700
|
writeTty("");
|
|
7879
8701
|
} catch {
|
|
7880
8702
|
} finally {
|
|
@@ -7887,7 +8709,8 @@ RAW: ${raw}
|
|
|
7887
8709
|
const aiFeedbackMessage = buildNegotiationMessage(
|
|
7888
8710
|
blockedByContext,
|
|
7889
8711
|
isHumanDecision,
|
|
7890
|
-
msg
|
|
8712
|
+
msg,
|
|
8713
|
+
result2?.recoveryCommand
|
|
7891
8714
|
);
|
|
7892
8715
|
process.stdout.write(
|
|
7893
8716
|
JSON.stringify({
|
|
@@ -7955,7 +8778,7 @@ RAW: ${raw}
|
|
|
7955
8778
|
});
|
|
7956
8779
|
} catch (err) {
|
|
7957
8780
|
if (process.env.NODE9_DEBUG === "1") {
|
|
7958
|
-
const logPath = path20.join(
|
|
8781
|
+
const logPath = path20.join(os14.homedir(), ".node9", "hook-debug.log");
|
|
7959
8782
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
7960
8783
|
fs18.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
7961
8784
|
`);
|
|
@@ -7996,7 +8819,7 @@ init_config();
|
|
|
7996
8819
|
init_policy();
|
|
7997
8820
|
import fs19 from "fs";
|
|
7998
8821
|
import path21 from "path";
|
|
7999
|
-
import
|
|
8822
|
+
import os15 from "os";
|
|
8000
8823
|
init_daemon();
|
|
8001
8824
|
|
|
8002
8825
|
// src/utils/cp-mv-parser.ts
|
|
@@ -8037,6 +8860,20 @@ function containsShellMetachar(token) {
|
|
|
8037
8860
|
}
|
|
8038
8861
|
|
|
8039
8862
|
// src/cli/commands/log.ts
|
|
8863
|
+
var TEST_COMMAND_RE = /(?:^|\s)(npm\s+(?:run\s+)?test|npx\s+(?:vitest|jest|mocha)|yarn\s+(?:run\s+)?test|pnpm\s+(?:run\s+)?test|vitest|jest|mocha|pytest|py\.test|cargo\s+test|go\s+test|bundle\s+exec\s+rspec|rspec|phpunit|dotnet\s+test)\b/i;
|
|
8864
|
+
function detectTestResult(command, output) {
|
|
8865
|
+
if (!TEST_COMMAND_RE.test(command)) return null;
|
|
8866
|
+
const out = output.toLowerCase();
|
|
8867
|
+
if (/\b(tests?\s+passed|all\s+tests?\s+passed|passing|test\s+suites?.*passed|ok\b|\d+\s+passed)/i.test(
|
|
8868
|
+
out
|
|
8869
|
+
) && !/\b(fail|error|failed)\b/.test(out)) {
|
|
8870
|
+
return "pass";
|
|
8871
|
+
}
|
|
8872
|
+
if (/\b(tests?\s+failed|failing|failed|error|assertion\s+error|\d+\s+failed)\b/i.test(out)) {
|
|
8873
|
+
return "fail";
|
|
8874
|
+
}
|
|
8875
|
+
return null;
|
|
8876
|
+
}
|
|
8040
8877
|
function sanitize3(value) {
|
|
8041
8878
|
return value.replace(/[\x00-\x1F\x7F]/g, "");
|
|
8042
8879
|
}
|
|
@@ -8055,7 +8892,7 @@ function registerLogCommand(program2) {
|
|
|
8055
8892
|
decision: "allowed",
|
|
8056
8893
|
source: "post-hook"
|
|
8057
8894
|
};
|
|
8058
|
-
const logPath = path21.join(
|
|
8895
|
+
const logPath = path21.join(os15.homedir(), ".node9", "audit.log");
|
|
8059
8896
|
if (!fs19.existsSync(path21.dirname(logPath)))
|
|
8060
8897
|
fs19.mkdirSync(path21.dirname(logPath), { recursive: true });
|
|
8061
8898
|
fs19.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
@@ -8068,6 +8905,21 @@ function registerLogCommand(program2) {
|
|
|
8068
8905
|
}
|
|
8069
8906
|
}
|
|
8070
8907
|
}
|
|
8908
|
+
if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
|
|
8909
|
+
const bashCommand = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
|
|
8910
|
+
const output = payload.tool_response?.output ?? "";
|
|
8911
|
+
if (bashCommand && output) {
|
|
8912
|
+
const testResult = detectTestResult(bashCommand, output);
|
|
8913
|
+
if (testResult) {
|
|
8914
|
+
await notifyActivitySocket({
|
|
8915
|
+
id: "test-result",
|
|
8916
|
+
ts: Date.now(),
|
|
8917
|
+
tool,
|
|
8918
|
+
status: testResult === "pass" ? "test_pass" : "test_fail"
|
|
8919
|
+
});
|
|
8920
|
+
}
|
|
8921
|
+
}
|
|
8922
|
+
}
|
|
8071
8923
|
const safeCwd = typeof payload.cwd === "string" && path21.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
8072
8924
|
const config = getConfig(safeCwd);
|
|
8073
8925
|
if (shouldSnapshot(tool, {}, config)) {
|
|
@@ -8077,7 +8929,7 @@ function registerLogCommand(program2) {
|
|
|
8077
8929
|
const msg = err instanceof Error ? err.message : String(err);
|
|
8078
8930
|
process.stderr.write(`[Node9] audit log error: ${msg}
|
|
8079
8931
|
`);
|
|
8080
|
-
const debugPath = path21.join(
|
|
8932
|
+
const debugPath = path21.join(os15.homedir(), ".node9", "hook-debug.log");
|
|
8081
8933
|
try {
|
|
8082
8934
|
fs19.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
|
|
8083
8935
|
`);
|
|
@@ -8386,11 +9238,11 @@ init_daemon();
|
|
|
8386
9238
|
import chalk7 from "chalk";
|
|
8387
9239
|
import fs20 from "fs";
|
|
8388
9240
|
import path22 from "path";
|
|
8389
|
-
import
|
|
9241
|
+
import os16 from "os";
|
|
8390
9242
|
import { execSync as execSync2 } from "child_process";
|
|
8391
9243
|
function registerDoctorCommand(program2, version2) {
|
|
8392
9244
|
program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
8393
|
-
const homeDir2 =
|
|
9245
|
+
const homeDir2 = os16.homedir();
|
|
8394
9246
|
let failures = 0;
|
|
8395
9247
|
function pass(msg) {
|
|
8396
9248
|
console.log(chalk7.green(" \u2705 ") + msg);
|
|
@@ -8553,7 +9405,7 @@ function registerDoctorCommand(program2, version2) {
|
|
|
8553
9405
|
import chalk8 from "chalk";
|
|
8554
9406
|
import fs21 from "fs";
|
|
8555
9407
|
import path23 from "path";
|
|
8556
|
-
import
|
|
9408
|
+
import os17 from "os";
|
|
8557
9409
|
function formatRelativeTime(timestamp) {
|
|
8558
9410
|
const diff = Date.now() - new Date(timestamp).getTime();
|
|
8559
9411
|
const sec = Math.floor(diff / 1e3);
|
|
@@ -8566,7 +9418,7 @@ function formatRelativeTime(timestamp) {
|
|
|
8566
9418
|
}
|
|
8567
9419
|
function registerAuditCommand(program2) {
|
|
8568
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) => {
|
|
8569
|
-
const logPath = path23.join(
|
|
9421
|
+
const logPath = path23.join(os17.homedir(), ".node9", "audit.log");
|
|
8570
9422
|
if (!fs21.existsSync(logPath)) {
|
|
8571
9423
|
console.log(
|
|
8572
9424
|
chalk8.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
@@ -8695,7 +9547,7 @@ init_daemon();
|
|
|
8695
9547
|
import chalk10 from "chalk";
|
|
8696
9548
|
import fs22 from "fs";
|
|
8697
9549
|
import path24 from "path";
|
|
8698
|
-
import
|
|
9550
|
+
import os18 from "os";
|
|
8699
9551
|
function readJson2(filePath) {
|
|
8700
9552
|
try {
|
|
8701
9553
|
if (fs22.existsSync(filePath)) return JSON.parse(fs22.readFileSync(filePath, "utf-8"));
|
|
@@ -8764,7 +9616,7 @@ function registerStatusCommand(program2) {
|
|
|
8764
9616
|
const modeLabel = settings.mode === "audit" ? chalk10.blue("audit") : settings.mode === "strict" ? chalk10.red("strict") : chalk10.white("standard");
|
|
8765
9617
|
console.log(` Mode: ${modeLabel}`);
|
|
8766
9618
|
const projectConfig = path24.join(process.cwd(), "node9.config.json");
|
|
8767
|
-
const globalConfig = path24.join(
|
|
9619
|
+
const globalConfig = path24.join(os18.homedir(), ".node9", "config.json");
|
|
8768
9620
|
console.log(
|
|
8769
9621
|
` Local: ${fs22.existsSync(projectConfig) ? chalk10.green("Active (node9.config.json)") : chalk10.gray("Not present")}`
|
|
8770
9622
|
);
|
|
@@ -8776,7 +9628,7 @@ function registerStatusCommand(program2) {
|
|
|
8776
9628
|
` Sandbox: ${chalk10.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
8777
9629
|
);
|
|
8778
9630
|
}
|
|
8779
|
-
const homeDir2 =
|
|
9631
|
+
const homeDir2 = os18.homedir();
|
|
8780
9632
|
const claudeSettings = readJson2(
|
|
8781
9633
|
path24.join(homeDir2, ".claude", "settings.json")
|
|
8782
9634
|
);
|
|
@@ -8846,11 +9698,11 @@ init_core();
|
|
|
8846
9698
|
import chalk11 from "chalk";
|
|
8847
9699
|
import fs23 from "fs";
|
|
8848
9700
|
import path25 from "path";
|
|
8849
|
-
import
|
|
9701
|
+
import os19 from "os";
|
|
8850
9702
|
function registerInitCommand(program2) {
|
|
8851
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) => {
|
|
8852
9704
|
console.log(chalk11.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
|
|
8853
|
-
const configPath = path25.join(
|
|
9705
|
+
const configPath = path25.join(os19.homedir(), ".node9", "config.json");
|
|
8854
9706
|
if (fs23.existsSync(configPath) && !options.force) {
|
|
8855
9707
|
console.log(chalk11.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
8856
9708
|
} else {
|
|
@@ -8891,107 +9743,306 @@ function registerInitCommand(program2) {
|
|
|
8891
9743
|
else if (agent === "cursor") await setupCursor();
|
|
8892
9744
|
console.log("");
|
|
8893
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
|
+
}
|
|
8894
9752
|
console.log(chalk11.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
|
|
8895
9753
|
console.log(chalk11.gray(" Run: node9 daemon start"));
|
|
8896
9754
|
});
|
|
8897
9755
|
}
|
|
8898
9756
|
|
|
8899
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";
|
|
8900
9763
|
import chalk12 from "chalk";
|
|
8901
|
-
|
|
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
|
+
}
|
|
8902
9929
|
function registerUndoCommand(program2) {
|
|
8903
9930
|
program2.command("undo").description(
|
|
8904
|
-
"
|
|
8905
|
-
).option("--steps <n>", "
|
|
8906
|
-
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) => {
|
|
8907
9933
|
const allHistory = getSnapshotHistory();
|
|
8908
|
-
const
|
|
9934
|
+
const matchedCwd = options.all ? null : findMatchingCwd(process.cwd(), allHistory);
|
|
9935
|
+
const history = options.all ? allHistory : allHistory.filter((s) => s.cwd === matchedCwd);
|
|
8909
9936
|
if (history.length === 0) {
|
|
8910
9937
|
if (!options.all && allHistory.length > 0) {
|
|
8911
9938
|
console.log(
|
|
8912
|
-
|
|
9939
|
+
chalk13.yellow(
|
|
8913
9940
|
`
|
|
8914
9941
|
\u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
|
|
8915
|
-
Run ${
|
|
9942
|
+
Run ${chalk13.cyan("node9 undo --all")} to see snapshots from all projects.
|
|
8916
9943
|
`
|
|
8917
9944
|
)
|
|
8918
9945
|
);
|
|
8919
9946
|
} else {
|
|
8920
|
-
console.log(
|
|
9947
|
+
console.log(chalk13.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
|
|
8921
9948
|
}
|
|
8922
9949
|
return;
|
|
8923
9950
|
}
|
|
8924
|
-
|
|
8925
|
-
|
|
9951
|
+
if (options.list) {
|
|
9952
|
+
console.log(chalk13.magenta.bold("\n\u23EA Snapshot History\n"));
|
|
8926
9953
|
console.log(
|
|
8927
|
-
|
|
8928
|
-
`
|
|
8929
|
-
\u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
|
|
8930
|
-
`
|
|
9954
|
+
chalk13.gray(
|
|
9955
|
+
` ${"#".padEnd(3)} ${"File / Command".padEnd(30)} ${"Tool".padEnd(8)} ${"When".padEnd(10)} Dir`
|
|
8931
9956
|
)
|
|
8932
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("");
|
|
8933
9977
|
return;
|
|
8934
9978
|
}
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
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(`
|
|
8940
9996
|
\u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`)
|
|
8941
|
-
|
|
8942
|
-
console.log(
|
|
8943
|
-
chalk12.white(
|
|
8944
|
-
` Tool: ${chalk12.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk12.gray(" \u2192 " + snapshot.argsSummary) : ""}`
|
|
8945
|
-
)
|
|
8946
|
-
);
|
|
8947
|
-
console.log(chalk12.white(` When: ${chalk12.gray(ageStr)}`));
|
|
8948
|
-
console.log(chalk12.white(` Dir: ${chalk12.gray(snapshot.cwd)}`));
|
|
8949
|
-
if (steps > 1)
|
|
9997
|
+
);
|
|
8950
9998
|
console.log(
|
|
8951
|
-
|
|
9999
|
+
chalk13.white(
|
|
10000
|
+
` Tool: ${chalk13.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk13.gray(" \u2192 " + snapshot.argsSummary) : ""}`
|
|
10001
|
+
)
|
|
8952
10002
|
);
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
|
|
8965
|
-
console.log(
|
|
8966
|
-
|
|
8967
|
-
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));
|
|
8968
10019
|
}
|
|
10020
|
+
console.log("");
|
|
10021
|
+
} else {
|
|
10022
|
+
console.log(
|
|
10023
|
+
chalk13.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
|
|
10024
|
+
);
|
|
8969
10025
|
}
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
default: false
|
|
8979
|
-
});
|
|
8980
|
-
if (proceed) {
|
|
8981
|
-
if (applyUndo(snapshot.hash, snapshot.cwd)) {
|
|
8982
|
-
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
|
+
}
|
|
8983
10034
|
} else {
|
|
8984
|
-
console.
|
|
10035
|
+
console.log(chalk13.gray("\nCancelled.\n"));
|
|
8985
10036
|
}
|
|
8986
|
-
|
|
8987
|
-
console.log(chalk12.gray("\nCancelled.\n"));
|
|
10037
|
+
return;
|
|
8988
10038
|
}
|
|
10039
|
+
await runUndoNavigator(history);
|
|
8989
10040
|
});
|
|
8990
10041
|
}
|
|
8991
10042
|
|
|
8992
10043
|
// src/cli/commands/watch.ts
|
|
8993
10044
|
init_daemon();
|
|
8994
|
-
import
|
|
10045
|
+
import chalk14 from "chalk";
|
|
8995
10046
|
import { spawn as spawn7, spawnSync as spawnSync5 } from "child_process";
|
|
8996
10047
|
function registerWatchCommand(program2) {
|
|
8997
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) => {
|
|
@@ -9007,7 +10058,7 @@ function registerWatchCommand(program2) {
|
|
|
9007
10058
|
throw new Error("not running");
|
|
9008
10059
|
}
|
|
9009
10060
|
} catch {
|
|
9010
|
-
console.error(
|
|
10061
|
+
console.error(chalk14.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
|
|
9011
10062
|
const child = spawn7(process.execPath, [process.argv[1], "daemon"], {
|
|
9012
10063
|
detached: true,
|
|
9013
10064
|
stdio: "ignore",
|
|
@@ -9029,12 +10080,12 @@ function registerWatchCommand(program2) {
|
|
|
9029
10080
|
}
|
|
9030
10081
|
}
|
|
9031
10082
|
if (!ready) {
|
|
9032
|
-
console.error(
|
|
10083
|
+
console.error(chalk14.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
9033
10084
|
process.exit(1);
|
|
9034
10085
|
}
|
|
9035
10086
|
}
|
|
9036
10087
|
console.error(
|
|
9037
|
-
|
|
10088
|
+
chalk14.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + chalk14.dim(` \u2192 localhost:${port}`) + chalk14.dim(
|
|
9038
10089
|
"\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
|
|
9039
10090
|
)
|
|
9040
10091
|
);
|
|
@@ -9043,7 +10094,7 @@ function registerWatchCommand(program2) {
|
|
|
9043
10094
|
env: { ...process.env, NODE9_WATCH_MODE: "1" }
|
|
9044
10095
|
});
|
|
9045
10096
|
if (result.error) {
|
|
9046
|
-
console.error(
|
|
10097
|
+
console.error(chalk14.red(`\u274C Failed to run command: ${result.error.message}`));
|
|
9047
10098
|
process.exit(1);
|
|
9048
10099
|
}
|
|
9049
10100
|
process.exit(result.status ?? 0);
|
|
@@ -9052,8 +10103,8 @@ function registerWatchCommand(program2) {
|
|
|
9052
10103
|
|
|
9053
10104
|
// src/mcp-gateway/index.ts
|
|
9054
10105
|
init_orchestrator();
|
|
9055
|
-
import
|
|
9056
|
-
import
|
|
10106
|
+
import readline3 from "readline";
|
|
10107
|
+
import chalk15 from "chalk";
|
|
9057
10108
|
import { spawn as spawn8 } from "child_process";
|
|
9058
10109
|
import { execa as execa2 } from "execa";
|
|
9059
10110
|
init_provenance();
|
|
@@ -9116,13 +10167,13 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
9116
10167
|
const prov = checkProvenance(executable);
|
|
9117
10168
|
if (prov.trustLevel === "suspect") {
|
|
9118
10169
|
console.error(
|
|
9119
|
-
|
|
10170
|
+
chalk15.red(
|
|
9120
10171
|
`\u26A0\uFE0F Node9: Upstream MCP server binary is suspect \u2014 ${prov.reason} (${prov.resolvedPath})`
|
|
9121
10172
|
)
|
|
9122
10173
|
);
|
|
9123
|
-
console.error(
|
|
10174
|
+
console.error(chalk15.red(" Verify this binary is trusted before proceeding."));
|
|
9124
10175
|
}
|
|
9125
|
-
console.error(
|
|
10176
|
+
console.error(chalk15.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
|
|
9126
10177
|
const UPSTREAM_INJECTOR_VARS = /* @__PURE__ */ new Set([
|
|
9127
10178
|
"NODE_OPTIONS",
|
|
9128
10179
|
"NODE_PATH",
|
|
@@ -9150,7 +10201,7 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
9150
10201
|
let authPending = false;
|
|
9151
10202
|
let deferredExitCode = null;
|
|
9152
10203
|
let deferredStdinEnd = false;
|
|
9153
|
-
const agentIn =
|
|
10204
|
+
const agentIn = readline3.createInterface({ input: process.stdin, terminal: false });
|
|
9154
10205
|
agentIn.on("line", async (line) => {
|
|
9155
10206
|
let message;
|
|
9156
10207
|
try {
|
|
@@ -9186,10 +10237,10 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
9186
10237
|
mcpServer
|
|
9187
10238
|
});
|
|
9188
10239
|
if (!result.approved) {
|
|
9189
|
-
console.error(
|
|
10240
|
+
console.error(chalk15.red(`
|
|
9190
10241
|
\u{1F6D1} Node9 MCP Gateway: Action Blocked`));
|
|
9191
|
-
console.error(
|
|
9192
|
-
console.error(
|
|
10242
|
+
console.error(chalk15.gray(` Tool: ${toolName}`));
|
|
10243
|
+
console.error(chalk15.gray(` Reason: ${result.reason ?? "Security Policy"}
|
|
9193
10244
|
`));
|
|
9194
10245
|
const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
|
|
9195
10246
|
const isHumanDecision = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
|
|
@@ -9263,7 +10314,7 @@ function registerMcpGatewayCommand(program2) {
|
|
|
9263
10314
|
|
|
9264
10315
|
// src/cli/commands/trust.ts
|
|
9265
10316
|
init_trusted_hosts();
|
|
9266
|
-
import
|
|
10317
|
+
import chalk16 from "chalk";
|
|
9267
10318
|
function isValidHost(host) {
|
|
9268
10319
|
return /^(\*\.)?[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/.test(host);
|
|
9269
10320
|
}
|
|
@@ -9273,44 +10324,44 @@ function registerTrustCommand(program2) {
|
|
|
9273
10324
|
const normalized = normalizeHost(host.trim());
|
|
9274
10325
|
if (!isValidHost(normalized)) {
|
|
9275
10326
|
console.error(
|
|
9276
|
-
|
|
10327
|
+
chalk16.red(`
|
|
9277
10328
|
\u274C Invalid host: "${host}"
|
|
9278
|
-
`) +
|
|
10329
|
+
`) + chalk16.gray(" Use an FQDN like api.mycompany.com or *.mycompany.com\n")
|
|
9279
10330
|
);
|
|
9280
10331
|
process.exit(1);
|
|
9281
10332
|
}
|
|
9282
10333
|
addTrustedHost(normalized);
|
|
9283
|
-
console.log(
|
|
10334
|
+
console.log(chalk16.green(`
|
|
9284
10335
|
\u2705 ${normalized} added to trusted hosts.`));
|
|
9285
10336
|
console.log(
|
|
9286
|
-
|
|
10337
|
+
chalk16.gray(" Pipe-chain blocks to this host: critical \u2192 review, high \u2192 allow\n")
|
|
9287
10338
|
);
|
|
9288
10339
|
});
|
|
9289
10340
|
trustCmd.command("remove <host>").description("Remove a trusted host").action((host) => {
|
|
9290
10341
|
const normalized = normalizeHost(host.trim());
|
|
9291
10342
|
const removed = removeTrustedHost(normalized);
|
|
9292
10343
|
if (!removed) {
|
|
9293
|
-
console.error(
|
|
10344
|
+
console.error(chalk16.yellow(`
|
|
9294
10345
|
\u26A0\uFE0F "${normalized}" is not in the trusted hosts list.
|
|
9295
10346
|
`));
|
|
9296
10347
|
process.exit(1);
|
|
9297
10348
|
}
|
|
9298
|
-
console.log(
|
|
10349
|
+
console.log(chalk16.green(`
|
|
9299
10350
|
\u2705 ${normalized} removed from trusted hosts.
|
|
9300
10351
|
`));
|
|
9301
10352
|
});
|
|
9302
10353
|
trustCmd.command("list").description("Show all trusted hosts").action(() => {
|
|
9303
10354
|
const hosts = readTrustedHosts();
|
|
9304
10355
|
if (hosts.length === 0) {
|
|
9305
|
-
console.log(
|
|
9306
|
-
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")}
|
|
9307
10358
|
`);
|
|
9308
10359
|
return;
|
|
9309
10360
|
}
|
|
9310
|
-
console.log(
|
|
10361
|
+
console.log(chalk16.bold("\n\u{1F513} Trusted Hosts\n"));
|
|
9311
10362
|
for (const entry of hosts) {
|
|
9312
10363
|
const date = new Date(entry.addedAt).toLocaleDateString();
|
|
9313
|
-
console.log(` ${
|
|
10364
|
+
console.log(` ${chalk16.cyan(entry.host.padEnd(40))} ${chalk16.gray(`added ${date}`)}`);
|
|
9314
10365
|
}
|
|
9315
10366
|
console.log("");
|
|
9316
10367
|
});
|
|
@@ -9318,20 +10369,20 @@ function registerTrustCommand(program2) {
|
|
|
9318
10369
|
|
|
9319
10370
|
// src/cli.ts
|
|
9320
10371
|
var { version } = JSON.parse(
|
|
9321
|
-
|
|
10372
|
+
fs26.readFileSync(path29.join(__dirname, "../package.json"), "utf-8")
|
|
9322
10373
|
);
|
|
9323
10374
|
var program = new Command();
|
|
9324
10375
|
program.name("node9").description("The Sudo Command for AI Agents").version(version);
|
|
9325
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) => {
|
|
9326
10377
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
9327
|
-
const credPath =
|
|
9328
|
-
if (!
|
|
9329
|
-
|
|
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 });
|
|
9330
10381
|
const profileName = options.profile || "default";
|
|
9331
10382
|
let existingCreds = {};
|
|
9332
10383
|
try {
|
|
9333
|
-
if (
|
|
9334
|
-
const raw = JSON.parse(
|
|
10384
|
+
if (fs26.existsSync(credPath)) {
|
|
10385
|
+
const raw = JSON.parse(fs26.readFileSync(credPath, "utf-8"));
|
|
9335
10386
|
if (raw.apiKey) {
|
|
9336
10387
|
existingCreds = {
|
|
9337
10388
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
|
|
@@ -9343,13 +10394,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
9343
10394
|
} catch {
|
|
9344
10395
|
}
|
|
9345
10396
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
9346
|
-
|
|
10397
|
+
fs26.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
9347
10398
|
if (profileName === "default") {
|
|
9348
|
-
const configPath =
|
|
10399
|
+
const configPath = path29.join(os22.homedir(), ".node9", "config.json");
|
|
9349
10400
|
let config = {};
|
|
9350
10401
|
try {
|
|
9351
|
-
if (
|
|
9352
|
-
config = JSON.parse(
|
|
10402
|
+
if (fs26.existsSync(configPath))
|
|
10403
|
+
config = JSON.parse(fs26.readFileSync(configPath, "utf-8"));
|
|
9353
10404
|
} catch {
|
|
9354
10405
|
}
|
|
9355
10406
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -9364,36 +10415,40 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
9364
10415
|
approvers.cloud = false;
|
|
9365
10416
|
}
|
|
9366
10417
|
s.approvers = approvers;
|
|
9367
|
-
if (!
|
|
9368
|
-
|
|
9369
|
-
|
|
10418
|
+
if (!fs26.existsSync(path29.dirname(configPath)))
|
|
10419
|
+
fs26.mkdirSync(path29.dirname(configPath), { recursive: true });
|
|
10420
|
+
fs26.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
9370
10421
|
}
|
|
9371
10422
|
if (options.profile && profileName !== "default") {
|
|
9372
|
-
console.log(
|
|
9373
|
-
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`));
|
|
9374
10425
|
} else if (options.local) {
|
|
9375
|
-
console.log(
|
|
9376
|
-
console.log(
|
|
10426
|
+
console.log(chalk18.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
10427
|
+
console.log(chalk18.gray(` All decisions stay on this machine.`));
|
|
9377
10428
|
} else {
|
|
9378
|
-
console.log(
|
|
9379
|
-
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.`));
|
|
9380
10431
|
}
|
|
9381
10432
|
});
|
|
9382
|
-
program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to protect: claude | gemini | cursor").action(async (target) => {
|
|
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) => {
|
|
9383
10434
|
if (target === "gemini") return await setupGemini();
|
|
9384
10435
|
if (target === "claude") return await setupClaude();
|
|
9385
10436
|
if (target === "cursor") return await setupCursor();
|
|
9386
|
-
|
|
10437
|
+
if (target === "hud") return setupHud();
|
|
10438
|
+
console.error(chalk18.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
|
|
9387
10439
|
process.exit(1);
|
|
9388
10440
|
});
|
|
9389
|
-
program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor").argument("[target]", "The agent to protect: claude | gemini | cursor").action(async (target) => {
|
|
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) => {
|
|
9390
10442
|
if (!target) {
|
|
9391
|
-
console.log(
|
|
9392
|
-
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");
|
|
9393
10445
|
console.log(" Targets:");
|
|
9394
|
-
console.log(" " +
|
|
9395
|
-
console.log(" " +
|
|
9396
|
-
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)");
|
|
10449
|
+
process.stdout.write(
|
|
10450
|
+
" " + chalk18.green("hud") + " \u2014 Claude Code security statusline\n"
|
|
10451
|
+
);
|
|
9397
10452
|
console.log("");
|
|
9398
10453
|
return;
|
|
9399
10454
|
}
|
|
@@ -9401,7 +10456,8 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
9401
10456
|
if (t === "gemini") return await setupGemini();
|
|
9402
10457
|
if (t === "claude") return await setupClaude();
|
|
9403
10458
|
if (t === "cursor") return await setupCursor();
|
|
9404
|
-
|
|
10459
|
+
if (t === "hud") return setupHud();
|
|
10460
|
+
console.error(chalk18.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
|
|
9405
10461
|
process.exit(1);
|
|
9406
10462
|
});
|
|
9407
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) => {
|
|
@@ -9409,31 +10465,34 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
|
|
|
9409
10465
|
if (target === "claude") fn = teardownClaude;
|
|
9410
10466
|
else if (target === "gemini") fn = teardownGemini;
|
|
9411
10467
|
else if (target === "cursor") fn = teardownCursor;
|
|
10468
|
+
else if (target === "hud") fn = teardownHud;
|
|
9412
10469
|
else {
|
|
9413
|
-
console.error(
|
|
10470
|
+
console.error(
|
|
10471
|
+
chalk18.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`)
|
|
10472
|
+
);
|
|
9414
10473
|
process.exit(1);
|
|
9415
10474
|
}
|
|
9416
|
-
console.log(
|
|
10475
|
+
console.log(chalk18.cyan(`
|
|
9417
10476
|
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
9418
10477
|
`));
|
|
9419
10478
|
try {
|
|
9420
10479
|
fn();
|
|
9421
10480
|
} catch (err) {
|
|
9422
|
-
console.error(
|
|
10481
|
+
console.error(chalk18.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
9423
10482
|
process.exit(1);
|
|
9424
10483
|
}
|
|
9425
|
-
console.log(
|
|
10484
|
+
console.log(chalk18.gray("\n Restart the agent for changes to take effect."));
|
|
9426
10485
|
});
|
|
9427
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) => {
|
|
9428
|
-
console.log(
|
|
9429
|
-
console.log(
|
|
10487
|
+
console.log(chalk18.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
10488
|
+
console.log(chalk18.bold("Stopping daemon..."));
|
|
9430
10489
|
try {
|
|
9431
10490
|
stopDaemon();
|
|
9432
|
-
console.log(
|
|
10491
|
+
console.log(chalk18.green(" \u2705 Daemon stopped"));
|
|
9433
10492
|
} catch {
|
|
9434
|
-
console.log(
|
|
10493
|
+
console.log(chalk18.blue(" \u2139\uFE0F Daemon was not running"));
|
|
9435
10494
|
}
|
|
9436
|
-
console.log(
|
|
10495
|
+
console.log(chalk18.bold("\nRemoving hooks..."));
|
|
9437
10496
|
let teardownFailed = false;
|
|
9438
10497
|
for (const [label, fn] of [
|
|
9439
10498
|
["Claude", teardownClaude],
|
|
@@ -9445,45 +10504,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
9445
10504
|
} catch (err) {
|
|
9446
10505
|
teardownFailed = true;
|
|
9447
10506
|
console.error(
|
|
9448
|
-
|
|
10507
|
+
chalk18.red(
|
|
9449
10508
|
` \u26A0\uFE0F Failed to remove ${label} hooks: ${err instanceof Error ? err.message : String(err)}`
|
|
9450
10509
|
)
|
|
9451
10510
|
);
|
|
9452
10511
|
}
|
|
9453
10512
|
}
|
|
9454
10513
|
if (options.purge) {
|
|
9455
|
-
const node9Dir =
|
|
9456
|
-
if (
|
|
9457
|
-
const confirmed = await
|
|
10514
|
+
const node9Dir = path29.join(os22.homedir(), ".node9");
|
|
10515
|
+
if (fs26.existsSync(node9Dir)) {
|
|
10516
|
+
const confirmed = await confirm2({
|
|
9458
10517
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
9459
10518
|
default: false
|
|
9460
10519
|
});
|
|
9461
10520
|
if (confirmed) {
|
|
9462
|
-
|
|
9463
|
-
if (
|
|
10521
|
+
fs26.rmSync(node9Dir, { recursive: true });
|
|
10522
|
+
if (fs26.existsSync(node9Dir)) {
|
|
9464
10523
|
console.error(
|
|
9465
|
-
|
|
10524
|
+
chalk18.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
9466
10525
|
);
|
|
9467
10526
|
} else {
|
|
9468
|
-
console.log(
|
|
10527
|
+
console.log(chalk18.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
9469
10528
|
}
|
|
9470
10529
|
} else {
|
|
9471
|
-
console.log(
|
|
10530
|
+
console.log(chalk18.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
9472
10531
|
}
|
|
9473
10532
|
} else {
|
|
9474
|
-
console.log(
|
|
10533
|
+
console.log(chalk18.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
9475
10534
|
}
|
|
9476
10535
|
} else {
|
|
9477
10536
|
console.log(
|
|
9478
|
-
|
|
10537
|
+
chalk18.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
9479
10538
|
);
|
|
9480
10539
|
}
|
|
9481
10540
|
if (teardownFailed) {
|
|
9482
|
-
console.error(
|
|
10541
|
+
console.error(chalk18.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
9483
10542
|
process.exit(1);
|
|
9484
10543
|
}
|
|
9485
|
-
console.log(
|
|
9486
|
-
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"));
|
|
9487
10546
|
});
|
|
9488
10547
|
registerDoctorCommand(program, version);
|
|
9489
10548
|
program.command("explain").description(
|
|
@@ -9496,7 +10555,7 @@ program.command("explain").description(
|
|
|
9496
10555
|
try {
|
|
9497
10556
|
args = JSON.parse(trimmed);
|
|
9498
10557
|
} catch {
|
|
9499
|
-
console.error(
|
|
10558
|
+
console.error(chalk18.red(`
|
|
9500
10559
|
\u274C Invalid JSON: ${trimmed}
|
|
9501
10560
|
`));
|
|
9502
10561
|
process.exit(1);
|
|
@@ -9507,54 +10566,54 @@ program.command("explain").description(
|
|
|
9507
10566
|
}
|
|
9508
10567
|
const result = await explainPolicy(tool, args);
|
|
9509
10568
|
console.log("");
|
|
9510
|
-
console.log(
|
|
10569
|
+
console.log(chalk18.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
9511
10570
|
console.log("");
|
|
9512
|
-
console.log(` ${
|
|
10571
|
+
console.log(` ${chalk18.bold("Tool:")} ${chalk18.white(result.tool)}`);
|
|
9513
10572
|
if (argsRaw) {
|
|
9514
10573
|
const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
9515
|
-
console.log(` ${
|
|
10574
|
+
console.log(` ${chalk18.bold("Input:")} ${chalk18.gray(preview)}`);
|
|
9516
10575
|
}
|
|
9517
10576
|
console.log("");
|
|
9518
|
-
console.log(
|
|
10577
|
+
console.log(chalk18.bold("Config Sources (Waterfall):"));
|
|
9519
10578
|
for (const tier of result.waterfall) {
|
|
9520
|
-
const num =
|
|
10579
|
+
const num = chalk18.gray(` ${tier.tier}.`);
|
|
9521
10580
|
const label = tier.label.padEnd(16);
|
|
9522
10581
|
let statusStr;
|
|
9523
10582
|
if (tier.tier === 1) {
|
|
9524
|
-
statusStr =
|
|
10583
|
+
statusStr = chalk18.gray(tier.note ?? "");
|
|
9525
10584
|
} else if (tier.status === "active") {
|
|
9526
|
-
const loc = tier.path ?
|
|
9527
|
-
const note = tier.note ?
|
|
9528
|
-
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 : "");
|
|
9529
10588
|
} else {
|
|
9530
|
-
statusStr =
|
|
10589
|
+
statusStr = chalk18.gray("\u25CB " + (tier.note ?? "not found"));
|
|
9531
10590
|
}
|
|
9532
|
-
console.log(`${num} ${
|
|
10591
|
+
console.log(`${num} ${chalk18.white(label)} ${statusStr}`);
|
|
9533
10592
|
}
|
|
9534
10593
|
console.log("");
|
|
9535
|
-
console.log(
|
|
10594
|
+
console.log(chalk18.bold("Policy Evaluation:"));
|
|
9536
10595
|
for (const step of result.steps) {
|
|
9537
10596
|
const isFinal = step.isFinal;
|
|
9538
10597
|
let icon;
|
|
9539
|
-
if (step.outcome === "allow") icon =
|
|
9540
|
-
else if (step.outcome === "review") icon =
|
|
9541
|
-
else if (step.outcome === "skip") icon =
|
|
9542
|
-
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 ");
|
|
9543
10602
|
const name = step.name.padEnd(18);
|
|
9544
|
-
const nameStr = isFinal ?
|
|
9545
|
-
const detail = isFinal ?
|
|
9546
|
-
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") : "";
|
|
9547
10606
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
9548
10607
|
}
|
|
9549
10608
|
console.log("");
|
|
9550
10609
|
if (result.decision === "allow") {
|
|
9551
|
-
console.log(
|
|
10610
|
+
console.log(chalk18.green.bold(" Decision: \u2705 ALLOW") + chalk18.gray(" \u2014 no approval needed"));
|
|
9552
10611
|
} else {
|
|
9553
10612
|
console.log(
|
|
9554
|
-
|
|
10613
|
+
chalk18.red.bold(" Decision: \u{1F534} REVIEW") + chalk18.gray(" \u2014 human approval required")
|
|
9555
10614
|
);
|
|
9556
10615
|
if (result.blockedByLabel) {
|
|
9557
|
-
console.log(
|
|
10616
|
+
console.log(chalk18.gray(` Reason: ${result.blockedByLabel}`));
|
|
9558
10617
|
}
|
|
9559
10618
|
}
|
|
9560
10619
|
console.log("");
|
|
@@ -9568,7 +10627,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
|
|
|
9568
10627
|
try {
|
|
9569
10628
|
await startTail2(options);
|
|
9570
10629
|
} catch (err) {
|
|
9571
|
-
console.error(
|
|
10630
|
+
console.error(chalk18.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
|
|
9572
10631
|
process.exit(1);
|
|
9573
10632
|
}
|
|
9574
10633
|
});
|
|
@@ -9576,11 +10635,15 @@ registerWatchCommand(program);
|
|
|
9576
10635
|
registerMcpGatewayCommand(program);
|
|
9577
10636
|
registerCheckCommand(program);
|
|
9578
10637
|
registerLogCommand(program);
|
|
10638
|
+
program.command("hud").description("Render node9 security statusline (spawned by Claude Code statusLine)").action(async () => {
|
|
10639
|
+
const { main: main2 } = await Promise.resolve().then(() => (init_hud(), hud_exports));
|
|
10640
|
+
await main2();
|
|
10641
|
+
});
|
|
9579
10642
|
program.command("pause").description("Temporarily disable Node9 protection for a set duration").option("-d, --duration <duration>", "How long to pause (e.g. 15m, 1h, 30s)", "15m").action((options) => {
|
|
9580
10643
|
const ms = parseDuration(options.duration);
|
|
9581
10644
|
if (ms === null) {
|
|
9582
10645
|
console.error(
|
|
9583
|
-
|
|
10646
|
+
chalk18.red(`
|
|
9584
10647
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
9585
10648
|
`)
|
|
9586
10649
|
);
|
|
@@ -9588,20 +10651,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
9588
10651
|
}
|
|
9589
10652
|
pauseNode9(ms, options.duration);
|
|
9590
10653
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
9591
|
-
console.log(
|
|
10654
|
+
console.log(chalk18.yellow(`
|
|
9592
10655
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
9593
|
-
console.log(
|
|
9594
|
-
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.
|
|
9595
10658
|
`));
|
|
9596
10659
|
});
|
|
9597
10660
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
9598
10661
|
const { paused } = checkPause();
|
|
9599
10662
|
if (!paused) {
|
|
9600
|
-
console.log(
|
|
10663
|
+
console.log(chalk18.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
9601
10664
|
return;
|
|
9602
10665
|
}
|
|
9603
10666
|
resumeNode9();
|
|
9604
|
-
console.log(
|
|
10667
|
+
console.log(chalk18.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
9605
10668
|
});
|
|
9606
10669
|
var HOOK_BASED_AGENTS = {
|
|
9607
10670
|
claude: "claude",
|
|
@@ -9614,15 +10677,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
9614
10677
|
if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
|
|
9615
10678
|
const target = HOOK_BASED_AGENTS[firstArg2];
|
|
9616
10679
|
console.error(
|
|
9617
|
-
|
|
10680
|
+
chalk18.yellow(`
|
|
9618
10681
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
9619
10682
|
);
|
|
9620
|
-
console.error(
|
|
10683
|
+
console.error(chalk18.white(`
|
|
9621
10684
|
"${target}" uses its own hook system. Use:`));
|
|
9622
10685
|
console.error(
|
|
9623
|
-
|
|
10686
|
+
chalk18.green(` node9 addto ${target} `) + chalk18.gray("# one-time setup")
|
|
9624
10687
|
);
|
|
9625
|
-
console.error(
|
|
10688
|
+
console.error(chalk18.green(` ${target} `) + chalk18.gray("# run normally"));
|
|
9626
10689
|
process.exit(1);
|
|
9627
10690
|
}
|
|
9628
10691
|
const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
|
|
@@ -9639,12 +10702,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
9639
10702
|
}
|
|
9640
10703
|
);
|
|
9641
10704
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
9642
|
-
console.error(
|
|
10705
|
+
console.error(chalk18.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
9643
10706
|
const daemonReady = await autoStartDaemonAndWait();
|
|
9644
10707
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
9645
10708
|
}
|
|
9646
10709
|
if (result.noApprovalMechanism && process.stdout.isTTY) {
|
|
9647
|
-
const approved = await
|
|
10710
|
+
const approved = await confirm2({
|
|
9648
10711
|
message: `\u{1F6E1}\uFE0F Node9: Allow "${fullCommand}"?`,
|
|
9649
10712
|
default: false
|
|
9650
10713
|
});
|
|
@@ -9652,12 +10715,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
9652
10715
|
}
|
|
9653
10716
|
if (!result.approved) {
|
|
9654
10717
|
console.error(
|
|
9655
|
-
|
|
10718
|
+
chalk18.red(`
|
|
9656
10719
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
9657
10720
|
);
|
|
9658
10721
|
process.exit(1);
|
|
9659
10722
|
}
|
|
9660
|
-
console.error(
|
|
10723
|
+
console.error(chalk18.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
9661
10724
|
await runProxy(fullCommand);
|
|
9662
10725
|
} else {
|
|
9663
10726
|
program.help();
|
|
@@ -9672,9 +10735,9 @@ if (process.argv[2] !== "daemon") {
|
|
|
9672
10735
|
const isCheckHook = process.argv[2] === "check";
|
|
9673
10736
|
if (isCheckHook) {
|
|
9674
10737
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
9675
|
-
const logPath =
|
|
10738
|
+
const logPath = path29.join(os22.homedir(), ".node9", "hook-debug.log");
|
|
9676
10739
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
9677
|
-
|
|
10740
|
+
fs26.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
9678
10741
|
`);
|
|
9679
10742
|
}
|
|
9680
10743
|
process.exit(0);
|