@node9/proxy 1.10.3 → 1.11.1
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 +57 -0
- package/dist/cli.js +1447 -474
- package/dist/cli.mjs +1443 -470
- package/dist/index.js +106 -38
- package/dist/index.mjs +106 -38
- package/dist/shields/builtin/bash-safe.json +2 -2
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -147,8 +147,8 @@ function sanitizeConfig(raw) {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
const lines = result.error.issues.map((issue) => {
|
|
150
|
-
const
|
|
151
|
-
return ` \u2022 ${
|
|
150
|
+
const path43 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
151
|
+
return ` \u2022 ${path43}: ${issue.message}`;
|
|
152
152
|
});
|
|
153
153
|
return {
|
|
154
154
|
sanitized,
|
|
@@ -254,6 +254,11 @@ var init_config_schema = __esm({
|
|
|
254
254
|
enabled: z.boolean().optional(),
|
|
255
255
|
threshold: z.number().min(2).optional(),
|
|
256
256
|
windowSeconds: z.number().min(10).optional()
|
|
257
|
+
}).optional(),
|
|
258
|
+
skillPinning: z.object({
|
|
259
|
+
enabled: z.boolean().optional(),
|
|
260
|
+
mode: z.enum(["warn", "block"]).optional(),
|
|
261
|
+
roots: z.array(z.string()).optional()
|
|
257
262
|
}).optional()
|
|
258
263
|
}).optional(),
|
|
259
264
|
environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
@@ -552,7 +557,11 @@ function getConfig(cwd) {
|
|
|
552
557
|
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
553
558
|
},
|
|
554
559
|
dlp: { ...DEFAULT_CONFIG.policy.dlp },
|
|
555
|
-
loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection }
|
|
560
|
+
loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection },
|
|
561
|
+
skillPinning: {
|
|
562
|
+
...DEFAULT_CONFIG.policy.skillPinning,
|
|
563
|
+
roots: [...DEFAULT_CONFIG.policy.skillPinning.roots]
|
|
564
|
+
}
|
|
556
565
|
};
|
|
557
566
|
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
558
567
|
const applyLayer = (source) => {
|
|
@@ -605,6 +614,16 @@ function getConfig(cwd) {
|
|
|
605
614
|
if (ld.windowSeconds !== void 0)
|
|
606
615
|
mergedPolicy.loopDetection.windowSeconds = ld.windowSeconds;
|
|
607
616
|
}
|
|
617
|
+
if (p.skillPinning && typeof p.skillPinning === "object") {
|
|
618
|
+
const sp = p.skillPinning;
|
|
619
|
+
if (sp.enabled !== void 0) mergedPolicy.skillPinning.enabled = sp.enabled;
|
|
620
|
+
if (sp.mode !== void 0) mergedPolicy.skillPinning.mode = sp.mode;
|
|
621
|
+
if (Array.isArray(sp.roots)) {
|
|
622
|
+
for (const r of sp.roots) {
|
|
623
|
+
if (typeof r === "string" && r.length > 0) mergedPolicy.skillPinning.roots.push(r);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
608
627
|
const envs = source.environments || {};
|
|
609
628
|
for (const [envName, envConfig] of Object.entries(envs)) {
|
|
610
629
|
if (envConfig && typeof envConfig === "object") {
|
|
@@ -656,6 +675,7 @@ function getConfig(cwd) {
|
|
|
656
675
|
mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
|
|
657
676
|
mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
|
|
658
677
|
mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
|
|
678
|
+
mergedPolicy.skillPinning.roots = [...new Set(mergedPolicy.skillPinning.roots)];
|
|
659
679
|
mergedPolicy.snapshot.tools = [...new Set(mergedPolicy.snapshot.tools)];
|
|
660
680
|
mergedPolicy.snapshot.onlyPaths = [...new Set(mergedPolicy.snapshot.onlyPaths)];
|
|
661
681
|
mergedPolicy.snapshot.ignorePaths = [...new Set(mergedPolicy.snapshot.ignorePaths)];
|
|
@@ -800,9 +820,8 @@ var init_config = __esm({
|
|
|
800
820
|
{
|
|
801
821
|
field: "command",
|
|
802
822
|
op: "matches",
|
|
803
|
-
//
|
|
804
|
-
|
|
805
|
-
value: "rm\\b.*\\s(-[rRfF]*[rR][rRfF]*|--recursive)(\\s|$)"
|
|
823
|
+
// Anchor rm as a shell command (not inside a string arg like a git commit message).
|
|
824
|
+
value: "(^|&&|\\|\\||;)\\s*rm\\b[^;&|]*\\s(-[rRfF]*[rR][rRfF]*|--recursive)(\\s|$)"
|
|
806
825
|
},
|
|
807
826
|
{
|
|
808
827
|
field: "command",
|
|
@@ -831,6 +850,13 @@ var init_config = __esm({
|
|
|
831
850
|
name: "review-drop-truncate-shell",
|
|
832
851
|
tool: "bash",
|
|
833
852
|
conditions: [
|
|
853
|
+
{
|
|
854
|
+
field: "command",
|
|
855
|
+
op: "matches",
|
|
856
|
+
// Require a DB CLI in the command so grep/cat/echo of SQL strings don't trigger.
|
|
857
|
+
value: "(^|&&|\\|\\||;|\\|)\\s*(psql|mysql|sqlite3|sqlplus|cockroach|clickhouse-client|mongo)\\b",
|
|
858
|
+
flags: "i"
|
|
859
|
+
},
|
|
834
860
|
{
|
|
835
861
|
field: "command",
|
|
836
862
|
op: "matches",
|
|
@@ -851,7 +877,9 @@ var init_config = __esm({
|
|
|
851
877
|
{
|
|
852
878
|
field: "command",
|
|
853
879
|
op: "matches",
|
|
854
|
-
|
|
880
|
+
// Anchor git as a shell command so node -e / python -c scripts containing
|
|
881
|
+
// "git push --force" as a string don't false-positive.
|
|
882
|
+
value: "(^|&&|\\|\\||;)\\s*git\\s+push[^;&|]*(--force|--force-with-lease|-f\\b)",
|
|
855
883
|
flags: "i"
|
|
856
884
|
}
|
|
857
885
|
],
|
|
@@ -861,29 +889,20 @@ var init_config = __esm({
|
|
|
861
889
|
description: "The AI wants to force push to a remote git branch. This rewrites shared history and can permanently destroy commits that teammates have already pulled."
|
|
862
890
|
},
|
|
863
891
|
{
|
|
864
|
-
name: "review-git-
|
|
892
|
+
name: "review-git-destructive",
|
|
865
893
|
tool: "bash",
|
|
866
894
|
conditions: [
|
|
867
895
|
{
|
|
868
896
|
field: "command",
|
|
869
897
|
op: "matches",
|
|
870
|
-
value: "\\bgit\\
|
|
898
|
+
value: "\\bgit\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase\\b|tag\\s+-d|branch\\s+-[dD])",
|
|
871
899
|
flags: "i"
|
|
872
|
-
}
|
|
873
|
-
],
|
|
874
|
-
conditionMode: "all",
|
|
875
|
-
verdict: "review",
|
|
876
|
-
reason: "git push sends changes to a shared remote",
|
|
877
|
-
description: "The AI wants to push commits to a remote repository. Once pushed, those changes are visible to everyone with access."
|
|
878
|
-
},
|
|
879
|
-
{
|
|
880
|
-
name: "review-git-destructive",
|
|
881
|
-
tool: "bash",
|
|
882
|
-
conditions: [
|
|
900
|
+
},
|
|
883
901
|
{
|
|
884
902
|
field: "command",
|
|
885
|
-
op: "
|
|
886
|
-
|
|
903
|
+
op: "notMatches",
|
|
904
|
+
// Exclude recovery ops — these resolve a conflict, not start a destructive action.
|
|
905
|
+
value: "\\bgit\\s+rebase\\s+--(abort|continue|skip)\\b",
|
|
887
906
|
flags: "i"
|
|
888
907
|
}
|
|
889
908
|
],
|
|
@@ -909,7 +928,9 @@ var init_config = __esm({
|
|
|
909
928
|
{
|
|
910
929
|
field: "command",
|
|
911
930
|
op: "matches",
|
|
912
|
-
|
|
931
|
+
// Anchor curl/wget as a shell command so node -e scripts testing this
|
|
932
|
+
// regex pattern don't self-match as a false positive.
|
|
933
|
+
value: "(^|&&|\\|\\||;)\\s*(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
|
|
913
934
|
flags: "i"
|
|
914
935
|
}
|
|
915
936
|
],
|
|
@@ -920,7 +941,8 @@ var init_config = __esm({
|
|
|
920
941
|
}
|
|
921
942
|
],
|
|
922
943
|
dlp: { enabled: true, scanIgnoredTools: true },
|
|
923
|
-
loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 }
|
|
944
|
+
loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 },
|
|
945
|
+
skillPinning: { enabled: false, mode: "warn", roots: [] }
|
|
924
946
|
},
|
|
925
947
|
environments: {}
|
|
926
948
|
};
|
|
@@ -1132,6 +1154,20 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
|
1132
1154
|
}
|
|
1133
1155
|
return null;
|
|
1134
1156
|
}
|
|
1157
|
+
function scanText(text) {
|
|
1158
|
+
const t = text.length > MAX_STRING_BYTES ? text.slice(0, MAX_STRING_BYTES) : text;
|
|
1159
|
+
for (const pattern of DLP_PATTERNS) {
|
|
1160
|
+
if (pattern.regex.test(t)) {
|
|
1161
|
+
return {
|
|
1162
|
+
patternName: pattern.name,
|
|
1163
|
+
fieldPath: "response-text",
|
|
1164
|
+
redactedSample: maskSecret(t, pattern.regex),
|
|
1165
|
+
severity: pattern.severity
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
return null;
|
|
1170
|
+
}
|
|
1135
1171
|
var DLP_PATTERNS, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
|
|
1136
1172
|
var init_dlp = __esm({
|
|
1137
1173
|
"src/dlp.ts"() {
|
|
@@ -1161,7 +1197,7 @@ var init_dlp = __esm({
|
|
|
1161
1197
|
regex: /_authToken\s*=\s*[A-Za-z0-9_\-]{20,}/,
|
|
1162
1198
|
severity: "block"
|
|
1163
1199
|
},
|
|
1164
|
-
{ name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]
|
|
1200
|
+
{ name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]{20,}=*/i, severity: "review" }
|
|
1165
1201
|
];
|
|
1166
1202
|
SENSITIVE_PATH_PATTERNS = [
|
|
1167
1203
|
/[/\\]\.ssh[/\\]/i,
|
|
@@ -1729,9 +1765,21 @@ function matchesPattern(text, patterns) {
|
|
|
1729
1765
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1730
1766
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1731
1767
|
}
|
|
1732
|
-
function getNestedValue(obj,
|
|
1768
|
+
function getNestedValue(obj, path43) {
|
|
1733
1769
|
if (!obj || typeof obj !== "object") return null;
|
|
1734
|
-
return
|
|
1770
|
+
return path43.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1771
|
+
}
|
|
1772
|
+
function stripStringArguments(cmd) {
|
|
1773
|
+
let result = cmd;
|
|
1774
|
+
result = result.replace(
|
|
1775
|
+
/\b(node|python3?|ruby|perl|php|deno)\s+(-[ecr]|eval)\s+("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/gi,
|
|
1776
|
+
'$1 $2 ""'
|
|
1777
|
+
);
|
|
1778
|
+
result = result.replace(
|
|
1779
|
+
/\s(-m|--message|--body|--title|--description)\s+("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g,
|
|
1780
|
+
' $1 ""'
|
|
1781
|
+
);
|
|
1782
|
+
return result;
|
|
1735
1783
|
}
|
|
1736
1784
|
function shouldSnapshot(toolName, args, config) {
|
|
1737
1785
|
if (!config.settings.enableUndo) return false;
|
|
@@ -1750,7 +1798,8 @@ function evaluateSmartConditions(args, rule) {
|
|
|
1750
1798
|
const mode = rule.conditionMode ?? "all";
|
|
1751
1799
|
const results = rule.conditions.map((cond) => {
|
|
1752
1800
|
const rawVal = getNestedValue(args, cond.field);
|
|
1753
|
-
const
|
|
1801
|
+
const normalized = rawVal !== null && rawVal !== void 0 ? String(rawVal).replace(/\s+/g, " ").trim() : null;
|
|
1802
|
+
const val = cond.field === "command" && normalized !== null ? stripStringArguments(normalized) : normalized;
|
|
1754
1803
|
switch (cond.op) {
|
|
1755
1804
|
case "exists":
|
|
1756
1805
|
return val !== null && val !== "";
|
|
@@ -2324,6 +2373,15 @@ var init_policy = __esm({
|
|
|
2324
2373
|
import fs8 from "fs";
|
|
2325
2374
|
import path9 from "path";
|
|
2326
2375
|
import os7 from "os";
|
|
2376
|
+
function extractCommandPattern(toolName, args) {
|
|
2377
|
+
const lower = toolName.toLowerCase();
|
|
2378
|
+
if (lower !== "bash" && lower !== "execute_bash" && lower !== "shell") return void 0;
|
|
2379
|
+
const a = args;
|
|
2380
|
+
const cmd = typeof a?.["command"] === "string" ? a["command"].trim() : "";
|
|
2381
|
+
if (!cmd) return void 0;
|
|
2382
|
+
const words = cmd.split(/\s+/);
|
|
2383
|
+
return words.slice(0, 2).join(" ");
|
|
2384
|
+
}
|
|
2327
2385
|
function checkPause() {
|
|
2328
2386
|
try {
|
|
2329
2387
|
if (!fs8.existsSync(PAUSED_FILE)) return { paused: false };
|
|
@@ -2357,7 +2415,7 @@ function resumeNode9() {
|
|
|
2357
2415
|
} catch {
|
|
2358
2416
|
}
|
|
2359
2417
|
}
|
|
2360
|
-
function getActiveTrustSession(toolName) {
|
|
2418
|
+
function getActiveTrustSession(toolName, args) {
|
|
2361
2419
|
try {
|
|
2362
2420
|
if (!fs8.existsSync(TRUST_FILE)) return false;
|
|
2363
2421
|
const trust = JSON.parse(fs8.readFileSync(TRUST_FILE, "utf-8"));
|
|
@@ -2366,12 +2424,20 @@ function getActiveTrustSession(toolName) {
|
|
|
2366
2424
|
if (active.length !== trust.entries.length) {
|
|
2367
2425
|
fs8.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
|
|
2368
2426
|
}
|
|
2369
|
-
return active.some((e) =>
|
|
2427
|
+
return active.some((e) => {
|
|
2428
|
+
if (!(e.tool === toolName || matchesPattern(toolName, e.tool))) return false;
|
|
2429
|
+
if (e.commandPattern) {
|
|
2430
|
+
const actual = extractCommandPattern(toolName, args) ?? "";
|
|
2431
|
+
return actual === e.commandPattern || actual.startsWith(e.commandPattern + " ");
|
|
2432
|
+
}
|
|
2433
|
+
return true;
|
|
2434
|
+
});
|
|
2370
2435
|
} catch {
|
|
2371
2436
|
return false;
|
|
2372
2437
|
}
|
|
2373
2438
|
}
|
|
2374
|
-
function writeTrustSession(toolName, durationMs) {
|
|
2439
|
+
function writeTrustSession(toolName, durationMs, args) {
|
|
2440
|
+
const commandPattern = extractCommandPattern(toolName, args);
|
|
2375
2441
|
try {
|
|
2376
2442
|
let trust = { entries: [] };
|
|
2377
2443
|
try {
|
|
@@ -2381,8 +2447,14 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
2381
2447
|
} catch {
|
|
2382
2448
|
}
|
|
2383
2449
|
const now = Date.now();
|
|
2384
|
-
trust.entries = trust.entries.filter(
|
|
2385
|
-
|
|
2450
|
+
trust.entries = trust.entries.filter(
|
|
2451
|
+
(e) => !(e.tool === toolName && e.commandPattern === commandPattern) && e.expiry > now
|
|
2452
|
+
);
|
|
2453
|
+
trust.entries.push({
|
|
2454
|
+
tool: toolName,
|
|
2455
|
+
...commandPattern && { commandPattern },
|
|
2456
|
+
expiry: now + durationMs
|
|
2457
|
+
});
|
|
2386
2458
|
atomicWriteSync(TRUST_FILE, JSON.stringify(trust, null, 2));
|
|
2387
2459
|
} catch (err2) {
|
|
2388
2460
|
if (process.env.NODE9_DEBUG === "1") {
|
|
@@ -2833,13 +2905,30 @@ ${smartTruncate(str, 500)}`
|
|
|
2833
2905
|
}
|
|
2834
2906
|
return { intent: "EXEC", message: smartTruncate(JSON.stringify(parsed), 200) };
|
|
2835
2907
|
}
|
|
2908
|
+
function sendDesktopNotification(title, body) {
|
|
2909
|
+
if (isTestEnv()) return;
|
|
2910
|
+
try {
|
|
2911
|
+
if (process.platform === "darwin") {
|
|
2912
|
+
const esc = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
2913
|
+
const script = `display notification "${esc(body)}" with title "${esc(title)}"`;
|
|
2914
|
+
spawn("osascript", ["-e", script], { detached: true, stdio: "ignore" }).unref();
|
|
2915
|
+
} else if (process.platform === "linux") {
|
|
2916
|
+
spawn("notify-send", [title, body, "--icon=dialog-warning"], {
|
|
2917
|
+
detached: true,
|
|
2918
|
+
stdio: "ignore"
|
|
2919
|
+
}).unref();
|
|
2920
|
+
}
|
|
2921
|
+
} catch {
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2836
2924
|
function escapePango(text) {
|
|
2837
2925
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2838
2926
|
}
|
|
2839
2927
|
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription) {
|
|
2840
2928
|
const lines = [];
|
|
2841
2929
|
if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
|
|
2842
|
-
|
|
2930
|
+
const safeAgent = (agent ?? "AI Agent").replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "").slice(0, 80);
|
|
2931
|
+
lines.push(`\u{1F916} ${safeAgent} | \u{1F527} ${toolName}`);
|
|
2843
2932
|
lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
|
|
2844
2933
|
if (ruleDescription) lines.push(`\u2139 ${ruleDescription}`);
|
|
2845
2934
|
lines.push("");
|
|
@@ -3224,7 +3313,16 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
3224
3313
|
if (!options?.calledFromDaemon) {
|
|
3225
3314
|
const actId = randomUUID();
|
|
3226
3315
|
const actTs = Date.now();
|
|
3227
|
-
await notifyActivity({
|
|
3316
|
+
await notifyActivity({
|
|
3317
|
+
id: actId,
|
|
3318
|
+
ts: actTs,
|
|
3319
|
+
tool: toolName,
|
|
3320
|
+
args,
|
|
3321
|
+
status: "pending",
|
|
3322
|
+
// Strip ANSI escape sequences — agent name comes from caller-supplied metadata
|
|
3323
|
+
// and may be displayed in a terminal (node9 tail/watch), enabling injection.
|
|
3324
|
+
agent: meta?.agent ? meta.agent.replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "").slice(0, 80) : void 0
|
|
3325
|
+
});
|
|
3228
3326
|
const result = await _authorizeHeadlessCore(toolName, args, meta, {
|
|
3229
3327
|
...options,
|
|
3230
3328
|
activityId: actId
|
|
@@ -3378,12 +3476,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3378
3476
|
};
|
|
3379
3477
|
}
|
|
3380
3478
|
}
|
|
3381
|
-
if (getActiveTrustSession(toolName)) {
|
|
3382
|
-
if (approvers.cloud && creds?.apiKey)
|
|
3383
|
-
await auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
3384
|
-
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
|
|
3385
|
-
return { approved: true, checkedBy: "trust" };
|
|
3386
|
-
}
|
|
3387
3479
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
3388
3480
|
if (policyResult.decision === "allow") {
|
|
3389
3481
|
if (approvers.cloud && creds?.apiKey)
|
|
@@ -3465,6 +3557,12 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3465
3557
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta, hashAuditArgs);
|
|
3466
3558
|
return { approved: true };
|
|
3467
3559
|
}
|
|
3560
|
+
if (!taintWarning && getActiveTrustSession(toolName, args)) {
|
|
3561
|
+
if (approvers.cloud && creds?.apiKey)
|
|
3562
|
+
await auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
3563
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
|
|
3564
|
+
return { approved: true, checkedBy: "trust" };
|
|
3565
|
+
}
|
|
3468
3566
|
if (taintWarning) {
|
|
3469
3567
|
explainableLabel = "\u{1F534} Node9 Taint (Exfiltration Prevention)";
|
|
3470
3568
|
riskMetadata = computeRiskMetadata(
|
|
@@ -3597,7 +3695,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3597
3695
|
riskMetadata?.ruleDescription
|
|
3598
3696
|
);
|
|
3599
3697
|
if (decision === "always_allow") {
|
|
3600
|
-
writeTrustSession(toolName, 36e5);
|
|
3698
|
+
writeTrustSession(toolName, 36e5, args);
|
|
3601
3699
|
return { approved: true, checkedBy: "trust" };
|
|
3602
3700
|
}
|
|
3603
3701
|
const isApproved = decision === "allow";
|
|
@@ -5775,7 +5873,7 @@ function writeGlobalSetting(key, value) {
|
|
|
5775
5873
|
config.settings[key] = value;
|
|
5776
5874
|
atomicWriteSync2(GLOBAL_CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
|
|
5777
5875
|
}
|
|
5778
|
-
function writeTrustEntry(toolName, durationMs) {
|
|
5876
|
+
function writeTrustEntry(toolName, durationMs, commandPattern) {
|
|
5779
5877
|
try {
|
|
5780
5878
|
let trust = { entries: [] };
|
|
5781
5879
|
try {
|
|
@@ -5783,8 +5881,14 @@ function writeTrustEntry(toolName, durationMs) {
|
|
|
5783
5881
|
trust = JSON.parse(fs14.readFileSync(TRUST_FILE2, "utf-8"));
|
|
5784
5882
|
} catch {
|
|
5785
5883
|
}
|
|
5786
|
-
trust.entries = trust.entries.filter(
|
|
5787
|
-
|
|
5884
|
+
trust.entries = trust.entries.filter(
|
|
5885
|
+
(e) => !(e.tool === toolName && e.commandPattern === commandPattern) && e.expiry > Date.now()
|
|
5886
|
+
);
|
|
5887
|
+
trust.entries.push({
|
|
5888
|
+
tool: toolName,
|
|
5889
|
+
...commandPattern && { commandPattern },
|
|
5890
|
+
expiry: Date.now() + durationMs
|
|
5891
|
+
});
|
|
5788
5892
|
atomicWriteSync2(TRUST_FILE2, JSON.stringify(trust, null, 2));
|
|
5789
5893
|
} catch {
|
|
5790
5894
|
}
|
|
@@ -5940,7 +6044,8 @@ function startActivitySocket() {
|
|
|
5940
6044
|
ts: data.ts,
|
|
5941
6045
|
tool: data.tool,
|
|
5942
6046
|
args: redactArgs(data.args),
|
|
5943
|
-
status: "pending"
|
|
6047
|
+
status: "pending",
|
|
6048
|
+
agent: data.agent
|
|
5944
6049
|
});
|
|
5945
6050
|
} else {
|
|
5946
6051
|
if (data.status === "allow") {
|
|
@@ -6398,16 +6503,167 @@ var init_sync = __esm({
|
|
|
6398
6503
|
}
|
|
6399
6504
|
});
|
|
6400
6505
|
|
|
6401
|
-
// src/daemon/
|
|
6402
|
-
import http from "http";
|
|
6506
|
+
// src/daemon/dlp-scanner.ts
|
|
6403
6507
|
import fs18 from "fs";
|
|
6404
6508
|
import path21 from "path";
|
|
6509
|
+
import os16 from "os";
|
|
6510
|
+
function loadIndex() {
|
|
6511
|
+
try {
|
|
6512
|
+
return JSON.parse(fs18.readFileSync(INDEX_FILE, "utf-8"));
|
|
6513
|
+
} catch {
|
|
6514
|
+
return {};
|
|
6515
|
+
}
|
|
6516
|
+
}
|
|
6517
|
+
function saveIndex(index) {
|
|
6518
|
+
try {
|
|
6519
|
+
fs18.writeFileSync(INDEX_FILE, JSON.stringify(index), { encoding: "utf-8", mode: 384 });
|
|
6520
|
+
} catch {
|
|
6521
|
+
}
|
|
6522
|
+
}
|
|
6523
|
+
function appendAuditEntry(entry) {
|
|
6524
|
+
try {
|
|
6525
|
+
fs18.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
|
|
6526
|
+
} catch {
|
|
6527
|
+
}
|
|
6528
|
+
}
|
|
6529
|
+
function runDlpScan() {
|
|
6530
|
+
if (!fs18.existsSync(PROJECTS_DIR)) return;
|
|
6531
|
+
const index = loadIndex();
|
|
6532
|
+
let updated = false;
|
|
6533
|
+
let projDirs;
|
|
6534
|
+
try {
|
|
6535
|
+
projDirs = fs18.readdirSync(PROJECTS_DIR);
|
|
6536
|
+
} catch {
|
|
6537
|
+
return;
|
|
6538
|
+
}
|
|
6539
|
+
for (const proj of projDirs) {
|
|
6540
|
+
const projPath = path21.join(PROJECTS_DIR, proj);
|
|
6541
|
+
try {
|
|
6542
|
+
if (!fs18.lstatSync(projPath).isDirectory()) continue;
|
|
6543
|
+
const real = fs18.realpathSync(projPath);
|
|
6544
|
+
if (!real.startsWith(PROJECTS_DIR + path21.sep) && real !== PROJECTS_DIR) continue;
|
|
6545
|
+
} catch {
|
|
6546
|
+
continue;
|
|
6547
|
+
}
|
|
6548
|
+
let files;
|
|
6549
|
+
try {
|
|
6550
|
+
files = fs18.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
|
|
6551
|
+
} catch {
|
|
6552
|
+
continue;
|
|
6553
|
+
}
|
|
6554
|
+
for (const file of files) {
|
|
6555
|
+
const filePath = path21.join(projPath, file);
|
|
6556
|
+
const lastOffset = index[filePath] ?? 0;
|
|
6557
|
+
let size;
|
|
6558
|
+
try {
|
|
6559
|
+
size = fs18.statSync(filePath).size;
|
|
6560
|
+
} catch {
|
|
6561
|
+
continue;
|
|
6562
|
+
}
|
|
6563
|
+
if (size <= lastOffset) continue;
|
|
6564
|
+
let fd;
|
|
6565
|
+
try {
|
|
6566
|
+
fd = fs18.openSync(filePath, "r");
|
|
6567
|
+
} catch {
|
|
6568
|
+
continue;
|
|
6569
|
+
}
|
|
6570
|
+
try {
|
|
6571
|
+
const chunkSize = size - lastOffset;
|
|
6572
|
+
const buf = Buffer.alloc(chunkSize);
|
|
6573
|
+
fs18.readSync(fd, buf, 0, chunkSize, lastOffset);
|
|
6574
|
+
const chunk = buf.toString("utf-8");
|
|
6575
|
+
for (const line of chunk.split("\n")) {
|
|
6576
|
+
if (!line.trim()) continue;
|
|
6577
|
+
let entry;
|
|
6578
|
+
try {
|
|
6579
|
+
entry = JSON.parse(line);
|
|
6580
|
+
} catch {
|
|
6581
|
+
continue;
|
|
6582
|
+
}
|
|
6583
|
+
if (entry.type !== "assistant") continue;
|
|
6584
|
+
const content = entry.message?.content;
|
|
6585
|
+
if (!Array.isArray(content)) continue;
|
|
6586
|
+
for (const block of content) {
|
|
6587
|
+
if (typeof block !== "object" || block === null || block.type !== "text")
|
|
6588
|
+
continue;
|
|
6589
|
+
const text = block.text;
|
|
6590
|
+
if (typeof text !== "string") continue;
|
|
6591
|
+
const match = scanText(text);
|
|
6592
|
+
if (!match) continue;
|
|
6593
|
+
const projLabel = decodeURIComponent(proj).replace(os16.homedir(), "~").slice(0, 40);
|
|
6594
|
+
const ts = entry.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
6595
|
+
appendAuditEntry({
|
|
6596
|
+
ts,
|
|
6597
|
+
tool: "response-text",
|
|
6598
|
+
decision: "dlp",
|
|
6599
|
+
checkedBy: "response-dlp",
|
|
6600
|
+
source: "response-dlp",
|
|
6601
|
+
dlpPattern: match.patternName,
|
|
6602
|
+
dlpSample: match.redactedSample,
|
|
6603
|
+
project: projLabel
|
|
6604
|
+
});
|
|
6605
|
+
sendDesktopNotification(
|
|
6606
|
+
"\u26A0\uFE0F node9 DLP Alert",
|
|
6607
|
+
`${match.patternName} found in Claude response
|
|
6608
|
+
Sample: ${match.redactedSample}
|
|
6609
|
+
Project: ${projLabel}
|
|
6610
|
+
Run: node9 report --period 30d`
|
|
6611
|
+
);
|
|
6612
|
+
}
|
|
6613
|
+
}
|
|
6614
|
+
index[filePath] = size;
|
|
6615
|
+
updated = true;
|
|
6616
|
+
} finally {
|
|
6617
|
+
try {
|
|
6618
|
+
fs18.closeSync(fd);
|
|
6619
|
+
} catch {
|
|
6620
|
+
}
|
|
6621
|
+
}
|
|
6622
|
+
}
|
|
6623
|
+
}
|
|
6624
|
+
if (updated) saveIndex(index);
|
|
6625
|
+
}
|
|
6626
|
+
function startDlpScanner() {
|
|
6627
|
+
setImmediate(() => {
|
|
6628
|
+
try {
|
|
6629
|
+
runDlpScan();
|
|
6630
|
+
} catch {
|
|
6631
|
+
}
|
|
6632
|
+
});
|
|
6633
|
+
const timer = setInterval(
|
|
6634
|
+
() => {
|
|
6635
|
+
try {
|
|
6636
|
+
runDlpScan();
|
|
6637
|
+
} catch {
|
|
6638
|
+
}
|
|
6639
|
+
},
|
|
6640
|
+
60 * 60 * 1e3
|
|
6641
|
+
);
|
|
6642
|
+
timer.unref();
|
|
6643
|
+
}
|
|
6644
|
+
var INDEX_FILE, PROJECTS_DIR;
|
|
6645
|
+
var init_dlp_scanner = __esm({
|
|
6646
|
+
"src/daemon/dlp-scanner.ts"() {
|
|
6647
|
+
"use strict";
|
|
6648
|
+
init_dlp();
|
|
6649
|
+
init_native();
|
|
6650
|
+
init_state2();
|
|
6651
|
+
INDEX_FILE = path21.join(os16.homedir(), ".node9", "dlp-index.json");
|
|
6652
|
+
PROJECTS_DIR = path21.join(os16.homedir(), ".claude", "projects");
|
|
6653
|
+
}
|
|
6654
|
+
});
|
|
6655
|
+
|
|
6656
|
+
// src/daemon/server.ts
|
|
6657
|
+
import http from "http";
|
|
6658
|
+
import fs19 from "fs";
|
|
6659
|
+
import path22 from "path";
|
|
6405
6660
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
6406
6661
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
6407
6662
|
import chalk2 from "chalk";
|
|
6408
6663
|
function startDaemon() {
|
|
6409
6664
|
startCostSync();
|
|
6410
6665
|
startCloudSync();
|
|
6666
|
+
startDlpScanner();
|
|
6411
6667
|
loadInsightCounts();
|
|
6412
6668
|
const csrfToken = randomUUID4();
|
|
6413
6669
|
const internalToken = randomUUID4();
|
|
@@ -6423,7 +6679,7 @@ function startDaemon() {
|
|
|
6423
6679
|
idleTimer = setTimeout(() => {
|
|
6424
6680
|
if (autoStarted) {
|
|
6425
6681
|
try {
|
|
6426
|
-
|
|
6682
|
+
fs19.unlinkSync(DAEMON_PID_FILE);
|
|
6427
6683
|
} catch {
|
|
6428
6684
|
}
|
|
6429
6685
|
}
|
|
@@ -6586,7 +6842,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
6586
6842
|
status: "pending"
|
|
6587
6843
|
});
|
|
6588
6844
|
}
|
|
6589
|
-
const projectCwd = typeof cwd === "string" &&
|
|
6845
|
+
const projectCwd = typeof cwd === "string" && path22.isAbsolute(cwd) ? cwd : void 0;
|
|
6590
6846
|
const projectConfig = getConfig(projectCwd);
|
|
6591
6847
|
const browserEnabled = projectConfig.settings.approvers?.browser !== false;
|
|
6592
6848
|
const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
|
|
@@ -6713,7 +6969,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
6713
6969
|
);
|
|
6714
6970
|
if (decision === "trust" && trustDuration) {
|
|
6715
6971
|
const ms = TRUST_DURATIONS[trustDuration] ?? 60 * 6e4;
|
|
6716
|
-
|
|
6972
|
+
const commandPattern = extractCommandPattern(entry.toolName, entry.args);
|
|
6973
|
+
writeTrustEntry(entry.toolName, ms, commandPattern);
|
|
6717
6974
|
appendAuditLog({
|
|
6718
6975
|
toolName: entry.toolName,
|
|
6719
6976
|
args: entry.args,
|
|
@@ -6976,8 +7233,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
6976
7233
|
const body = await readBody(req);
|
|
6977
7234
|
const data = body ? JSON.parse(body) : {};
|
|
6978
7235
|
const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
|
|
6979
|
-
const node9Dir =
|
|
6980
|
-
if (!
|
|
7236
|
+
const node9Dir = path22.dirname(GLOBAL_CONFIG_PATH);
|
|
7237
|
+
if (!path22.resolve(configPath).startsWith(node9Dir + path22.sep)) {
|
|
6981
7238
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6982
7239
|
return res.end(
|
|
6983
7240
|
JSON.stringify({ error: "configPath must be within the node9 config directory" })
|
|
@@ -7088,14 +7345,14 @@ data: ${JSON.stringify(item.data)}
|
|
|
7088
7345
|
server.on("error", (e) => {
|
|
7089
7346
|
if (e.code === "EADDRINUSE") {
|
|
7090
7347
|
try {
|
|
7091
|
-
if (
|
|
7092
|
-
const { pid } = JSON.parse(
|
|
7348
|
+
if (fs19.existsSync(DAEMON_PID_FILE)) {
|
|
7349
|
+
const { pid } = JSON.parse(fs19.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
7093
7350
|
process.kill(pid, 0);
|
|
7094
7351
|
return process.exit(0);
|
|
7095
7352
|
}
|
|
7096
7353
|
} catch {
|
|
7097
7354
|
try {
|
|
7098
|
-
|
|
7355
|
+
fs19.unlinkSync(DAEMON_PID_FILE);
|
|
7099
7356
|
} catch {
|
|
7100
7357
|
}
|
|
7101
7358
|
server.listen(DAEMON_PORT, DAEMON_HOST);
|
|
@@ -7161,23 +7418,25 @@ var init_server = __esm({
|
|
|
7161
7418
|
init_shields();
|
|
7162
7419
|
init_ui2();
|
|
7163
7420
|
init_state2();
|
|
7421
|
+
init_state();
|
|
7164
7422
|
init_patch();
|
|
7165
7423
|
init_config_schema();
|
|
7166
7424
|
init_costSync();
|
|
7167
7425
|
init_sync();
|
|
7426
|
+
init_dlp_scanner();
|
|
7168
7427
|
}
|
|
7169
7428
|
});
|
|
7170
7429
|
|
|
7171
7430
|
// src/daemon/service.ts
|
|
7172
|
-
import
|
|
7173
|
-
import
|
|
7174
|
-
import
|
|
7431
|
+
import fs20 from "fs";
|
|
7432
|
+
import path23 from "path";
|
|
7433
|
+
import os17 from "os";
|
|
7175
7434
|
import { spawnSync as spawnSync3, execFileSync } from "child_process";
|
|
7176
7435
|
function resolveNode9Binary() {
|
|
7177
7436
|
try {
|
|
7178
7437
|
const script = process.argv[1];
|
|
7179
|
-
if (typeof script === "string" &&
|
|
7180
|
-
return
|
|
7438
|
+
if (typeof script === "string" && path23.isAbsolute(script) && fs20.existsSync(script)) {
|
|
7439
|
+
return fs20.realpathSync(script);
|
|
7181
7440
|
}
|
|
7182
7441
|
} catch {
|
|
7183
7442
|
}
|
|
@@ -7195,11 +7454,11 @@ function xmlEscape(s) {
|
|
|
7195
7454
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
7196
7455
|
}
|
|
7197
7456
|
function launchdPlist(binaryPath) {
|
|
7198
|
-
const logDir =
|
|
7457
|
+
const logDir = path23.join(os17.homedir(), ".node9");
|
|
7199
7458
|
const nodePath = xmlEscape(process.execPath);
|
|
7200
7459
|
const scriptPath = xmlEscape(binaryPath);
|
|
7201
|
-
const outLog = xmlEscape(
|
|
7202
|
-
const errLog = xmlEscape(
|
|
7460
|
+
const outLog = xmlEscape(path23.join(logDir, "daemon.log"));
|
|
7461
|
+
const errLog = xmlEscape(path23.join(logDir, "daemon-error.log"));
|
|
7203
7462
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
7204
7463
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
7205
7464
|
<plist version="1.0">
|
|
@@ -7234,9 +7493,9 @@ function launchdPlist(binaryPath) {
|
|
|
7234
7493
|
`;
|
|
7235
7494
|
}
|
|
7236
7495
|
function installLaunchd(binaryPath) {
|
|
7237
|
-
const dir =
|
|
7238
|
-
if (!
|
|
7239
|
-
|
|
7496
|
+
const dir = path23.dirname(LAUNCHD_PLIST);
|
|
7497
|
+
if (!fs20.existsSync(dir)) fs20.mkdirSync(dir, { recursive: true });
|
|
7498
|
+
fs20.writeFileSync(LAUNCHD_PLIST, launchdPlist(binaryPath), "utf-8");
|
|
7240
7499
|
spawnSync3("launchctl", ["unload", LAUNCHD_PLIST], { encoding: "utf8" });
|
|
7241
7500
|
const r = spawnSync3("launchctl", ["load", "-w", LAUNCHD_PLIST], {
|
|
7242
7501
|
encoding: "utf8",
|
|
@@ -7247,13 +7506,13 @@ function installLaunchd(binaryPath) {
|
|
|
7247
7506
|
}
|
|
7248
7507
|
}
|
|
7249
7508
|
function uninstallLaunchd() {
|
|
7250
|
-
if (
|
|
7509
|
+
if (fs20.existsSync(LAUNCHD_PLIST)) {
|
|
7251
7510
|
spawnSync3("launchctl", ["unload", "-w", LAUNCHD_PLIST], { encoding: "utf8", timeout: 5e3 });
|
|
7252
|
-
|
|
7511
|
+
fs20.unlinkSync(LAUNCHD_PLIST);
|
|
7253
7512
|
}
|
|
7254
7513
|
}
|
|
7255
7514
|
function isLaunchdInstalled() {
|
|
7256
|
-
return
|
|
7515
|
+
return fs20.existsSync(LAUNCHD_PLIST);
|
|
7257
7516
|
}
|
|
7258
7517
|
function systemdUnit(binaryPath) {
|
|
7259
7518
|
return `[Unit]
|
|
@@ -7273,12 +7532,12 @@ WantedBy=default.target
|
|
|
7273
7532
|
`;
|
|
7274
7533
|
}
|
|
7275
7534
|
function installSystemd(binaryPath) {
|
|
7276
|
-
if (!
|
|
7277
|
-
|
|
7535
|
+
if (!fs20.existsSync(SYSTEMD_UNIT_DIR)) {
|
|
7536
|
+
fs20.mkdirSync(SYSTEMD_UNIT_DIR, { recursive: true });
|
|
7278
7537
|
}
|
|
7279
|
-
|
|
7538
|
+
fs20.writeFileSync(SYSTEMD_UNIT, systemdUnit(binaryPath), "utf-8");
|
|
7280
7539
|
try {
|
|
7281
|
-
execFileSync("loginctl", ["enable-linger",
|
|
7540
|
+
execFileSync("loginctl", ["enable-linger", os17.userInfo().username], { timeout: 3e3 });
|
|
7282
7541
|
} catch {
|
|
7283
7542
|
}
|
|
7284
7543
|
const reload = spawnSync3("systemctl", ["--user", "daemon-reload"], {
|
|
@@ -7298,23 +7557,23 @@ function installSystemd(binaryPath) {
|
|
|
7298
7557
|
}
|
|
7299
7558
|
}
|
|
7300
7559
|
function uninstallSystemd() {
|
|
7301
|
-
if (
|
|
7560
|
+
if (fs20.existsSync(SYSTEMD_UNIT)) {
|
|
7302
7561
|
spawnSync3("systemctl", ["--user", "disable", "--now", "node9-daemon"], {
|
|
7303
7562
|
encoding: "utf8",
|
|
7304
7563
|
timeout: 5e3
|
|
7305
7564
|
});
|
|
7306
7565
|
spawnSync3("systemctl", ["--user", "daemon-reload"], { encoding: "utf8", timeout: 5e3 });
|
|
7307
|
-
|
|
7566
|
+
fs20.unlinkSync(SYSTEMD_UNIT);
|
|
7308
7567
|
}
|
|
7309
7568
|
}
|
|
7310
7569
|
function isSystemdInstalled() {
|
|
7311
|
-
return
|
|
7570
|
+
return fs20.existsSync(SYSTEMD_UNIT);
|
|
7312
7571
|
}
|
|
7313
7572
|
function stopRunningDaemon() {
|
|
7314
|
-
const pidFile =
|
|
7315
|
-
if (!
|
|
7573
|
+
const pidFile = path23.join(os17.homedir(), ".node9", "daemon.pid");
|
|
7574
|
+
if (!fs20.existsSync(pidFile)) return;
|
|
7316
7575
|
try {
|
|
7317
|
-
const data = JSON.parse(
|
|
7576
|
+
const data = JSON.parse(fs20.readFileSync(pidFile, "utf-8"));
|
|
7318
7577
|
const pid = data.pid;
|
|
7319
7578
|
const MAX_PID2 = 4194304;
|
|
7320
7579
|
if (typeof pid === "number" && Number.isInteger(pid) && pid > 0 && pid <= MAX_PID2) {
|
|
@@ -7334,7 +7593,7 @@ function stopRunningDaemon() {
|
|
|
7334
7593
|
}
|
|
7335
7594
|
}
|
|
7336
7595
|
try {
|
|
7337
|
-
|
|
7596
|
+
fs20.unlinkSync(pidFile);
|
|
7338
7597
|
} catch {
|
|
7339
7598
|
}
|
|
7340
7599
|
} catch {
|
|
@@ -7409,20 +7668,20 @@ var init_service = __esm({
|
|
|
7409
7668
|
"src/daemon/service.ts"() {
|
|
7410
7669
|
"use strict";
|
|
7411
7670
|
LAUNCHD_LABEL = "ai.node9.daemon";
|
|
7412
|
-
LAUNCHD_PLIST =
|
|
7413
|
-
SYSTEMD_UNIT_DIR =
|
|
7414
|
-
SYSTEMD_UNIT =
|
|
7671
|
+
LAUNCHD_PLIST = path23.join(os17.homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
7672
|
+
SYSTEMD_UNIT_DIR = path23.join(os17.homedir(), ".config", "systemd", "user");
|
|
7673
|
+
SYSTEMD_UNIT = path23.join(SYSTEMD_UNIT_DIR, "node9-daemon.service");
|
|
7415
7674
|
}
|
|
7416
7675
|
});
|
|
7417
7676
|
|
|
7418
7677
|
// src/daemon/index.ts
|
|
7419
|
-
import
|
|
7678
|
+
import fs21 from "fs";
|
|
7420
7679
|
import chalk3 from "chalk";
|
|
7421
7680
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
7422
7681
|
function stopDaemon() {
|
|
7423
|
-
if (!
|
|
7682
|
+
if (!fs21.existsSync(DAEMON_PID_FILE)) return console.log(chalk3.yellow("Not running."));
|
|
7424
7683
|
try {
|
|
7425
|
-
const data = JSON.parse(
|
|
7684
|
+
const data = JSON.parse(fs21.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
7426
7685
|
const pid = data.pid;
|
|
7427
7686
|
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || pid > MAX_PID) {
|
|
7428
7687
|
console.log(chalk3.gray("Cleaned up invalid PID file."));
|
|
@@ -7434,7 +7693,7 @@ function stopDaemon() {
|
|
|
7434
7693
|
console.log(chalk3.gray("Cleaned up stale PID file."));
|
|
7435
7694
|
} finally {
|
|
7436
7695
|
try {
|
|
7437
|
-
|
|
7696
|
+
fs21.unlinkSync(DAEMON_PID_FILE);
|
|
7438
7697
|
} catch {
|
|
7439
7698
|
}
|
|
7440
7699
|
}
|
|
@@ -7443,9 +7702,9 @@ function daemonStatus() {
|
|
|
7443
7702
|
const serviceInstalled = isDaemonServiceInstalled();
|
|
7444
7703
|
const serviceLabel = serviceInstalled ? chalk3.green("installed (starts on login)") : chalk3.yellow("not installed \u2014 run: node9 daemon install");
|
|
7445
7704
|
let processStatus;
|
|
7446
|
-
if (
|
|
7705
|
+
if (fs21.existsSync(DAEMON_PID_FILE)) {
|
|
7447
7706
|
try {
|
|
7448
|
-
const data = JSON.parse(
|
|
7707
|
+
const data = JSON.parse(fs21.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
7449
7708
|
const pid = data.pid;
|
|
7450
7709
|
const port = data.port;
|
|
7451
7710
|
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || pid > MAX_PID) {
|
|
@@ -7494,10 +7753,10 @@ __export(tail_exports, {
|
|
|
7494
7753
|
startTail: () => startTail
|
|
7495
7754
|
});
|
|
7496
7755
|
import http2 from "http";
|
|
7497
|
-
import
|
|
7498
|
-
import
|
|
7499
|
-
import
|
|
7500
|
-
import
|
|
7756
|
+
import chalk25 from "chalk";
|
|
7757
|
+
import fs37 from "fs";
|
|
7758
|
+
import os33 from "os";
|
|
7759
|
+
import path40 from "path";
|
|
7501
7760
|
import readline5 from "readline";
|
|
7502
7761
|
import { spawn as spawn10, execSync as execSync3 } from "child_process";
|
|
7503
7762
|
function getIcon(tool) {
|
|
@@ -7507,6 +7766,74 @@ function getIcon(tool) {
|
|
|
7507
7766
|
}
|
|
7508
7767
|
return "\u{1F6E0}\uFE0F";
|
|
7509
7768
|
}
|
|
7769
|
+
function getModelContextLimit(model) {
|
|
7770
|
+
const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
|
|
7771
|
+
for (const [key, limit] of Object.entries(MODEL_CONTEXT_LIMITS)) {
|
|
7772
|
+
if (base.startsWith(key)) return limit;
|
|
7773
|
+
}
|
|
7774
|
+
return 2e5;
|
|
7775
|
+
}
|
|
7776
|
+
function readSessionUsage() {
|
|
7777
|
+
const projectsDir = path40.join(os33.homedir(), ".claude", "projects");
|
|
7778
|
+
if (!fs37.existsSync(projectsDir)) return null;
|
|
7779
|
+
let latestFile = null;
|
|
7780
|
+
let latestMtime = 0;
|
|
7781
|
+
try {
|
|
7782
|
+
for (const dir of fs37.readdirSync(projectsDir)) {
|
|
7783
|
+
const dirPath = path40.join(projectsDir, dir);
|
|
7784
|
+
try {
|
|
7785
|
+
if (!fs37.statSync(dirPath).isDirectory()) continue;
|
|
7786
|
+
for (const file of fs37.readdirSync(dirPath)) {
|
|
7787
|
+
if (!file.endsWith(".jsonl") || file.startsWith("agent-")) continue;
|
|
7788
|
+
const filePath = path40.join(dirPath, file);
|
|
7789
|
+
try {
|
|
7790
|
+
const mtime = fs37.statSync(filePath).mtimeMs;
|
|
7791
|
+
if (mtime > latestMtime) {
|
|
7792
|
+
latestMtime = mtime;
|
|
7793
|
+
latestFile = filePath;
|
|
7794
|
+
}
|
|
7795
|
+
} catch {
|
|
7796
|
+
}
|
|
7797
|
+
}
|
|
7798
|
+
} catch {
|
|
7799
|
+
}
|
|
7800
|
+
}
|
|
7801
|
+
} catch {
|
|
7802
|
+
}
|
|
7803
|
+
if (!latestFile) return null;
|
|
7804
|
+
try {
|
|
7805
|
+
const lines = fs37.readFileSync(latestFile, "utf-8").split("\n");
|
|
7806
|
+
let lastModel = "";
|
|
7807
|
+
let lastInput = 0;
|
|
7808
|
+
let lastOutput = 0;
|
|
7809
|
+
for (const line of lines) {
|
|
7810
|
+
if (!line.trim()) continue;
|
|
7811
|
+
try {
|
|
7812
|
+
const entry = JSON.parse(line);
|
|
7813
|
+
if (entry.type !== "assistant" || !entry.message?.usage) continue;
|
|
7814
|
+
const u = entry.message.usage;
|
|
7815
|
+
lastInput = (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
|
|
7816
|
+
lastOutput = u.output_tokens ?? 0;
|
|
7817
|
+
if (entry.message.model) lastModel = entry.message.model;
|
|
7818
|
+
} catch {
|
|
7819
|
+
}
|
|
7820
|
+
}
|
|
7821
|
+
if (!lastModel || lastInput === 0) return null;
|
|
7822
|
+
const limit = getModelContextLimit(lastModel);
|
|
7823
|
+
const fillPct = Math.round(lastInput / limit * 100);
|
|
7824
|
+
return { inputTokens: lastInput, outputTokens: lastOutput, model: lastModel, fillPct };
|
|
7825
|
+
} catch {
|
|
7826
|
+
return null;
|
|
7827
|
+
}
|
|
7828
|
+
}
|
|
7829
|
+
function formatContextStat(stat) {
|
|
7830
|
+
const pctColor = stat.fillPct >= 80 ? chalk25.red : stat.fillPct >= 50 ? chalk25.yellow : chalk25.cyan;
|
|
7831
|
+
const k = (n) => `${Math.round(n / 1e3)}k`;
|
|
7832
|
+
const modelShort = stat.model.replace(/@.*$/, "").replace(/-\d{8}$/, "").replace(/^claude-/, "");
|
|
7833
|
+
return chalk25.dim("ctx: ") + pctColor(`${stat.fillPct}%`) + chalk25.dim(
|
|
7834
|
+
` (${k(stat.inputTokens)}/${k(getModelContextLimit(stat.model))} out ${k(stat.outputTokens)} \xB7 ${modelShort})`
|
|
7835
|
+
);
|
|
7836
|
+
}
|
|
7510
7837
|
function visibleLength(s) {
|
|
7511
7838
|
return s.replace(/\x1B\[[0-9;]*m/g, "").length;
|
|
7512
7839
|
}
|
|
@@ -7516,26 +7843,31 @@ function wrappedLineCount(text) {
|
|
|
7516
7843
|
const len = visibleLength(text);
|
|
7517
7844
|
return Math.max(1, Math.ceil(len / cols));
|
|
7518
7845
|
}
|
|
7846
|
+
function agentLabel(agent) {
|
|
7847
|
+
if (!agent || agent === "Terminal") return "";
|
|
7848
|
+
const short = agent === "Claude Code" ? "Claude" : agent === "Gemini CLI" ? "Gemini" : agent === "Unknown Agent" ? "" : agent.split(" ")[0];
|
|
7849
|
+
return short ? chalk25.dim(`[${short}] `) : "";
|
|
7850
|
+
}
|
|
7519
7851
|
function formatBase(activity) {
|
|
7520
7852
|
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
7521
7853
|
const icon = getIcon(activity.tool);
|
|
7522
7854
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
7523
|
-
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(
|
|
7855
|
+
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os33.homedir(), "~");
|
|
7524
7856
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
7525
|
-
return `${
|
|
7857
|
+
return `${chalk25.gray(time)} ${icon} ${agentLabel(activity.agent)}${chalk25.white.bold(toolName)} ${chalk25.dim(argsPreview)}`;
|
|
7526
7858
|
}
|
|
7527
7859
|
function renderResult(activity, result) {
|
|
7528
7860
|
const base = formatBase(activity);
|
|
7529
7861
|
let status;
|
|
7530
7862
|
if (result.status === "allow") {
|
|
7531
|
-
status =
|
|
7863
|
+
status = chalk25.green("\u2713 ALLOW");
|
|
7532
7864
|
} else if (result.status === "dlp") {
|
|
7533
|
-
status =
|
|
7865
|
+
status = chalk25.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
7534
7866
|
} else {
|
|
7535
|
-
status =
|
|
7867
|
+
status = chalk25.red("\u2717 BLOCK");
|
|
7536
7868
|
}
|
|
7537
7869
|
const cost = result.costEstimate ?? activity.costEstimate;
|
|
7538
|
-
const costSuffix = cost == null ? "" :
|
|
7870
|
+
const costSuffix = cost == null ? "" : chalk25.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
|
|
7539
7871
|
if (process.stdout.isTTY) {
|
|
7540
7872
|
if (pendingShownForId === activity.id && pendingWrappedLines > 1) {
|
|
7541
7873
|
readline5.moveCursor(process.stdout, 0, -(pendingWrappedLines - 1));
|
|
@@ -7552,19 +7884,19 @@ function renderResult(activity, result) {
|
|
|
7552
7884
|
}
|
|
7553
7885
|
function renderPending(activity) {
|
|
7554
7886
|
if (!process.stdout.isTTY) return;
|
|
7555
|
-
const line = `${formatBase(activity)} ${
|
|
7887
|
+
const line = `${formatBase(activity)} ${chalk25.yellow("\u25CF \u2026")}`;
|
|
7556
7888
|
pendingShownForId = activity.id;
|
|
7557
7889
|
pendingWrappedLines = wrappedLineCount(line);
|
|
7558
7890
|
process.stdout.write(`${line}\r`);
|
|
7559
7891
|
}
|
|
7560
7892
|
async function ensureDaemon() {
|
|
7561
7893
|
let pidPort = null;
|
|
7562
|
-
if (
|
|
7894
|
+
if (fs37.existsSync(PID_FILE)) {
|
|
7563
7895
|
try {
|
|
7564
|
-
const { port } = JSON.parse(
|
|
7896
|
+
const { port } = JSON.parse(fs37.readFileSync(PID_FILE, "utf-8"));
|
|
7565
7897
|
pidPort = port;
|
|
7566
7898
|
} catch {
|
|
7567
|
-
console.error(
|
|
7899
|
+
console.error(chalk25.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
7568
7900
|
}
|
|
7569
7901
|
}
|
|
7570
7902
|
const checkPort = pidPort ?? DAEMON_PORT;
|
|
@@ -7575,7 +7907,7 @@ async function ensureDaemon() {
|
|
|
7575
7907
|
if (res.ok) return checkPort;
|
|
7576
7908
|
} catch {
|
|
7577
7909
|
}
|
|
7578
|
-
console.log(
|
|
7910
|
+
console.log(chalk25.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
7579
7911
|
const child = spawn10(process.execPath, [process.argv[1], "daemon"], {
|
|
7580
7912
|
detached: true,
|
|
7581
7913
|
stdio: "ignore",
|
|
@@ -7592,7 +7924,7 @@ async function ensureDaemon() {
|
|
|
7592
7924
|
} catch {
|
|
7593
7925
|
}
|
|
7594
7926
|
}
|
|
7595
|
-
console.error(
|
|
7927
|
+
console.error(chalk25.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
7596
7928
|
process.exit(1);
|
|
7597
7929
|
}
|
|
7598
7930
|
function postDecisionHttp(id, decision, csrfToken, port, opts) {
|
|
@@ -7658,10 +7990,11 @@ function buildCardLines(req, localCount = 0) {
|
|
|
7658
7990
|
const severityIcon = isBlock ? `${RED}\u{1F6D1}` : `${YELLOW}\u26A0 `;
|
|
7659
7991
|
const rawDesc = req.riskMetadata?.ruleDescription ?? "";
|
|
7660
7992
|
const description = rawDesc ? cleanReason(rawDesc) : "";
|
|
7993
|
+
const agentSuffix = req.agent && req.agent !== "Terminal" ? ` ${RESET2}${chalk25.dim(`(${req.agent})`)}` : "";
|
|
7661
7994
|
const lines = [
|
|
7662
7995
|
``,
|
|
7663
7996
|
`${BOLD2}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET2}`,
|
|
7664
|
-
`${CYAN}\u2551${RESET2} Tool: ${BOLD2}${req.toolName}${RESET2}`,
|
|
7997
|
+
`${CYAN}\u2551${RESET2} Tool: ${BOLD2}${req.toolName}${RESET2}${agentSuffix}`,
|
|
7665
7998
|
`${CYAN}\u2551${RESET2} Policy: ${severityIcon} ${blockedBy}${RESET2}`
|
|
7666
7999
|
];
|
|
7667
8000
|
if (description) {
|
|
@@ -7713,9 +8046,9 @@ function buildRecoveryCardLines(req) {
|
|
|
7713
8046
|
];
|
|
7714
8047
|
}
|
|
7715
8048
|
function readApproversFromDisk() {
|
|
7716
|
-
const configPath =
|
|
8049
|
+
const configPath = path40.join(os33.homedir(), ".node9", "config.json");
|
|
7717
8050
|
try {
|
|
7718
|
-
const raw = JSON.parse(
|
|
8051
|
+
const raw = JSON.parse(fs37.readFileSync(configPath, "utf-8"));
|
|
7719
8052
|
const settings = raw.settings ?? {};
|
|
7720
8053
|
return settings.approvers ?? {};
|
|
7721
8054
|
} catch {
|
|
@@ -7726,20 +8059,20 @@ function approverStatusLine() {
|
|
|
7726
8059
|
const a = readApproversFromDisk();
|
|
7727
8060
|
const fmt = (label, key) => {
|
|
7728
8061
|
const on = a[key] !== false;
|
|
7729
|
-
return `[${key[0]}]${label.slice(1)} ${on ?
|
|
8062
|
+
return `[${key[0]}]${label.slice(1)} ${on ? chalk25.green("\u2713") : chalk25.dim("\u2717")}`;
|
|
7730
8063
|
};
|
|
7731
8064
|
return `${fmt("native", "native")} ${fmt("browser", "browser")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
|
|
7732
8065
|
}
|
|
7733
8066
|
function toggleApprover(channel) {
|
|
7734
|
-
const configPath =
|
|
8067
|
+
const configPath = path40.join(os33.homedir(), ".node9", "config.json");
|
|
7735
8068
|
try {
|
|
7736
|
-
const raw = JSON.parse(
|
|
8069
|
+
const raw = JSON.parse(fs37.readFileSync(configPath, "utf-8"));
|
|
7737
8070
|
const settings = raw.settings ?? {};
|
|
7738
8071
|
const approvers = settings.approvers ?? {};
|
|
7739
8072
|
approvers[channel] = approvers[channel] === false;
|
|
7740
8073
|
settings.approvers = approvers;
|
|
7741
8074
|
raw.settings = settings;
|
|
7742
|
-
|
|
8075
|
+
fs37.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
|
|
7743
8076
|
} catch (err2) {
|
|
7744
8077
|
process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
|
|
7745
8078
|
`);
|
|
@@ -7771,7 +8104,7 @@ async function startTail(options = {}) {
|
|
|
7771
8104
|
req2.end();
|
|
7772
8105
|
});
|
|
7773
8106
|
if (result.ok) {
|
|
7774
|
-
console.log(
|
|
8107
|
+
console.log(chalk25.green("\u2713 Flight Recorder buffer cleared."));
|
|
7775
8108
|
} else if (result.code === "ECONNREFUSED") {
|
|
7776
8109
|
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
7777
8110
|
} else if (result.code === "ETIMEDOUT") {
|
|
@@ -7815,7 +8148,7 @@ async function startTail(options = {}) {
|
|
|
7815
8148
|
const channel = name === "n" ? "native" : name === "b" ? "browser" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
|
|
7816
8149
|
if (channel) {
|
|
7817
8150
|
toggleApprover(channel);
|
|
7818
|
-
console.log(
|
|
8151
|
+
console.log(chalk25.dim(` Approvers: ${approverStatusLine()}`));
|
|
7819
8152
|
}
|
|
7820
8153
|
};
|
|
7821
8154
|
process.stdin.on("keypress", idleKeypressHandler);
|
|
@@ -7881,7 +8214,7 @@ async function startTail(options = {}) {
|
|
|
7881
8214
|
localAllowCounts.get(req2.toolName) ?? 0
|
|
7882
8215
|
)
|
|
7883
8216
|
);
|
|
7884
|
-
const decisionStamp = action === "always-allow" ?
|
|
8217
|
+
const decisionStamp = action === "always-allow" ? chalk25.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk25.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk25.green("\u2713 ALLOWED") : action === "redirect" ? chalk25.yellow("\u21A9 REDIRECT AI") : chalk25.red("\u2717 DENIED");
|
|
7885
8218
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
|
|
7886
8219
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
7887
8220
|
process.stdout.write(SHOW_CURSOR);
|
|
@@ -7909,8 +8242,8 @@ async function startTail(options = {}) {
|
|
|
7909
8242
|
}
|
|
7910
8243
|
postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err2) => {
|
|
7911
8244
|
try {
|
|
7912
|
-
|
|
7913
|
-
|
|
8245
|
+
fs37.appendFileSync(
|
|
8246
|
+
path40.join(os33.homedir(), ".node9", "hook-debug.log"),
|
|
7914
8247
|
`[tail] POST /decision failed: ${String(err2)}
|
|
7915
8248
|
`
|
|
7916
8249
|
);
|
|
@@ -7932,7 +8265,7 @@ async function startTail(options = {}) {
|
|
|
7932
8265
|
);
|
|
7933
8266
|
const stampedLines = buildCardLines(req2, priorCount);
|
|
7934
8267
|
if (externalDecision) {
|
|
7935
|
-
const source = externalDecision === "allow" ?
|
|
8268
|
+
const source = externalDecision === "allow" ? chalk25.green("\u2713 ALLOWED") : chalk25.red("\u2717 DENIED");
|
|
7936
8269
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
|
|
7937
8270
|
}
|
|
7938
8271
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
@@ -7991,16 +8324,31 @@ async function startTail(options = {}) {
|
|
|
7991
8324
|
}
|
|
7992
8325
|
} catch {
|
|
7993
8326
|
}
|
|
7994
|
-
|
|
7995
|
-
|
|
8327
|
+
const auditLog = path40.join(os33.homedir(), ".node9", "audit.log");
|
|
8328
|
+
try {
|
|
8329
|
+
const unackedDlp = fs37.readFileSync(auditLog, "utf-8").split("\n").filter((l) => l.includes('"response-dlp"')).length;
|
|
8330
|
+
if (unackedDlp > 0) {
|
|
8331
|
+
console.log("");
|
|
8332
|
+
console.log(
|
|
8333
|
+
chalk25.bgRed.white.bold(
|
|
8334
|
+
` \u26A0\uFE0F DLP ALERT: ${unackedDlp} secret${unackedDlp !== 1 ? "s" : ""} found in Claude response text \u2014 run: node9 dlp `
|
|
8335
|
+
)
|
|
8336
|
+
);
|
|
8337
|
+
}
|
|
8338
|
+
} catch {
|
|
8339
|
+
}
|
|
8340
|
+
console.log(chalk25.cyan.bold(`
|
|
8341
|
+
\u{1F6F0}\uFE0F Node9 tail `) + chalk25.dim(`\u2192 ${dashboardUrl}`));
|
|
7996
8342
|
if (canApprove) {
|
|
7997
|
-
console.log(
|
|
7998
|
-
console.log(
|
|
8343
|
+
console.log(chalk25.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
|
|
8344
|
+
console.log(chalk25.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
|
|
7999
8345
|
}
|
|
8346
|
+
const ctxStat = readSessionUsage();
|
|
8347
|
+
if (ctxStat) console.log(" " + formatContextStat(ctxStat));
|
|
8000
8348
|
if (options.history) {
|
|
8001
|
-
console.log(
|
|
8349
|
+
console.log(chalk25.dim("Showing history + live events.\n"));
|
|
8002
8350
|
} else {
|
|
8003
|
-
console.log(
|
|
8351
|
+
console.log(chalk25.dim("Showing live events only. Use --history to include past.\n"));
|
|
8004
8352
|
}
|
|
8005
8353
|
process.on("SIGINT", () => {
|
|
8006
8354
|
exitIdleMode();
|
|
@@ -8010,13 +8358,13 @@ async function startTail(options = {}) {
|
|
|
8010
8358
|
readline5.clearLine(process.stdout, 0);
|
|
8011
8359
|
readline5.cursorTo(process.stdout, 0);
|
|
8012
8360
|
}
|
|
8013
|
-
console.log(
|
|
8361
|
+
console.log(chalk25.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
8014
8362
|
process.exit(0);
|
|
8015
8363
|
});
|
|
8016
8364
|
const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
|
|
8017
8365
|
const req = http2.get(sseUrl, (res) => {
|
|
8018
8366
|
if (res.statusCode !== 200) {
|
|
8019
|
-
console.error(
|
|
8367
|
+
console.error(chalk25.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
8020
8368
|
process.exit(1);
|
|
8021
8369
|
}
|
|
8022
8370
|
if (canApprove) enterIdleMode();
|
|
@@ -8047,7 +8395,7 @@ async function startTail(options = {}) {
|
|
|
8047
8395
|
readline5.clearLine(process.stdout, 0);
|
|
8048
8396
|
readline5.cursorTo(process.stdout, 0);
|
|
8049
8397
|
}
|
|
8050
|
-
console.log(
|
|
8398
|
+
console.log(chalk25.red("\n\u274C Daemon disconnected."));
|
|
8051
8399
|
process.exit(1);
|
|
8052
8400
|
});
|
|
8053
8401
|
});
|
|
@@ -8139,9 +8487,9 @@ async function startTail(options = {}) {
|
|
|
8139
8487
|
const hash = data.hash ?? "";
|
|
8140
8488
|
const summary = data.argsSummary ?? data.tool;
|
|
8141
8489
|
const fileCount = data.fileCount ?? 0;
|
|
8142
|
-
const files = fileCount > 0 ?
|
|
8490
|
+
const files = fileCount > 0 ? chalk25.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
|
|
8143
8491
|
process.stdout.write(
|
|
8144
|
-
`${
|
|
8492
|
+
`${chalk25.dim(time)} ${chalk25.cyan("\u{1F4F8} snapshot")} ${chalk25.dim(hash)} ${summary}${files}
|
|
8145
8493
|
`
|
|
8146
8494
|
);
|
|
8147
8495
|
return;
|
|
@@ -8158,19 +8506,19 @@ async function startTail(options = {}) {
|
|
|
8158
8506
|
}
|
|
8159
8507
|
req.on("error", (err2) => {
|
|
8160
8508
|
const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
|
|
8161
|
-
console.error(
|
|
8509
|
+
console.error(chalk25.red(`
|
|
8162
8510
|
\u274C ${msg}`));
|
|
8163
8511
|
process.exit(1);
|
|
8164
8512
|
});
|
|
8165
8513
|
}
|
|
8166
|
-
var PID_FILE, ICONS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, pendingShownForId, pendingWrappedLines, DIVIDER;
|
|
8514
|
+
var PID_FILE, ICONS, MODEL_CONTEXT_LIMITS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, pendingShownForId, pendingWrappedLines, DIVIDER;
|
|
8167
8515
|
var init_tail = __esm({
|
|
8168
8516
|
"src/tui/tail.ts"() {
|
|
8169
8517
|
"use strict";
|
|
8170
8518
|
init_daemon2();
|
|
8171
8519
|
init_daemon();
|
|
8172
8520
|
init_core();
|
|
8173
|
-
PID_FILE =
|
|
8521
|
+
PID_FILE = path40.join(os33.homedir(), ".node9", "daemon.pid");
|
|
8174
8522
|
ICONS = {
|
|
8175
8523
|
bash: "\u{1F4BB}",
|
|
8176
8524
|
shell: "\u{1F4BB}",
|
|
@@ -8188,6 +8536,13 @@ var init_tail = __esm({
|
|
|
8188
8536
|
delete: "\u{1F5D1}\uFE0F",
|
|
8189
8537
|
web: "\u{1F310}"
|
|
8190
8538
|
};
|
|
8539
|
+
MODEL_CONTEXT_LIMITS = {
|
|
8540
|
+
"claude-opus-4": 2e5,
|
|
8541
|
+
"claude-sonnet-4": 2e5,
|
|
8542
|
+
"claude-haiku-4": 2e5,
|
|
8543
|
+
"claude-3-7": 2e5,
|
|
8544
|
+
"claude-3-5": 2e5
|
|
8545
|
+
};
|
|
8191
8546
|
RESET2 = "\x1B[0m";
|
|
8192
8547
|
BOLD2 = "\x1B[1m";
|
|
8193
8548
|
RED = "\x1B[31m";
|
|
@@ -8211,9 +8566,9 @@ __export(hud_exports, {
|
|
|
8211
8566
|
main: () => main,
|
|
8212
8567
|
renderEnvironmentLine: () => renderEnvironmentLine
|
|
8213
8568
|
});
|
|
8214
|
-
import
|
|
8215
|
-
import
|
|
8216
|
-
import
|
|
8569
|
+
import fs38 from "fs";
|
|
8570
|
+
import path41 from "path";
|
|
8571
|
+
import os34 from "os";
|
|
8217
8572
|
import http3 from "http";
|
|
8218
8573
|
async function readStdin() {
|
|
8219
8574
|
const chunks = [];
|
|
@@ -8289,9 +8644,9 @@ function formatTimeLeft(resetsAt) {
|
|
|
8289
8644
|
return ` (${m}m left)`;
|
|
8290
8645
|
}
|
|
8291
8646
|
function safeReadJson(filePath) {
|
|
8292
|
-
if (!
|
|
8647
|
+
if (!fs38.existsSync(filePath)) return null;
|
|
8293
8648
|
try {
|
|
8294
|
-
return JSON.parse(
|
|
8649
|
+
return JSON.parse(fs38.readFileSync(filePath, "utf-8"));
|
|
8295
8650
|
} catch {
|
|
8296
8651
|
return null;
|
|
8297
8652
|
}
|
|
@@ -8312,12 +8667,12 @@ function countHooksInFile(filePath) {
|
|
|
8312
8667
|
return Object.keys(cfg.hooks).length;
|
|
8313
8668
|
}
|
|
8314
8669
|
function countRulesInDir(rulesDir) {
|
|
8315
|
-
if (!
|
|
8670
|
+
if (!fs38.existsSync(rulesDir)) return 0;
|
|
8316
8671
|
let count = 0;
|
|
8317
8672
|
try {
|
|
8318
|
-
for (const entry of
|
|
8673
|
+
for (const entry of fs38.readdirSync(rulesDir, { withFileTypes: true })) {
|
|
8319
8674
|
if (entry.isDirectory()) {
|
|
8320
|
-
count += countRulesInDir(
|
|
8675
|
+
count += countRulesInDir(path41.join(rulesDir, entry.name));
|
|
8321
8676
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
8322
8677
|
count++;
|
|
8323
8678
|
}
|
|
@@ -8328,46 +8683,46 @@ function countRulesInDir(rulesDir) {
|
|
|
8328
8683
|
}
|
|
8329
8684
|
function isSamePath(a, b) {
|
|
8330
8685
|
try {
|
|
8331
|
-
return
|
|
8686
|
+
return path41.resolve(a) === path41.resolve(b);
|
|
8332
8687
|
} catch {
|
|
8333
8688
|
return false;
|
|
8334
8689
|
}
|
|
8335
8690
|
}
|
|
8336
8691
|
function countConfigs(cwd) {
|
|
8337
|
-
const homeDir2 =
|
|
8338
|
-
const claudeDir =
|
|
8692
|
+
const homeDir2 = os34.homedir();
|
|
8693
|
+
const claudeDir = path41.join(homeDir2, ".claude");
|
|
8339
8694
|
let claudeMdCount = 0;
|
|
8340
8695
|
let rulesCount = 0;
|
|
8341
8696
|
let hooksCount = 0;
|
|
8342
8697
|
const userMcpServers = /* @__PURE__ */ new Set();
|
|
8343
8698
|
const projectMcpServers = /* @__PURE__ */ new Set();
|
|
8344
|
-
if (
|
|
8345
|
-
rulesCount += countRulesInDir(
|
|
8346
|
-
const userSettings =
|
|
8699
|
+
if (fs38.existsSync(path41.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
8700
|
+
rulesCount += countRulesInDir(path41.join(claudeDir, "rules"));
|
|
8701
|
+
const userSettings = path41.join(claudeDir, "settings.json");
|
|
8347
8702
|
for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
|
|
8348
8703
|
hooksCount += countHooksInFile(userSettings);
|
|
8349
|
-
const userClaudeJson =
|
|
8704
|
+
const userClaudeJson = path41.join(homeDir2, ".claude.json");
|
|
8350
8705
|
for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
|
|
8351
8706
|
for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
|
|
8352
8707
|
userMcpServers.delete(name);
|
|
8353
8708
|
}
|
|
8354
8709
|
if (cwd) {
|
|
8355
|
-
if (
|
|
8356
|
-
if (
|
|
8357
|
-
const projectClaudeDir =
|
|
8710
|
+
if (fs38.existsSync(path41.join(cwd, "CLAUDE.md"))) claudeMdCount++;
|
|
8711
|
+
if (fs38.existsSync(path41.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
|
|
8712
|
+
const projectClaudeDir = path41.join(cwd, ".claude");
|
|
8358
8713
|
const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
|
|
8359
8714
|
if (!overlapsUserScope) {
|
|
8360
|
-
if (
|
|
8361
|
-
rulesCount += countRulesInDir(
|
|
8362
|
-
const projSettings =
|
|
8715
|
+
if (fs38.existsSync(path41.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
8716
|
+
rulesCount += countRulesInDir(path41.join(projectClaudeDir, "rules"));
|
|
8717
|
+
const projSettings = path41.join(projectClaudeDir, "settings.json");
|
|
8363
8718
|
for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
|
|
8364
8719
|
hooksCount += countHooksInFile(projSettings);
|
|
8365
8720
|
}
|
|
8366
|
-
if (
|
|
8367
|
-
const localSettings =
|
|
8721
|
+
if (fs38.existsSync(path41.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
|
|
8722
|
+
const localSettings = path41.join(projectClaudeDir, "settings.local.json");
|
|
8368
8723
|
for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
|
|
8369
8724
|
hooksCount += countHooksInFile(localSettings);
|
|
8370
|
-
const mcpJsonServers = getMcpServerNames(
|
|
8725
|
+
const mcpJsonServers = getMcpServerNames(path41.join(cwd, ".mcp.json"));
|
|
8371
8726
|
const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
|
|
8372
8727
|
for (const name of disabledMcpJson) mcpJsonServers.delete(name);
|
|
8373
8728
|
for (const name of mcpJsonServers) projectMcpServers.add(name);
|
|
@@ -8400,12 +8755,12 @@ function readActiveShieldsHud() {
|
|
|
8400
8755
|
return shieldsCache.value;
|
|
8401
8756
|
}
|
|
8402
8757
|
try {
|
|
8403
|
-
const shieldsPath =
|
|
8404
|
-
if (!
|
|
8758
|
+
const shieldsPath = path41.join(os34.homedir(), ".node9", "shields.json");
|
|
8759
|
+
if (!fs38.existsSync(shieldsPath)) {
|
|
8405
8760
|
shieldsCache = { value: [], ts: now };
|
|
8406
8761
|
return [];
|
|
8407
8762
|
}
|
|
8408
|
-
const parsed = JSON.parse(
|
|
8763
|
+
const parsed = JSON.parse(fs38.readFileSync(shieldsPath, "utf-8"));
|
|
8409
8764
|
if (!Array.isArray(parsed.active)) {
|
|
8410
8765
|
shieldsCache = { value: [], ts: now };
|
|
8411
8766
|
return [];
|
|
@@ -8507,17 +8862,17 @@ function renderContextLine(stdin) {
|
|
|
8507
8862
|
async function main() {
|
|
8508
8863
|
try {
|
|
8509
8864
|
const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
|
|
8510
|
-
if (
|
|
8865
|
+
if (fs38.existsSync(path41.join(os34.homedir(), ".node9", "hud-debug"))) {
|
|
8511
8866
|
try {
|
|
8512
|
-
const logPath =
|
|
8867
|
+
const logPath = path41.join(os34.homedir(), ".node9", "hud-debug.log");
|
|
8513
8868
|
const MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
8514
8869
|
let size = 0;
|
|
8515
8870
|
try {
|
|
8516
|
-
size =
|
|
8871
|
+
size = fs38.statSync(logPath).size;
|
|
8517
8872
|
} catch {
|
|
8518
8873
|
}
|
|
8519
8874
|
if (size < MAX_LOG_SIZE) {
|
|
8520
|
-
|
|
8875
|
+
fs38.appendFileSync(
|
|
8521
8876
|
logPath,
|
|
8522
8877
|
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
|
|
8523
8878
|
);
|
|
@@ -8538,11 +8893,11 @@ async function main() {
|
|
|
8538
8893
|
try {
|
|
8539
8894
|
const cwd = stdin.cwd ?? process.cwd();
|
|
8540
8895
|
for (const configPath of [
|
|
8541
|
-
|
|
8542
|
-
|
|
8896
|
+
path41.join(cwd, "node9.config.json"),
|
|
8897
|
+
path41.join(os34.homedir(), ".node9", "config.json")
|
|
8543
8898
|
]) {
|
|
8544
|
-
if (!
|
|
8545
|
-
const cfg = JSON.parse(
|
|
8899
|
+
if (!fs38.existsSync(configPath)) continue;
|
|
8900
|
+
const cfg = JSON.parse(fs38.readFileSync(configPath, "utf-8"));
|
|
8546
8901
|
const hud = cfg.settings?.hud;
|
|
8547
8902
|
if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
|
|
8548
8903
|
}
|
|
@@ -9477,10 +9832,10 @@ function getAgentsStatus(homeDir2 = os11.homedir()) {
|
|
|
9477
9832
|
|
|
9478
9833
|
// src/cli.ts
|
|
9479
9834
|
init_daemon2();
|
|
9480
|
-
import
|
|
9481
|
-
import
|
|
9482
|
-
import
|
|
9483
|
-
import
|
|
9835
|
+
import chalk26 from "chalk";
|
|
9836
|
+
import fs39 from "fs";
|
|
9837
|
+
import path42 from "path";
|
|
9838
|
+
import os35 from "os";
|
|
9484
9839
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
9485
9840
|
|
|
9486
9841
|
// src/utils/duration.ts
|
|
@@ -9709,19 +10064,19 @@ init_daemon();
|
|
|
9709
10064
|
init_config();
|
|
9710
10065
|
init_policy();
|
|
9711
10066
|
import chalk5 from "chalk";
|
|
9712
|
-
import
|
|
10067
|
+
import fs24 from "fs";
|
|
9713
10068
|
import { spawn as spawn6 } from "child_process";
|
|
9714
|
-
import
|
|
9715
|
-
import
|
|
10069
|
+
import path26 from "path";
|
|
10070
|
+
import os20 from "os";
|
|
9716
10071
|
|
|
9717
10072
|
// src/undo.ts
|
|
9718
10073
|
import { spawnSync as spawnSync5, spawn as spawn5 } from "child_process";
|
|
9719
10074
|
import crypto3 from "crypto";
|
|
9720
|
-
import
|
|
10075
|
+
import fs22 from "fs";
|
|
9721
10076
|
import net3 from "net";
|
|
9722
|
-
import
|
|
9723
|
-
import
|
|
9724
|
-
var ACTIVITY_SOCKET_PATH3 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
10077
|
+
import path24 from "path";
|
|
10078
|
+
import os18 from "os";
|
|
10079
|
+
var ACTIVITY_SOCKET_PATH3 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path24.join(os18.tmpdir(), "node9-activity.sock");
|
|
9725
10080
|
function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
|
|
9726
10081
|
try {
|
|
9727
10082
|
const payload = JSON.stringify({
|
|
@@ -9741,22 +10096,22 @@ function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
|
|
|
9741
10096
|
} catch {
|
|
9742
10097
|
}
|
|
9743
10098
|
}
|
|
9744
|
-
var SNAPSHOT_STACK_PATH =
|
|
9745
|
-
var UNDO_LATEST_PATH =
|
|
10099
|
+
var SNAPSHOT_STACK_PATH = path24.join(os18.homedir(), ".node9", "snapshots.json");
|
|
10100
|
+
var UNDO_LATEST_PATH = path24.join(os18.homedir(), ".node9", "undo_latest.txt");
|
|
9746
10101
|
var MAX_SNAPSHOTS = 10;
|
|
9747
10102
|
var GIT_TIMEOUT = 15e3;
|
|
9748
10103
|
function readStack() {
|
|
9749
10104
|
try {
|
|
9750
|
-
if (
|
|
9751
|
-
return JSON.parse(
|
|
10105
|
+
if (fs22.existsSync(SNAPSHOT_STACK_PATH))
|
|
10106
|
+
return JSON.parse(fs22.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
|
|
9752
10107
|
} catch {
|
|
9753
10108
|
}
|
|
9754
10109
|
return [];
|
|
9755
10110
|
}
|
|
9756
10111
|
function writeStack(stack) {
|
|
9757
|
-
const dir =
|
|
9758
|
-
if (!
|
|
9759
|
-
|
|
10112
|
+
const dir = path24.dirname(SNAPSHOT_STACK_PATH);
|
|
10113
|
+
if (!fs22.existsSync(dir)) fs22.mkdirSync(dir, { recursive: true });
|
|
10114
|
+
fs22.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
9760
10115
|
}
|
|
9761
10116
|
function extractFilePath(args) {
|
|
9762
10117
|
if (!args || typeof args !== "object") return null;
|
|
@@ -9776,12 +10131,12 @@ function buildArgsSummary(tool, args) {
|
|
|
9776
10131
|
return "";
|
|
9777
10132
|
}
|
|
9778
10133
|
function findProjectRoot(filePath) {
|
|
9779
|
-
let dir =
|
|
10134
|
+
let dir = path24.dirname(filePath);
|
|
9780
10135
|
while (true) {
|
|
9781
|
-
if (
|
|
10136
|
+
if (fs22.existsSync(path24.join(dir, ".git")) || fs22.existsSync(path24.join(dir, "package.json"))) {
|
|
9782
10137
|
return dir;
|
|
9783
10138
|
}
|
|
9784
|
-
const parent =
|
|
10139
|
+
const parent = path24.dirname(dir);
|
|
9785
10140
|
if (parent === dir) return process.cwd();
|
|
9786
10141
|
dir = parent;
|
|
9787
10142
|
}
|
|
@@ -9789,7 +10144,7 @@ function findProjectRoot(filePath) {
|
|
|
9789
10144
|
function normalizeCwdForHash(cwd) {
|
|
9790
10145
|
let normalized;
|
|
9791
10146
|
try {
|
|
9792
|
-
normalized =
|
|
10147
|
+
normalized = fs22.realpathSync(cwd);
|
|
9793
10148
|
} catch {
|
|
9794
10149
|
normalized = cwd;
|
|
9795
10150
|
}
|
|
@@ -9799,16 +10154,16 @@ function normalizeCwdForHash(cwd) {
|
|
|
9799
10154
|
}
|
|
9800
10155
|
function getShadowRepoDir(cwd) {
|
|
9801
10156
|
const hash = crypto3.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
|
|
9802
|
-
return
|
|
10157
|
+
return path24.join(os18.homedir(), ".node9", "snapshots", hash);
|
|
9803
10158
|
}
|
|
9804
10159
|
function cleanOrphanedIndexFiles(shadowDir) {
|
|
9805
10160
|
try {
|
|
9806
10161
|
const cutoff = Date.now() - 6e4;
|
|
9807
|
-
for (const f of
|
|
10162
|
+
for (const f of fs22.readdirSync(shadowDir)) {
|
|
9808
10163
|
if (f.startsWith("index_")) {
|
|
9809
|
-
const fp =
|
|
10164
|
+
const fp = path24.join(shadowDir, f);
|
|
9810
10165
|
try {
|
|
9811
|
-
if (
|
|
10166
|
+
if (fs22.statSync(fp).mtimeMs < cutoff) fs22.unlinkSync(fp);
|
|
9812
10167
|
} catch {
|
|
9813
10168
|
}
|
|
9814
10169
|
}
|
|
@@ -9820,7 +10175,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
|
|
|
9820
10175
|
const hardcoded = [".git", ".node9"];
|
|
9821
10176
|
const lines = [...hardcoded, ...ignorePaths].join("\n");
|
|
9822
10177
|
try {
|
|
9823
|
-
|
|
10178
|
+
fs22.writeFileSync(path24.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
|
|
9824
10179
|
} catch {
|
|
9825
10180
|
}
|
|
9826
10181
|
}
|
|
@@ -9833,25 +10188,25 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
9833
10188
|
timeout: 3e3
|
|
9834
10189
|
});
|
|
9835
10190
|
if (check.status === 0) {
|
|
9836
|
-
const ptPath =
|
|
10191
|
+
const ptPath = path24.join(shadowDir, "project-path.txt");
|
|
9837
10192
|
try {
|
|
9838
|
-
const stored =
|
|
10193
|
+
const stored = fs22.readFileSync(ptPath, "utf8").trim();
|
|
9839
10194
|
if (stored === normalizedCwd) return true;
|
|
9840
10195
|
if (process.env.NODE9_DEBUG === "1")
|
|
9841
10196
|
console.error(
|
|
9842
10197
|
`[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
|
|
9843
10198
|
);
|
|
9844
|
-
|
|
10199
|
+
fs22.rmSync(shadowDir, { recursive: true, force: true });
|
|
9845
10200
|
} catch {
|
|
9846
10201
|
try {
|
|
9847
|
-
|
|
10202
|
+
fs22.writeFileSync(ptPath, normalizedCwd, "utf8");
|
|
9848
10203
|
} catch {
|
|
9849
10204
|
}
|
|
9850
10205
|
return true;
|
|
9851
10206
|
}
|
|
9852
10207
|
}
|
|
9853
10208
|
try {
|
|
9854
|
-
|
|
10209
|
+
fs22.mkdirSync(shadowDir, { recursive: true });
|
|
9855
10210
|
} catch {
|
|
9856
10211
|
}
|
|
9857
10212
|
const init = spawnSync5("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
|
|
@@ -9860,7 +10215,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
9860
10215
|
if (process.env.NODE9_DEBUG === "1") console.error("[Node9] git init --bare failed:", reason);
|
|
9861
10216
|
return false;
|
|
9862
10217
|
}
|
|
9863
|
-
const configFile =
|
|
10218
|
+
const configFile = path24.join(shadowDir, "config");
|
|
9864
10219
|
spawnSync5("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
|
|
9865
10220
|
timeout: 3e3
|
|
9866
10221
|
});
|
|
@@ -9868,7 +10223,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
9868
10223
|
timeout: 3e3
|
|
9869
10224
|
});
|
|
9870
10225
|
try {
|
|
9871
|
-
|
|
10226
|
+
fs22.writeFileSync(path24.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
|
|
9872
10227
|
} catch {
|
|
9873
10228
|
}
|
|
9874
10229
|
return true;
|
|
@@ -9888,12 +10243,12 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
9888
10243
|
let indexFile = null;
|
|
9889
10244
|
try {
|
|
9890
10245
|
const rawFilePath = extractFilePath(args);
|
|
9891
|
-
const absFilePath = rawFilePath &&
|
|
10246
|
+
const absFilePath = rawFilePath && path24.isAbsolute(rawFilePath) ? rawFilePath : null;
|
|
9892
10247
|
const cwd = absFilePath ? findProjectRoot(absFilePath) : process.cwd();
|
|
9893
10248
|
const shadowDir = getShadowRepoDir(cwd);
|
|
9894
10249
|
if (!ensureShadowRepo(shadowDir, cwd)) return null;
|
|
9895
10250
|
writeShadowExcludes(shadowDir, ignorePaths);
|
|
9896
|
-
indexFile =
|
|
10251
|
+
indexFile = path24.join(shadowDir, `index_${process.pid}_${Date.now()}`);
|
|
9897
10252
|
const shadowEnv = {
|
|
9898
10253
|
...process.env,
|
|
9899
10254
|
GIT_DIR: shadowDir,
|
|
@@ -9965,7 +10320,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
9965
10320
|
writeStack(stack);
|
|
9966
10321
|
const entry = stack[stack.length - 1];
|
|
9967
10322
|
notifySnapshotTaken(commitHash.slice(0, 7), tool, entry.argsSummary, capturedFiles.length);
|
|
9968
|
-
|
|
10323
|
+
fs22.writeFileSync(UNDO_LATEST_PATH, commitHash);
|
|
9969
10324
|
if (shouldGc) {
|
|
9970
10325
|
spawn5("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
|
|
9971
10326
|
}
|
|
@@ -9976,7 +10331,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
9976
10331
|
} finally {
|
|
9977
10332
|
if (indexFile) {
|
|
9978
10333
|
try {
|
|
9979
|
-
|
|
10334
|
+
fs22.unlinkSync(indexFile);
|
|
9980
10335
|
} catch {
|
|
9981
10336
|
}
|
|
9982
10337
|
}
|
|
@@ -10052,9 +10407,9 @@ function applyUndo(hash, cwd) {
|
|
|
10052
10407
|
timeout: GIT_TIMEOUT
|
|
10053
10408
|
}).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
10054
10409
|
for (const file of [...tracked, ...untracked]) {
|
|
10055
|
-
const fullPath =
|
|
10056
|
-
if (!snapshotFiles.has(file) &&
|
|
10057
|
-
|
|
10410
|
+
const fullPath = path24.join(dir, file);
|
|
10411
|
+
if (!snapshotFiles.has(file) && fs22.existsSync(fullPath)) {
|
|
10412
|
+
fs22.unlinkSync(fullPath);
|
|
10058
10413
|
}
|
|
10059
10414
|
}
|
|
10060
10415
|
return true;
|
|
@@ -10063,6 +10418,187 @@ function applyUndo(hash, cwd) {
|
|
|
10063
10418
|
}
|
|
10064
10419
|
}
|
|
10065
10420
|
|
|
10421
|
+
// src/skill-pin.ts
|
|
10422
|
+
import fs23 from "fs";
|
|
10423
|
+
import path25 from "path";
|
|
10424
|
+
import os19 from "os";
|
|
10425
|
+
import crypto4 from "crypto";
|
|
10426
|
+
function getPinsFilePath() {
|
|
10427
|
+
return path25.join(os19.homedir(), ".node9", "skill-pins.json");
|
|
10428
|
+
}
|
|
10429
|
+
var MAX_FILES = 5e3;
|
|
10430
|
+
var MAX_TOTAL_BYTES = 50 * 1024 * 1024;
|
|
10431
|
+
function sha256Bytes(buf) {
|
|
10432
|
+
return crypto4.createHash("sha256").update(buf).digest("hex");
|
|
10433
|
+
}
|
|
10434
|
+
function walkDir(root) {
|
|
10435
|
+
const out = [];
|
|
10436
|
+
let totalBytes = 0;
|
|
10437
|
+
const visit = (dir, relDir) => {
|
|
10438
|
+
if (out.length >= MAX_FILES) return;
|
|
10439
|
+
let entries;
|
|
10440
|
+
try {
|
|
10441
|
+
entries = fs23.readdirSync(dir, { withFileTypes: true });
|
|
10442
|
+
} catch {
|
|
10443
|
+
return;
|
|
10444
|
+
}
|
|
10445
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
10446
|
+
for (const entry of entries) {
|
|
10447
|
+
if (out.length >= MAX_FILES) return;
|
|
10448
|
+
const full = path25.join(dir, entry.name);
|
|
10449
|
+
const rel = relDir ? path25.posix.join(relDir, entry.name) : entry.name;
|
|
10450
|
+
let lst;
|
|
10451
|
+
try {
|
|
10452
|
+
lst = fs23.lstatSync(full);
|
|
10453
|
+
} catch {
|
|
10454
|
+
continue;
|
|
10455
|
+
}
|
|
10456
|
+
if (lst.isSymbolicLink()) continue;
|
|
10457
|
+
if (lst.isDirectory()) {
|
|
10458
|
+
visit(full, rel);
|
|
10459
|
+
continue;
|
|
10460
|
+
}
|
|
10461
|
+
if (!lst.isFile()) continue;
|
|
10462
|
+
if (totalBytes + lst.size > MAX_TOTAL_BYTES) continue;
|
|
10463
|
+
try {
|
|
10464
|
+
const buf = fs23.readFileSync(full);
|
|
10465
|
+
totalBytes += buf.length;
|
|
10466
|
+
out.push({ rel, hash: sha256Bytes(buf) });
|
|
10467
|
+
} catch {
|
|
10468
|
+
}
|
|
10469
|
+
}
|
|
10470
|
+
};
|
|
10471
|
+
visit(root, "");
|
|
10472
|
+
out.sort((a, b) => a.rel.localeCompare(b.rel));
|
|
10473
|
+
return out.map((e) => `${e.rel}\0${e.hash}`);
|
|
10474
|
+
}
|
|
10475
|
+
function hashSkillRoot(absPath) {
|
|
10476
|
+
let lst;
|
|
10477
|
+
try {
|
|
10478
|
+
lst = fs23.lstatSync(absPath);
|
|
10479
|
+
} catch {
|
|
10480
|
+
return { exists: false, contentHash: "", fileCount: 0 };
|
|
10481
|
+
}
|
|
10482
|
+
if (lst.isSymbolicLink()) return { exists: false, contentHash: "", fileCount: 0 };
|
|
10483
|
+
if (lst.isFile()) {
|
|
10484
|
+
try {
|
|
10485
|
+
return { exists: true, contentHash: sha256Bytes(fs23.readFileSync(absPath)), fileCount: 1 };
|
|
10486
|
+
} catch {
|
|
10487
|
+
return { exists: false, contentHash: "", fileCount: 0 };
|
|
10488
|
+
}
|
|
10489
|
+
}
|
|
10490
|
+
if (lst.isDirectory()) {
|
|
10491
|
+
const entries = walkDir(absPath);
|
|
10492
|
+
const contentHash = crypto4.createHash("sha256").update(entries.join("\n")).digest("hex");
|
|
10493
|
+
return { exists: true, contentHash, fileCount: entries.length };
|
|
10494
|
+
}
|
|
10495
|
+
return { exists: false, contentHash: "", fileCount: 0 };
|
|
10496
|
+
}
|
|
10497
|
+
function getRootKey(absPath) {
|
|
10498
|
+
return crypto4.createHash("sha256").update(absPath).digest("hex").slice(0, 16);
|
|
10499
|
+
}
|
|
10500
|
+
function readSkillPinsSafe() {
|
|
10501
|
+
const filePath = getPinsFilePath();
|
|
10502
|
+
try {
|
|
10503
|
+
const raw = fs23.readFileSync(filePath, "utf-8");
|
|
10504
|
+
if (!raw.trim()) return { ok: false, reason: "corrupt", detail: "empty file" };
|
|
10505
|
+
const parsed = JSON.parse(raw);
|
|
10506
|
+
if (!parsed.roots || typeof parsed.roots !== "object" || Array.isArray(parsed.roots)) {
|
|
10507
|
+
return { ok: false, reason: "corrupt", detail: "invalid structure: missing roots object" };
|
|
10508
|
+
}
|
|
10509
|
+
return { ok: true, pins: { roots: parsed.roots } };
|
|
10510
|
+
} catch (err2) {
|
|
10511
|
+
if (err2.code === "ENOENT") return { ok: false, reason: "missing" };
|
|
10512
|
+
return { ok: false, reason: "corrupt", detail: String(err2) };
|
|
10513
|
+
}
|
|
10514
|
+
}
|
|
10515
|
+
function readSkillPins() {
|
|
10516
|
+
const result = readSkillPinsSafe();
|
|
10517
|
+
if (result.ok) return result.pins;
|
|
10518
|
+
if (result.reason === "missing") return { roots: {} };
|
|
10519
|
+
throw new Error(`[node9] skill pin file is corrupt: ${result.detail}`);
|
|
10520
|
+
}
|
|
10521
|
+
function writeSkillPins(data) {
|
|
10522
|
+
const filePath = getPinsFilePath();
|
|
10523
|
+
fs23.mkdirSync(path25.dirname(filePath), { recursive: true });
|
|
10524
|
+
const tmp = `${filePath}.${crypto4.randomBytes(6).toString("hex")}.tmp`;
|
|
10525
|
+
fs23.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
|
|
10526
|
+
fs23.renameSync(tmp, filePath);
|
|
10527
|
+
}
|
|
10528
|
+
function removePin(rootKey) {
|
|
10529
|
+
const pins = readSkillPins();
|
|
10530
|
+
delete pins.roots[rootKey];
|
|
10531
|
+
writeSkillPins(pins);
|
|
10532
|
+
}
|
|
10533
|
+
function clearAllPins() {
|
|
10534
|
+
writeSkillPins({ roots: {} });
|
|
10535
|
+
}
|
|
10536
|
+
function verifyAndPinRoots(roots) {
|
|
10537
|
+
const pinsRead = readSkillPinsSafe();
|
|
10538
|
+
if (!pinsRead.ok && pinsRead.reason === "corrupt") {
|
|
10539
|
+
return { kind: "corrupt", detail: pinsRead.detail };
|
|
10540
|
+
}
|
|
10541
|
+
const pins = pinsRead.ok ? pinsRead.pins : { roots: {} };
|
|
10542
|
+
let mutated = false;
|
|
10543
|
+
for (const rootPath of new Set(roots)) {
|
|
10544
|
+
const rootKey = getRootKey(rootPath);
|
|
10545
|
+
const current = hashSkillRoot(rootPath);
|
|
10546
|
+
const existing = pins.roots[rootKey];
|
|
10547
|
+
if (!existing) {
|
|
10548
|
+
pins.roots[rootKey] = {
|
|
10549
|
+
rootPath,
|
|
10550
|
+
exists: current.exists,
|
|
10551
|
+
contentHash: current.contentHash,
|
|
10552
|
+
fileCount: current.fileCount,
|
|
10553
|
+
pinnedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10554
|
+
};
|
|
10555
|
+
mutated = true;
|
|
10556
|
+
continue;
|
|
10557
|
+
}
|
|
10558
|
+
if (existing.exists !== current.exists || existing.contentHash !== current.contentHash) {
|
|
10559
|
+
let summary;
|
|
10560
|
+
if (existing.exists && !current.exists) summary = `vanished: ${rootPath}`;
|
|
10561
|
+
else if (!existing.exists && current.exists) summary = `appeared: ${rootPath}`;
|
|
10562
|
+
else summary = `changed: ${rootPath}`;
|
|
10563
|
+
return { kind: "drift", changedRootKey: rootKey, changedRootPath: rootPath, summary };
|
|
10564
|
+
}
|
|
10565
|
+
}
|
|
10566
|
+
if (mutated) writeSkillPins(pins);
|
|
10567
|
+
return { kind: "verified" };
|
|
10568
|
+
}
|
|
10569
|
+
function defaultSkillRoots(_cwd) {
|
|
10570
|
+
const marketplaces = path25.join(os19.homedir(), ".claude", "plugins", "marketplaces");
|
|
10571
|
+
const roots = [];
|
|
10572
|
+
let registries;
|
|
10573
|
+
try {
|
|
10574
|
+
registries = fs23.readdirSync(marketplaces, { withFileTypes: true });
|
|
10575
|
+
} catch {
|
|
10576
|
+
return [];
|
|
10577
|
+
}
|
|
10578
|
+
for (const registry of registries) {
|
|
10579
|
+
if (!registry.isDirectory()) continue;
|
|
10580
|
+
const pluginsDir = path25.join(marketplaces, registry.name, "plugins");
|
|
10581
|
+
let plugins;
|
|
10582
|
+
try {
|
|
10583
|
+
plugins = fs23.readdirSync(pluginsDir, { withFileTypes: true });
|
|
10584
|
+
} catch {
|
|
10585
|
+
continue;
|
|
10586
|
+
}
|
|
10587
|
+
for (const plugin of plugins) {
|
|
10588
|
+
if (!plugin.isDirectory()) continue;
|
|
10589
|
+
roots.push(path25.join(pluginsDir, plugin.name));
|
|
10590
|
+
}
|
|
10591
|
+
}
|
|
10592
|
+
return roots;
|
|
10593
|
+
}
|
|
10594
|
+
function resolveUserSkillRoot(entry, cwd) {
|
|
10595
|
+
if (!entry) return null;
|
|
10596
|
+
if (entry.startsWith("~/") || entry === "~") return path25.join(os19.homedir(), entry.slice(1));
|
|
10597
|
+
if (path25.isAbsolute(entry)) return entry;
|
|
10598
|
+
if (!cwd || !path25.isAbsolute(cwd)) return null;
|
|
10599
|
+
return path25.join(cwd, entry);
|
|
10600
|
+
}
|
|
10601
|
+
|
|
10066
10602
|
// src/cli/commands/check.ts
|
|
10067
10603
|
function sanitize2(value) {
|
|
10068
10604
|
return value.replace(/[\x00-\x1F\x7F]/g, "");
|
|
@@ -10078,9 +10614,9 @@ function registerCheckCommand(program2) {
|
|
|
10078
10614
|
} catch (err2) {
|
|
10079
10615
|
const tempConfig = getConfig();
|
|
10080
10616
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
10081
|
-
const logPath =
|
|
10617
|
+
const logPath = path26.join(os20.homedir(), ".node9", "hook-debug.log");
|
|
10082
10618
|
const errMsg = err2 instanceof Error ? err2.message : String(err2);
|
|
10083
|
-
|
|
10619
|
+
fs24.appendFileSync(
|
|
10084
10620
|
logPath,
|
|
10085
10621
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
10086
10622
|
RAW: ${raw}
|
|
@@ -10093,11 +10629,11 @@ RAW: ${raw}
|
|
|
10093
10629
|
if (config.settings.autoStartDaemon && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON) {
|
|
10094
10630
|
try {
|
|
10095
10631
|
const scriptPath = process.argv[1];
|
|
10096
|
-
if (typeof scriptPath !== "string" || !
|
|
10632
|
+
if (typeof scriptPath !== "string" || !path26.isAbsolute(scriptPath))
|
|
10097
10633
|
throw new Error("node9: argv[1] is not an absolute path");
|
|
10098
|
-
const resolvedScript =
|
|
10099
|
-
const packageDist =
|
|
10100
|
-
if (!resolvedScript.startsWith(packageDist +
|
|
10634
|
+
const resolvedScript = fs24.realpathSync(scriptPath);
|
|
10635
|
+
const packageDist = fs24.realpathSync(path26.resolve(__dirname, "../.."));
|
|
10636
|
+
if (!resolvedScript.startsWith(packageDist + path26.sep) && resolvedScript !== packageDist)
|
|
10101
10637
|
throw new Error(
|
|
10102
10638
|
`node9: daemon spawn aborted \u2014 argv[1] (${resolvedScript}) is outside package dist (${packageDist})`
|
|
10103
10639
|
);
|
|
@@ -10119,10 +10655,10 @@ RAW: ${raw}
|
|
|
10119
10655
|
});
|
|
10120
10656
|
d.unref();
|
|
10121
10657
|
} catch (spawnErr) {
|
|
10122
|
-
const logPath =
|
|
10658
|
+
const logPath = path26.join(os20.homedir(), ".node9", "hook-debug.log");
|
|
10123
10659
|
const msg = spawnErr instanceof Error ? spawnErr.message : String(spawnErr);
|
|
10124
10660
|
try {
|
|
10125
|
-
|
|
10661
|
+
fs24.appendFileSync(
|
|
10126
10662
|
logPath,
|
|
10127
10663
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon-autostart-failed: ${msg}
|
|
10128
10664
|
`
|
|
@@ -10132,10 +10668,10 @@ RAW: ${raw}
|
|
|
10132
10668
|
}
|
|
10133
10669
|
}
|
|
10134
10670
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
10135
|
-
const logPath =
|
|
10136
|
-
if (!
|
|
10137
|
-
|
|
10138
|
-
|
|
10671
|
+
const logPath = path26.join(os20.homedir(), ".node9", "hook-debug.log");
|
|
10672
|
+
if (!fs24.existsSync(path26.dirname(logPath)))
|
|
10673
|
+
fs24.mkdirSync(path26.dirname(logPath), { recursive: true });
|
|
10674
|
+
fs24.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
10139
10675
|
`);
|
|
10140
10676
|
}
|
|
10141
10677
|
const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
|
|
@@ -10148,8 +10684,8 @@ RAW: ${raw}
|
|
|
10148
10684
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
10149
10685
|
let ttyFd = null;
|
|
10150
10686
|
try {
|
|
10151
|
-
ttyFd =
|
|
10152
|
-
const writeTty = (line) =>
|
|
10687
|
+
ttyFd = fs24.openSync("/dev/tty", "w");
|
|
10688
|
+
const writeTty = (line) => fs24.writeSync(ttyFd, line + "\n");
|
|
10153
10689
|
if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
|
|
10154
10690
|
writeTty(chalk5.bgRed.white.bold(`
|
|
10155
10691
|
\u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
@@ -10168,7 +10704,7 @@ RAW: ${raw}
|
|
|
10168
10704
|
} finally {
|
|
10169
10705
|
if (ttyFd !== null)
|
|
10170
10706
|
try {
|
|
10171
|
-
|
|
10707
|
+
fs24.closeSync(ttyFd);
|
|
10172
10708
|
} catch {
|
|
10173
10709
|
}
|
|
10174
10710
|
}
|
|
@@ -10197,10 +10733,131 @@ RAW: ${raw}
|
|
|
10197
10733
|
return;
|
|
10198
10734
|
}
|
|
10199
10735
|
const meta = { agent, mcpServer };
|
|
10736
|
+
const skillPinCfg = config.policy.skillPinning;
|
|
10737
|
+
const rawSessionId = typeof payload.session_id === "string" ? payload.session_id : "";
|
|
10738
|
+
const safeSessionId = /^[A-Za-z0-9_\-]{1,128}$/.test(rawSessionId) ? rawSessionId : "";
|
|
10739
|
+
if (skillPinCfg.enabled && safeSessionId) {
|
|
10740
|
+
try {
|
|
10741
|
+
const sessionsDir = path26.join(os20.homedir(), ".node9", "skill-sessions");
|
|
10742
|
+
const flagPath = path26.join(sessionsDir, `${safeSessionId}.json`);
|
|
10743
|
+
let flag = null;
|
|
10744
|
+
try {
|
|
10745
|
+
flag = JSON.parse(fs24.readFileSync(flagPath, "utf-8"));
|
|
10746
|
+
} catch {
|
|
10747
|
+
}
|
|
10748
|
+
const writeFlag = (data2) => {
|
|
10749
|
+
try {
|
|
10750
|
+
fs24.mkdirSync(sessionsDir, { recursive: true });
|
|
10751
|
+
fs24.writeFileSync(
|
|
10752
|
+
flagPath,
|
|
10753
|
+
JSON.stringify({ ...data2, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
|
|
10754
|
+
{ mode: 384 }
|
|
10755
|
+
);
|
|
10756
|
+
} catch {
|
|
10757
|
+
}
|
|
10758
|
+
};
|
|
10759
|
+
const sendSkillWarn = (detail, recoveryCmd) => {
|
|
10760
|
+
let ttyFd = null;
|
|
10761
|
+
try {
|
|
10762
|
+
ttyFd = fs24.openSync("/dev/tty", "w");
|
|
10763
|
+
const w = (line) => fs24.writeSync(ttyFd, line + "\n");
|
|
10764
|
+
w(chalk5.yellow(`
|
|
10765
|
+
\u26A0\uFE0F Node9: installed skill drift detected`));
|
|
10766
|
+
w(chalk5.gray(` ${detail}`));
|
|
10767
|
+
w(
|
|
10768
|
+
chalk5.gray(
|
|
10769
|
+
` If you updated a plugin, acknowledge the change to clear this warning.`
|
|
10770
|
+
)
|
|
10771
|
+
);
|
|
10772
|
+
if (recoveryCmd) w(chalk5.green(` \u{1F4A1} Run: ${recoveryCmd}`));
|
|
10773
|
+
w("");
|
|
10774
|
+
} catch {
|
|
10775
|
+
} finally {
|
|
10776
|
+
if (ttyFd !== null)
|
|
10777
|
+
try {
|
|
10778
|
+
fs24.closeSync(ttyFd);
|
|
10779
|
+
} catch {
|
|
10780
|
+
}
|
|
10781
|
+
}
|
|
10782
|
+
};
|
|
10783
|
+
if (flag && flag.state === "quarantined" && skillPinCfg.mode === "block") {
|
|
10784
|
+
sendBlock(
|
|
10785
|
+
`Node9: session quarantined \u2014 installed skill changed. Open a separate terminal and run: node9 skill pin list (to see what changed) then: node9 skill pin update <rootKey> (to acknowledge). If you updated a plugin intentionally, this is expected.`,
|
|
10786
|
+
{
|
|
10787
|
+
blockedByLabel: "Skill Pin Quarantine",
|
|
10788
|
+
recoveryCommand: "node9 skill pin list"
|
|
10789
|
+
}
|
|
10790
|
+
);
|
|
10791
|
+
return;
|
|
10792
|
+
}
|
|
10793
|
+
if (!flag || flag.state !== "verified" && flag.state !== "warned") {
|
|
10794
|
+
const absoluteCwd = typeof payload.cwd === "string" && path26.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
10795
|
+
const extraRoots = skillPinCfg.roots;
|
|
10796
|
+
const resolvedExtra = extraRoots.map((r) => resolveUserSkillRoot(r, absoluteCwd)).filter((r) => typeof r === "string");
|
|
10797
|
+
const roots = [...defaultSkillRoots(absoluteCwd), ...resolvedExtra];
|
|
10798
|
+
const result2 = verifyAndPinRoots(roots);
|
|
10799
|
+
if (result2.kind === "corrupt") {
|
|
10800
|
+
if (skillPinCfg.mode === "block") {
|
|
10801
|
+
writeFlag({
|
|
10802
|
+
state: "quarantined",
|
|
10803
|
+
detail: `pin file corrupt: ${result2.detail}`
|
|
10804
|
+
});
|
|
10805
|
+
sendBlock("Node9: skill pin file is corrupt \u2014 fail-closed.", {
|
|
10806
|
+
blockedByLabel: "Skill Pin Quarantine",
|
|
10807
|
+
recoveryCommand: "node9 skill pin reset"
|
|
10808
|
+
});
|
|
10809
|
+
return;
|
|
10810
|
+
}
|
|
10811
|
+
writeFlag({ state: "warned", detail: `pin file corrupt: ${result2.detail}` });
|
|
10812
|
+
sendSkillWarn(
|
|
10813
|
+
`Skill pin file is corrupt: ${result2.detail}`,
|
|
10814
|
+
"node9 skill pin reset"
|
|
10815
|
+
);
|
|
10816
|
+
} else if (result2.kind === "drift") {
|
|
10817
|
+
if (skillPinCfg.mode === "block") {
|
|
10818
|
+
writeFlag({ state: "quarantined", detail: result2.summary });
|
|
10819
|
+
sendBlock(
|
|
10820
|
+
`Node9: installed skill changed \u2014 ${result2.summary}. If you updated a plugin, open a separate terminal and run: node9 skill pin update ${result2.changedRootKey}`,
|
|
10821
|
+
{
|
|
10822
|
+
blockedByLabel: "Skill Pin Quarantine",
|
|
10823
|
+
recoveryCommand: `node9 skill pin update ${result2.changedRootKey}`
|
|
10824
|
+
}
|
|
10825
|
+
);
|
|
10826
|
+
return;
|
|
10827
|
+
}
|
|
10828
|
+
writeFlag({ state: "warned", detail: result2.summary });
|
|
10829
|
+
sendSkillWarn(result2.summary, `node9 skill pin update ${result2.changedRootKey}`);
|
|
10830
|
+
} else {
|
|
10831
|
+
writeFlag({ state: "verified" });
|
|
10832
|
+
}
|
|
10833
|
+
try {
|
|
10834
|
+
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1e3;
|
|
10835
|
+
for (const name of fs24.readdirSync(sessionsDir)) {
|
|
10836
|
+
const p = path26.join(sessionsDir, name);
|
|
10837
|
+
try {
|
|
10838
|
+
if (fs24.statSync(p).mtimeMs < cutoff) fs24.unlinkSync(p);
|
|
10839
|
+
} catch {
|
|
10840
|
+
}
|
|
10841
|
+
}
|
|
10842
|
+
} catch {
|
|
10843
|
+
}
|
|
10844
|
+
}
|
|
10845
|
+
} catch (err2) {
|
|
10846
|
+
if (process.env.NODE9_DEBUG === "1") {
|
|
10847
|
+
try {
|
|
10848
|
+
const dbg = path26.join(os20.homedir(), ".node9", "hook-debug.log");
|
|
10849
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
10850
|
+
fs24.appendFileSync(dbg, `[${(/* @__PURE__ */ new Date()).toISOString()}] SKILL_PIN_ERROR: ${msg}
|
|
10851
|
+
`);
|
|
10852
|
+
} catch {
|
|
10853
|
+
}
|
|
10854
|
+
}
|
|
10855
|
+
}
|
|
10856
|
+
}
|
|
10200
10857
|
if (shouldSnapshot(toolName, toolInput, config)) {
|
|
10201
10858
|
await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
|
|
10202
10859
|
}
|
|
10203
|
-
const safeCwdForAuth = typeof payload.cwd === "string" &&
|
|
10860
|
+
const safeCwdForAuth = typeof payload.cwd === "string" && path26.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
10204
10861
|
const result = await authorizeHeadless(toolName, toolInput, meta, {
|
|
10205
10862
|
cwd: safeCwdForAuth
|
|
10206
10863
|
});
|
|
@@ -10212,12 +10869,12 @@ RAW: ${raw}
|
|
|
10212
10869
|
}
|
|
10213
10870
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
10214
10871
|
try {
|
|
10215
|
-
const tty =
|
|
10216
|
-
|
|
10872
|
+
const tty = fs24.openSync("/dev/tty", "w");
|
|
10873
|
+
fs24.writeSync(
|
|
10217
10874
|
tty,
|
|
10218
10875
|
chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
|
|
10219
10876
|
);
|
|
10220
|
-
|
|
10877
|
+
fs24.closeSync(tty);
|
|
10221
10878
|
} catch {
|
|
10222
10879
|
}
|
|
10223
10880
|
const daemonReady = await autoStartDaemonAndWait();
|
|
@@ -10244,9 +10901,9 @@ RAW: ${raw}
|
|
|
10244
10901
|
});
|
|
10245
10902
|
} catch (err2) {
|
|
10246
10903
|
if (process.env.NODE9_DEBUG === "1") {
|
|
10247
|
-
const logPath =
|
|
10904
|
+
const logPath = path26.join(os20.homedir(), ".node9", "hook-debug.log");
|
|
10248
10905
|
const errMsg = err2 instanceof Error ? err2.message : String(err2);
|
|
10249
|
-
|
|
10906
|
+
fs24.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
10250
10907
|
`);
|
|
10251
10908
|
}
|
|
10252
10909
|
process.exit(0);
|
|
@@ -10283,9 +10940,9 @@ RAW: ${raw}
|
|
|
10283
10940
|
init_audit();
|
|
10284
10941
|
init_config();
|
|
10285
10942
|
init_policy();
|
|
10286
|
-
import
|
|
10287
|
-
import
|
|
10288
|
-
import
|
|
10943
|
+
import fs25 from "fs";
|
|
10944
|
+
import path27 from "path";
|
|
10945
|
+
import os21 from "os";
|
|
10289
10946
|
init_daemon();
|
|
10290
10947
|
|
|
10291
10948
|
// src/utils/cp-mv-parser.ts
|
|
@@ -10358,10 +11015,10 @@ function registerLogCommand(program2) {
|
|
|
10358
11015
|
decision: "allowed",
|
|
10359
11016
|
source: "post-hook"
|
|
10360
11017
|
};
|
|
10361
|
-
const logPath =
|
|
10362
|
-
if (!
|
|
10363
|
-
|
|
10364
|
-
|
|
11018
|
+
const logPath = path27.join(os21.homedir(), ".node9", "audit.log");
|
|
11019
|
+
if (!fs25.existsSync(path27.dirname(logPath)))
|
|
11020
|
+
fs25.mkdirSync(path27.dirname(logPath), { recursive: true });
|
|
11021
|
+
fs25.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
10365
11022
|
if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
|
|
10366
11023
|
const command = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
|
|
10367
11024
|
if (command) {
|
|
@@ -10394,7 +11051,7 @@ function registerLogCommand(program2) {
|
|
|
10394
11051
|
}
|
|
10395
11052
|
}
|
|
10396
11053
|
}
|
|
10397
|
-
const safeCwd = typeof payload.cwd === "string" &&
|
|
11054
|
+
const safeCwd = typeof payload.cwd === "string" && path27.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
10398
11055
|
const config = getConfig(safeCwd);
|
|
10399
11056
|
if (shouldSnapshot(tool, {}, config)) {
|
|
10400
11057
|
await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
|
|
@@ -10403,9 +11060,9 @@ function registerLogCommand(program2) {
|
|
|
10403
11060
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
10404
11061
|
process.stderr.write(`[Node9] audit log error: ${msg}
|
|
10405
11062
|
`);
|
|
10406
|
-
const debugPath =
|
|
11063
|
+
const debugPath = path27.join(os21.homedir(), ".node9", "hook-debug.log");
|
|
10407
11064
|
try {
|
|
10408
|
-
|
|
11065
|
+
fs25.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
|
|
10409
11066
|
`);
|
|
10410
11067
|
} catch {
|
|
10411
11068
|
}
|
|
@@ -10806,13 +11463,13 @@ function registerConfigShowCommand(program2) {
|
|
|
10806
11463
|
// src/cli/commands/doctor.ts
|
|
10807
11464
|
init_daemon();
|
|
10808
11465
|
import chalk7 from "chalk";
|
|
10809
|
-
import
|
|
10810
|
-
import
|
|
10811
|
-
import
|
|
11466
|
+
import fs26 from "fs";
|
|
11467
|
+
import path28 from "path";
|
|
11468
|
+
import os22 from "os";
|
|
10812
11469
|
import { execSync as execSync2 } from "child_process";
|
|
10813
11470
|
function registerDoctorCommand(program2, version2) {
|
|
10814
11471
|
program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
10815
|
-
const homeDir2 =
|
|
11472
|
+
const homeDir2 = os22.homedir();
|
|
10816
11473
|
let failures = 0;
|
|
10817
11474
|
function pass(msg) {
|
|
10818
11475
|
console.log(chalk7.green(" \u2705 ") + msg);
|
|
@@ -10861,10 +11518,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10861
11518
|
);
|
|
10862
11519
|
}
|
|
10863
11520
|
section("Configuration");
|
|
10864
|
-
const globalConfigPath =
|
|
10865
|
-
if (
|
|
11521
|
+
const globalConfigPath = path28.join(homeDir2, ".node9", "config.json");
|
|
11522
|
+
if (fs26.existsSync(globalConfigPath)) {
|
|
10866
11523
|
try {
|
|
10867
|
-
JSON.parse(
|
|
11524
|
+
JSON.parse(fs26.readFileSync(globalConfigPath, "utf-8"));
|
|
10868
11525
|
pass("~/.node9/config.json found and valid");
|
|
10869
11526
|
} catch {
|
|
10870
11527
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -10872,10 +11529,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10872
11529
|
} else {
|
|
10873
11530
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
10874
11531
|
}
|
|
10875
|
-
const projectConfigPath =
|
|
10876
|
-
if (
|
|
11532
|
+
const projectConfigPath = path28.join(process.cwd(), "node9.config.json");
|
|
11533
|
+
if (fs26.existsSync(projectConfigPath)) {
|
|
10877
11534
|
try {
|
|
10878
|
-
JSON.parse(
|
|
11535
|
+
JSON.parse(fs26.readFileSync(projectConfigPath, "utf-8"));
|
|
10879
11536
|
pass("node9.config.json found and valid (project)");
|
|
10880
11537
|
} catch {
|
|
10881
11538
|
fail(
|
|
@@ -10884,8 +11541,8 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10884
11541
|
);
|
|
10885
11542
|
}
|
|
10886
11543
|
}
|
|
10887
|
-
const credsPath =
|
|
10888
|
-
if (
|
|
11544
|
+
const credsPath = path28.join(homeDir2, ".node9", "credentials.json");
|
|
11545
|
+
if (fs26.existsSync(credsPath)) {
|
|
10889
11546
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
10890
11547
|
} else {
|
|
10891
11548
|
warn(
|
|
@@ -10894,10 +11551,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10894
11551
|
);
|
|
10895
11552
|
}
|
|
10896
11553
|
section("Agent Hooks");
|
|
10897
|
-
const claudeSettingsPath =
|
|
10898
|
-
if (
|
|
11554
|
+
const claudeSettingsPath = path28.join(homeDir2, ".claude", "settings.json");
|
|
11555
|
+
if (fs26.existsSync(claudeSettingsPath)) {
|
|
10899
11556
|
try {
|
|
10900
|
-
const cs = JSON.parse(
|
|
11557
|
+
const cs = JSON.parse(fs26.readFileSync(claudeSettingsPath, "utf-8"));
|
|
10901
11558
|
const hasHook = cs.hooks?.PreToolUse?.some(
|
|
10902
11559
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
10903
11560
|
);
|
|
@@ -10913,10 +11570,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10913
11570
|
} else {
|
|
10914
11571
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
10915
11572
|
}
|
|
10916
|
-
const geminiSettingsPath =
|
|
10917
|
-
if (
|
|
11573
|
+
const geminiSettingsPath = path28.join(homeDir2, ".gemini", "settings.json");
|
|
11574
|
+
if (fs26.existsSync(geminiSettingsPath)) {
|
|
10918
11575
|
try {
|
|
10919
|
-
const gs = JSON.parse(
|
|
11576
|
+
const gs = JSON.parse(fs26.readFileSync(geminiSettingsPath, "utf-8"));
|
|
10920
11577
|
const hasHook = gs.hooks?.BeforeTool?.some(
|
|
10921
11578
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
10922
11579
|
);
|
|
@@ -10932,10 +11589,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10932
11589
|
} else {
|
|
10933
11590
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
10934
11591
|
}
|
|
10935
|
-
const cursorHooksPath =
|
|
10936
|
-
if (
|
|
11592
|
+
const cursorHooksPath = path28.join(homeDir2, ".cursor", "hooks.json");
|
|
11593
|
+
if (fs26.existsSync(cursorHooksPath)) {
|
|
10937
11594
|
try {
|
|
10938
|
-
const cur = JSON.parse(
|
|
11595
|
+
const cur = JSON.parse(fs26.readFileSync(cursorHooksPath, "utf-8"));
|
|
10939
11596
|
const hasHook = cur.hooks?.preToolUse?.some(
|
|
10940
11597
|
(h) => h.command?.includes("node9") || h.command?.includes("cli.js")
|
|
10941
11598
|
);
|
|
@@ -10973,9 +11630,9 @@ function registerDoctorCommand(program2, version2) {
|
|
|
10973
11630
|
|
|
10974
11631
|
// src/cli/commands/audit.ts
|
|
10975
11632
|
import chalk8 from "chalk";
|
|
10976
|
-
import
|
|
10977
|
-
import
|
|
10978
|
-
import
|
|
11633
|
+
import fs27 from "fs";
|
|
11634
|
+
import path29 from "path";
|
|
11635
|
+
import os23 from "os";
|
|
10979
11636
|
function formatRelativeTime(timestamp) {
|
|
10980
11637
|
const diff = Date.now() - new Date(timestamp).getTime();
|
|
10981
11638
|
const sec = Math.floor(diff / 1e3);
|
|
@@ -10988,14 +11645,14 @@ function formatRelativeTime(timestamp) {
|
|
|
10988
11645
|
}
|
|
10989
11646
|
function registerAuditCommand(program2) {
|
|
10990
11647
|
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) => {
|
|
10991
|
-
const logPath =
|
|
10992
|
-
if (!
|
|
11648
|
+
const logPath = path29.join(os23.homedir(), ".node9", "audit.log");
|
|
11649
|
+
if (!fs27.existsSync(logPath)) {
|
|
10993
11650
|
console.log(
|
|
10994
11651
|
chalk8.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
10995
11652
|
);
|
|
10996
11653
|
return;
|
|
10997
11654
|
}
|
|
10998
|
-
const raw =
|
|
11655
|
+
const raw = fs27.readFileSync(logPath, "utf-8");
|
|
10999
11656
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
11000
11657
|
let entries = lines.flatMap((line) => {
|
|
11001
11658
|
try {
|
|
@@ -11049,9 +11706,9 @@ function registerAuditCommand(program2) {
|
|
|
11049
11706
|
|
|
11050
11707
|
// src/cli/commands/report.ts
|
|
11051
11708
|
import chalk9 from "chalk";
|
|
11052
|
-
import
|
|
11053
|
-
import
|
|
11054
|
-
import
|
|
11709
|
+
import fs28 from "fs";
|
|
11710
|
+
import path30 from "path";
|
|
11711
|
+
import os24 from "os";
|
|
11055
11712
|
var TEST_COMMAND_RE3 = /(?:^|\s)(npm\s+(?:run\s+)?test|npx\s+(?:vitest|jest|mocha)|yarn\s+(?:run\s+)?test|pnpm\s+(?:run\s+)?test|vitest|jest|mocha|pytest|py\.test|cargo\s+test|go\s+test|bundle\s+exec\s+rspec|rspec|phpunit|dotnet\s+test)\b/i;
|
|
11056
11713
|
function buildTestTimestamps(allEntries) {
|
|
11057
11714
|
const testTs = /* @__PURE__ */ new Set();
|
|
@@ -11098,8 +11755,8 @@ function getDateRange(period) {
|
|
|
11098
11755
|
}
|
|
11099
11756
|
}
|
|
11100
11757
|
function parseAuditLog(logPath) {
|
|
11101
|
-
if (!
|
|
11102
|
-
const raw =
|
|
11758
|
+
if (!fs28.existsSync(logPath)) return [];
|
|
11759
|
+
const raw = fs28.readFileSync(logPath, "utf-8");
|
|
11103
11760
|
return raw.split("\n").flatMap((line) => {
|
|
11104
11761
|
if (!line.trim()) return [];
|
|
11105
11762
|
try {
|
|
@@ -11166,34 +11823,38 @@ function loadClaudeCost(start, end) {
|
|
|
11166
11823
|
byDay: /* @__PURE__ */ new Map(),
|
|
11167
11824
|
byModel: /* @__PURE__ */ new Map(),
|
|
11168
11825
|
inputTokens: 0,
|
|
11826
|
+
outputTokens: 0,
|
|
11827
|
+
cacheWriteTokens: 0,
|
|
11169
11828
|
cacheReadTokens: 0
|
|
11170
11829
|
};
|
|
11171
|
-
const projectsDir =
|
|
11172
|
-
if (!
|
|
11830
|
+
const projectsDir = path30.join(os24.homedir(), ".claude", "projects");
|
|
11831
|
+
if (!fs28.existsSync(projectsDir)) return empty;
|
|
11173
11832
|
let dirs;
|
|
11174
11833
|
try {
|
|
11175
|
-
dirs =
|
|
11834
|
+
dirs = fs28.readdirSync(projectsDir);
|
|
11176
11835
|
} catch {
|
|
11177
11836
|
return empty;
|
|
11178
11837
|
}
|
|
11179
11838
|
let total = 0;
|
|
11180
11839
|
let inputTokens = 0;
|
|
11840
|
+
let outputTokens = 0;
|
|
11841
|
+
let cacheWriteTokens = 0;
|
|
11181
11842
|
let cacheReadTokens = 0;
|
|
11182
11843
|
const byDay = /* @__PURE__ */ new Map();
|
|
11183
11844
|
const byModel = /* @__PURE__ */ new Map();
|
|
11184
11845
|
for (const proj of dirs) {
|
|
11185
|
-
const projPath =
|
|
11846
|
+
const projPath = path30.join(projectsDir, proj);
|
|
11186
11847
|
let files;
|
|
11187
11848
|
try {
|
|
11188
|
-
const stat =
|
|
11849
|
+
const stat = fs28.statSync(projPath);
|
|
11189
11850
|
if (!stat.isDirectory()) continue;
|
|
11190
|
-
files =
|
|
11851
|
+
files = fs28.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
|
|
11191
11852
|
} catch {
|
|
11192
11853
|
continue;
|
|
11193
11854
|
}
|
|
11194
11855
|
for (const file of files) {
|
|
11195
11856
|
try {
|
|
11196
|
-
const raw =
|
|
11857
|
+
const raw = fs28.readFileSync(path30.join(projPath, file), "utf-8");
|
|
11197
11858
|
for (const line of raw.split("\n")) {
|
|
11198
11859
|
if (!line.trim()) continue;
|
|
11199
11860
|
let entry;
|
|
@@ -11218,6 +11879,8 @@ function loadClaudeCost(start, end) {
|
|
|
11218
11879
|
const cost = inp * p.i + out * p.o + cw * p.cw + cr * p.cr;
|
|
11219
11880
|
total += cost;
|
|
11220
11881
|
inputTokens += inp;
|
|
11882
|
+
outputTokens += out;
|
|
11883
|
+
cacheWriteTokens += cw;
|
|
11221
11884
|
cacheReadTokens += cr;
|
|
11222
11885
|
const dateKey = entry.timestamp.slice(0, 10);
|
|
11223
11886
|
byDay.set(dateKey, (byDay.get(dateKey) ?? 0) + cost);
|
|
@@ -11229,15 +11892,24 @@ function loadClaudeCost(start, end) {
|
|
|
11229
11892
|
}
|
|
11230
11893
|
}
|
|
11231
11894
|
}
|
|
11232
|
-
return { total, byDay, byModel, inputTokens, cacheReadTokens };
|
|
11895
|
+
return { total, byDay, byModel, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens };
|
|
11233
11896
|
}
|
|
11234
11897
|
function registerReportCommand(program2) {
|
|
11235
11898
|
program2.command("report").description("Activity and security report \u2014 what Claude did, what was blocked").option("--period <period>", "today | 7d | 30d | month", "7d").option("--no-tests", "exclude test runner calls (npm test, vitest, pytest\u2026) from stats").action((options) => {
|
|
11236
11899
|
const period = ["today", "7d", "30d", "month"].includes(
|
|
11237
11900
|
options.period
|
|
11238
11901
|
) ? options.period : "7d";
|
|
11239
|
-
const logPath =
|
|
11902
|
+
const logPath = path30.join(os24.homedir(), ".node9", "audit.log");
|
|
11240
11903
|
const allEntries = parseAuditLog(logPath);
|
|
11904
|
+
const unackedDlp = allEntries.filter((e) => e.source === "response-dlp");
|
|
11905
|
+
if (unackedDlp.length > 0) {
|
|
11906
|
+
console.log("");
|
|
11907
|
+
console.log(
|
|
11908
|
+
chalk9.bgRed.white.bold(
|
|
11909
|
+
` \u26A0\uFE0F DLP ALERT: ${unackedDlp.length} secret${unackedDlp.length !== 1 ? "s" : ""} found in Claude response text `
|
|
11910
|
+
) + " " + chalk9.yellow("\u2192 run: node9 dlp")
|
|
11911
|
+
);
|
|
11912
|
+
}
|
|
11241
11913
|
if (allEntries.length === 0) {
|
|
11242
11914
|
console.log(
|
|
11243
11915
|
chalk9.yellow("\n No audit data found. Run node9 with Claude Code to generate entries.\n")
|
|
@@ -11250,6 +11922,8 @@ function registerReportCommand(program2) {
|
|
|
11250
11922
|
byDay: costByDay,
|
|
11251
11923
|
byModel: costByModel,
|
|
11252
11924
|
inputTokens: costInputTokens,
|
|
11925
|
+
outputTokens: costOutputTokens,
|
|
11926
|
+
cacheWriteTokens: costCacheWrite,
|
|
11253
11927
|
cacheReadTokens: costCacheRead
|
|
11254
11928
|
} = loadClaudeCost(start, end);
|
|
11255
11929
|
const periodMs = end.getTime() - start.getTime();
|
|
@@ -11267,6 +11941,7 @@ function registerReportCommand(program2) {
|
|
|
11267
11941
|
let filteredTestCount = 0;
|
|
11268
11942
|
const entries = allEntries.filter((e) => {
|
|
11269
11943
|
if (e.source === "post-hook") return false;
|
|
11944
|
+
if (e.source === "response-dlp") return false;
|
|
11270
11945
|
const ts = new Date(e.ts);
|
|
11271
11946
|
if (ts < start || ts > end) return false;
|
|
11272
11947
|
if (excludeTests && isTestEntry(e, testTs)) {
|
|
@@ -11400,7 +12075,7 @@ function registerReportCommand(program2) {
|
|
|
11400
12075
|
if (topBlocks.length === 0) {
|
|
11401
12076
|
console.log(" " + " ".repeat(COL) + " " + chalk9.dim("nothing blocked \u2713"));
|
|
11402
12077
|
}
|
|
11403
|
-
if (agentMap.size
|
|
12078
|
+
if (agentMap.size >= 1) {
|
|
11404
12079
|
console.log("");
|
|
11405
12080
|
console.log(" " + chalk9.bold("Agents"));
|
|
11406
12081
|
console.log(" " + chalk9.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
@@ -11450,6 +12125,40 @@ function registerReportCommand(program2) {
|
|
|
11450
12125
|
);
|
|
11451
12126
|
}
|
|
11452
12127
|
}
|
|
12128
|
+
const totalTokens = costInputTokens + costOutputTokens + costCacheWrite + costCacheRead;
|
|
12129
|
+
if (totalTokens > 0) {
|
|
12130
|
+
const cacheHitPct = costInputTokens + costCacheRead > 0 ? Math.round(costCacheRead / (costInputTokens + costCacheRead) * 100) : 0;
|
|
12131
|
+
console.log("");
|
|
12132
|
+
console.log(" " + chalk9.bold("Tokens") + " " + chalk9.dim(`${num(totalTokens)} total`));
|
|
12133
|
+
console.log(" " + chalk9.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
12134
|
+
const tokenRows = [
|
|
12135
|
+
["Input", costInputTokens, chalk9.cyan(num(costInputTokens))],
|
|
12136
|
+
["Output", costOutputTokens, chalk9.white(num(costOutputTokens))],
|
|
12137
|
+
["Cache write", costCacheWrite, chalk9.yellow(num(costCacheWrite))],
|
|
12138
|
+
["Cache read", costCacheRead, chalk9.green(num(costCacheRead))]
|
|
12139
|
+
];
|
|
12140
|
+
const maxTok = Math.max(
|
|
12141
|
+
costInputTokens,
|
|
12142
|
+
costOutputTokens,
|
|
12143
|
+
costCacheWrite,
|
|
12144
|
+
costCacheRead,
|
|
12145
|
+
1
|
|
12146
|
+
);
|
|
12147
|
+
const TOK_BAR = Math.max(6, Math.min(20, W - 30));
|
|
12148
|
+
const TOK_LABEL = 14;
|
|
12149
|
+
for (const [label, count, colored] of tokenRows) {
|
|
12150
|
+
if (count === 0) continue;
|
|
12151
|
+
const b = colorBar(count, maxTok, TOK_BAR);
|
|
12152
|
+
console.log(" " + chalk9.white(label.padEnd(TOK_LABEL)) + b + " " + colored);
|
|
12153
|
+
}
|
|
12154
|
+
if (cacheHitPct > 0) {
|
|
12155
|
+
console.log(
|
|
12156
|
+
" " + chalk9.dim(
|
|
12157
|
+
`Cache hit rate: ${cacheHitPct}% (saves ~${fmtCost(costCacheRead * 27e-7)} vs fresh input)`
|
|
12158
|
+
)
|
|
12159
|
+
);
|
|
12160
|
+
}
|
|
12161
|
+
}
|
|
11453
12162
|
if (costUSD > 0) {
|
|
11454
12163
|
const periodDays = Math.max(1, Math.ceil((end.getTime() - start.getTime()) / 864e5));
|
|
11455
12164
|
const avgPerDay = costUSD / periodDays;
|
|
@@ -11474,6 +12183,33 @@ function registerReportCommand(program2) {
|
|
|
11474
12183
|
);
|
|
11475
12184
|
}
|
|
11476
12185
|
}
|
|
12186
|
+
const responseDlpEntries = allEntries.filter((e) => {
|
|
12187
|
+
if (e.source !== "response-dlp") return false;
|
|
12188
|
+
const ts = new Date(e.ts);
|
|
12189
|
+
return ts >= start && ts <= end;
|
|
12190
|
+
});
|
|
12191
|
+
if (responseDlpEntries.length > 0) {
|
|
12192
|
+
console.log("");
|
|
12193
|
+
console.log(
|
|
12194
|
+
" " + chalk9.red.bold("\u26A0\uFE0F Response DLP") + chalk9.dim(" \xB7 ") + chalk9.red(
|
|
12195
|
+
`${responseDlpEntries.length} secret${responseDlpEntries.length !== 1 ? "s" : ""} found in Claude response text`
|
|
12196
|
+
)
|
|
12197
|
+
);
|
|
12198
|
+
console.log(" " + chalk9.dim("\u2500".repeat(Math.min(60, W - 4))));
|
|
12199
|
+
console.log(
|
|
12200
|
+
" " + chalk9.yellow("These were NOT blocked \u2014 Claude included them in response prose.")
|
|
12201
|
+
);
|
|
12202
|
+
console.log(" " + chalk9.yellow("Rotate affected keys immediately."));
|
|
12203
|
+
for (const e of responseDlpEntries.slice(0, 5)) {
|
|
12204
|
+
const ts = chalk9.dim(fmtDate(e.ts) + " ");
|
|
12205
|
+
const pattern = chalk9.red(e.dlpPattern ?? "DLP");
|
|
12206
|
+
const sample = chalk9.gray(e.dlpSample ?? "");
|
|
12207
|
+
console.log(` ${ts}${pattern} ${sample}`);
|
|
12208
|
+
}
|
|
12209
|
+
if (responseDlpEntries.length > 5) {
|
|
12210
|
+
console.log(chalk9.dim(` \u2026 and ${responseDlpEntries.length - 5} more`));
|
|
12211
|
+
}
|
|
12212
|
+
}
|
|
11477
12213
|
console.log("");
|
|
11478
12214
|
console.log(
|
|
11479
12215
|
" " + chalk9.dim("node9 audit --deny") + chalk9.dim(" \xB7 ") + chalk9.dim("node9 report --period today|7d|30d|month --no-tests")
|
|
@@ -11591,12 +12327,12 @@ function registerDaemonCommand(program2) {
|
|
|
11591
12327
|
init_core();
|
|
11592
12328
|
init_daemon();
|
|
11593
12329
|
import chalk11 from "chalk";
|
|
11594
|
-
import
|
|
11595
|
-
import
|
|
11596
|
-
import
|
|
12330
|
+
import fs29 from "fs";
|
|
12331
|
+
import path31 from "path";
|
|
12332
|
+
import os25 from "os";
|
|
11597
12333
|
function readJson2(filePath) {
|
|
11598
12334
|
try {
|
|
11599
|
-
if (
|
|
12335
|
+
if (fs29.existsSync(filePath)) return JSON.parse(fs29.readFileSync(filePath, "utf-8"));
|
|
11600
12336
|
} catch {
|
|
11601
12337
|
}
|
|
11602
12338
|
return null;
|
|
@@ -11661,28 +12397,28 @@ function registerStatusCommand(program2) {
|
|
|
11661
12397
|
console.log("");
|
|
11662
12398
|
const modeLabel = settings.mode === "audit" ? chalk11.blue("audit") : settings.mode === "strict" ? chalk11.red("strict") : chalk11.white("standard");
|
|
11663
12399
|
console.log(` Mode: ${modeLabel}`);
|
|
11664
|
-
const projectConfig =
|
|
11665
|
-
const globalConfig =
|
|
12400
|
+
const projectConfig = path31.join(process.cwd(), "node9.config.json");
|
|
12401
|
+
const globalConfig = path31.join(os25.homedir(), ".node9", "config.json");
|
|
11666
12402
|
console.log(
|
|
11667
|
-
` Local: ${
|
|
12403
|
+
` Local: ${fs29.existsSync(projectConfig) ? chalk11.green("Active (node9.config.json)") : chalk11.gray("Not present")}`
|
|
11668
12404
|
);
|
|
11669
12405
|
console.log(
|
|
11670
|
-
` Global: ${
|
|
12406
|
+
` Global: ${fs29.existsSync(globalConfig) ? chalk11.green("Active (~/.node9/config.json)") : chalk11.gray("Not present")}`
|
|
11671
12407
|
);
|
|
11672
12408
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
11673
12409
|
console.log(
|
|
11674
12410
|
` Sandbox: ${chalk11.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
11675
12411
|
);
|
|
11676
12412
|
}
|
|
11677
|
-
const homeDir2 =
|
|
12413
|
+
const homeDir2 = os25.homedir();
|
|
11678
12414
|
const claudeSettings = readJson2(
|
|
11679
|
-
|
|
12415
|
+
path31.join(homeDir2, ".claude", "settings.json")
|
|
11680
12416
|
);
|
|
11681
|
-
const claudeConfig = readJson2(
|
|
12417
|
+
const claudeConfig = readJson2(path31.join(homeDir2, ".claude.json"));
|
|
11682
12418
|
const geminiSettings = readJson2(
|
|
11683
|
-
|
|
12419
|
+
path31.join(homeDir2, ".gemini", "settings.json")
|
|
11684
12420
|
);
|
|
11685
|
-
const cursorConfig = readJson2(
|
|
12421
|
+
const cursorConfig = readJson2(path31.join(homeDir2, ".cursor", "mcp.json"));
|
|
11686
12422
|
const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
|
|
11687
12423
|
if (agentFound) {
|
|
11688
12424
|
console.log("");
|
|
@@ -11742,9 +12478,9 @@ function registerStatusCommand(program2) {
|
|
|
11742
12478
|
// src/cli/commands/init.ts
|
|
11743
12479
|
init_core();
|
|
11744
12480
|
import chalk12 from "chalk";
|
|
11745
|
-
import
|
|
11746
|
-
import
|
|
11747
|
-
import
|
|
12481
|
+
import fs30 from "fs";
|
|
12482
|
+
import path32 from "path";
|
|
12483
|
+
import os26 from "os";
|
|
11748
12484
|
import https3 from "https";
|
|
11749
12485
|
init_shields();
|
|
11750
12486
|
init_service();
|
|
@@ -11804,15 +12540,15 @@ function registerInitCommand(program2) {
|
|
|
11804
12540
|
}
|
|
11805
12541
|
console.log("");
|
|
11806
12542
|
}
|
|
11807
|
-
const configPath =
|
|
11808
|
-
if (
|
|
12543
|
+
const configPath = path32.join(os26.homedir(), ".node9", "config.json");
|
|
12544
|
+
if (fs30.existsSync(configPath) && !options.force) {
|
|
11809
12545
|
try {
|
|
11810
|
-
const existing = JSON.parse(
|
|
12546
|
+
const existing = JSON.parse(fs30.readFileSync(configPath, "utf-8"));
|
|
11811
12547
|
const settings = existing.settings ?? {};
|
|
11812
12548
|
if (settings.mode !== chosenMode) {
|
|
11813
12549
|
settings.mode = chosenMode;
|
|
11814
12550
|
existing.settings = settings;
|
|
11815
|
-
|
|
12551
|
+
fs30.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
|
|
11816
12552
|
console.log(chalk12.green(`\u2705 Mode updated: ${chosenMode}`));
|
|
11817
12553
|
} else {
|
|
11818
12554
|
console.log(chalk12.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
@@ -11825,9 +12561,9 @@ function registerInitCommand(program2) {
|
|
|
11825
12561
|
...DEFAULT_CONFIG,
|
|
11826
12562
|
settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
|
|
11827
12563
|
};
|
|
11828
|
-
const dir =
|
|
11829
|
-
if (!
|
|
11830
|
-
|
|
12564
|
+
const dir = path32.dirname(configPath);
|
|
12565
|
+
if (!fs30.existsSync(dir)) fs30.mkdirSync(dir, { recursive: true });
|
|
12566
|
+
fs30.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
|
|
11831
12567
|
console.log(chalk12.green(`\u2705 Config created: ${configPath}`));
|
|
11832
12568
|
console.log(chalk12.gray(` Mode: ${chosenMode}`));
|
|
11833
12569
|
}
|
|
@@ -11911,7 +12647,7 @@ function registerInitCommand(program2) {
|
|
|
11911
12647
|
}
|
|
11912
12648
|
|
|
11913
12649
|
// src/cli/commands/undo.ts
|
|
11914
|
-
import
|
|
12650
|
+
import path33 from "path";
|
|
11915
12651
|
import chalk14 from "chalk";
|
|
11916
12652
|
|
|
11917
12653
|
// src/tui/undo-navigator.ts
|
|
@@ -12070,7 +12806,7 @@ function findMatchingCwd(startDir, history) {
|
|
|
12070
12806
|
let dir = startDir;
|
|
12071
12807
|
while (true) {
|
|
12072
12808
|
if (cwds.has(dir)) return dir;
|
|
12073
|
-
const parent =
|
|
12809
|
+
const parent = path33.dirname(dir);
|
|
12074
12810
|
if (parent === dir) return null;
|
|
12075
12811
|
dir = parent;
|
|
12076
12812
|
}
|
|
@@ -12266,12 +13002,12 @@ import { execa as execa2 } from "execa";
|
|
|
12266
13002
|
init_provenance();
|
|
12267
13003
|
|
|
12268
13004
|
// src/mcp-pin.ts
|
|
12269
|
-
import
|
|
12270
|
-
import
|
|
12271
|
-
import
|
|
12272
|
-
import
|
|
12273
|
-
function
|
|
12274
|
-
return
|
|
13005
|
+
import fs31 from "fs";
|
|
13006
|
+
import path34 from "path";
|
|
13007
|
+
import os27 from "os";
|
|
13008
|
+
import crypto5 from "crypto";
|
|
13009
|
+
function getPinsFilePath2() {
|
|
13010
|
+
return path34.join(os27.homedir(), ".node9", "mcp-pins.json");
|
|
12275
13011
|
}
|
|
12276
13012
|
function hashToolDefinitions(tools) {
|
|
12277
13013
|
const sorted = [...tools].sort((a, b) => {
|
|
@@ -12280,15 +13016,15 @@ function hashToolDefinitions(tools) {
|
|
|
12280
13016
|
return nameA.localeCompare(nameB);
|
|
12281
13017
|
});
|
|
12282
13018
|
const canonical = JSON.stringify(sorted);
|
|
12283
|
-
return
|
|
13019
|
+
return crypto5.createHash("sha256").update(canonical).digest("hex");
|
|
12284
13020
|
}
|
|
12285
13021
|
function getServerKey(upstreamCommand) {
|
|
12286
|
-
return
|
|
13022
|
+
return crypto5.createHash("sha256").update(upstreamCommand).digest("hex").slice(0, 16);
|
|
12287
13023
|
}
|
|
12288
13024
|
function readMcpPinsSafe() {
|
|
12289
|
-
const filePath =
|
|
13025
|
+
const filePath = getPinsFilePath2();
|
|
12290
13026
|
try {
|
|
12291
|
-
const raw =
|
|
13027
|
+
const raw = fs31.readFileSync(filePath, "utf-8");
|
|
12292
13028
|
if (!raw.trim()) {
|
|
12293
13029
|
return { ok: false, reason: "corrupt", detail: "empty file" };
|
|
12294
13030
|
}
|
|
@@ -12311,11 +13047,11 @@ function readMcpPins() {
|
|
|
12311
13047
|
throw new Error(`[node9] MCP pin file is corrupt: ${result.detail}`);
|
|
12312
13048
|
}
|
|
12313
13049
|
function writeMcpPins(data) {
|
|
12314
|
-
const filePath =
|
|
12315
|
-
|
|
12316
|
-
const tmp = `${filePath}.${
|
|
12317
|
-
|
|
12318
|
-
|
|
13050
|
+
const filePath = getPinsFilePath2();
|
|
13051
|
+
fs31.mkdirSync(path34.dirname(filePath), { recursive: true });
|
|
13052
|
+
const tmp = `${filePath}.${crypto5.randomBytes(6).toString("hex")}.tmp`;
|
|
13053
|
+
fs31.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
|
|
13054
|
+
fs31.renameSync(tmp, filePath);
|
|
12319
13055
|
}
|
|
12320
13056
|
function checkPin(serverKey, currentHash) {
|
|
12321
13057
|
const result = readMcpPinsSafe();
|
|
@@ -12338,12 +13074,12 @@ function updatePin(serverKey, label, toolsHash, toolNames) {
|
|
|
12338
13074
|
};
|
|
12339
13075
|
writeMcpPins(pins);
|
|
12340
13076
|
}
|
|
12341
|
-
function
|
|
13077
|
+
function removePin2(serverKey) {
|
|
12342
13078
|
const pins = readMcpPins();
|
|
12343
13079
|
delete pins.servers[serverKey];
|
|
12344
13080
|
writeMcpPins(pins);
|
|
12345
13081
|
}
|
|
12346
|
-
function
|
|
13082
|
+
function clearAllPins2() {
|
|
12347
13083
|
writeMcpPins({ servers: {} });
|
|
12348
13084
|
}
|
|
12349
13085
|
|
|
@@ -12687,9 +13423,9 @@ function registerMcpGatewayCommand(program2) {
|
|
|
12687
13423
|
|
|
12688
13424
|
// src/mcp-server/index.ts
|
|
12689
13425
|
import readline4 from "readline";
|
|
12690
|
-
import
|
|
12691
|
-
import
|
|
12692
|
-
import
|
|
13426
|
+
import fs32 from "fs";
|
|
13427
|
+
import os28 from "os";
|
|
13428
|
+
import path35 from "path";
|
|
12693
13429
|
init_core();
|
|
12694
13430
|
init_daemon();
|
|
12695
13431
|
init_shields();
|
|
@@ -12864,13 +13600,13 @@ function handleStatus() {
|
|
|
12864
13600
|
lines.push(`Active shields: ${activeShields.length > 0 ? activeShields.join(", ") : "none"}`);
|
|
12865
13601
|
lines.push(`Smart rules: ${config.policy.smartRules.length} loaded`);
|
|
12866
13602
|
lines.push(`DLP: ${config.policy.dlp?.enabled !== false ? "enabled" : "disabled"}`);
|
|
12867
|
-
const projectConfig =
|
|
12868
|
-
const globalConfig =
|
|
13603
|
+
const projectConfig = path35.join(process.cwd(), "node9.config.json");
|
|
13604
|
+
const globalConfig = path35.join(os28.homedir(), ".node9", "config.json");
|
|
12869
13605
|
lines.push(
|
|
12870
|
-
`Project config (node9.config.json): ${
|
|
13606
|
+
`Project config (node9.config.json): ${fs32.existsSync(projectConfig) ? "present" : "not found"}`
|
|
12871
13607
|
);
|
|
12872
13608
|
lines.push(
|
|
12873
|
-
`Global config (~/.node9/config.json): ${
|
|
13609
|
+
`Global config (~/.node9/config.json): ${fs32.existsSync(globalConfig) ? "present" : "not found"}`
|
|
12874
13610
|
);
|
|
12875
13611
|
return lines.join("\n");
|
|
12876
13612
|
}
|
|
@@ -12944,21 +13680,21 @@ function handleShieldDisable(args) {
|
|
|
12944
13680
|
writeActiveShields(active.filter((s) => s !== name));
|
|
12945
13681
|
return `Shield "${name}" disabled.`;
|
|
12946
13682
|
}
|
|
12947
|
-
var GLOBAL_CONFIG_PATH2 =
|
|
13683
|
+
var GLOBAL_CONFIG_PATH2 = path35.join(os28.homedir(), ".node9", "config.json");
|
|
12948
13684
|
var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
|
|
12949
13685
|
function readGlobalConfigRaw() {
|
|
12950
13686
|
try {
|
|
12951
|
-
if (
|
|
12952
|
-
return JSON.parse(
|
|
13687
|
+
if (fs32.existsSync(GLOBAL_CONFIG_PATH2)) {
|
|
13688
|
+
return JSON.parse(fs32.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
|
|
12953
13689
|
}
|
|
12954
13690
|
} catch {
|
|
12955
13691
|
}
|
|
12956
13692
|
return {};
|
|
12957
13693
|
}
|
|
12958
13694
|
function writeGlobalConfigRaw(data) {
|
|
12959
|
-
const dir =
|
|
12960
|
-
if (!
|
|
12961
|
-
|
|
13695
|
+
const dir = path35.dirname(GLOBAL_CONFIG_PATH2);
|
|
13696
|
+
if (!fs32.existsSync(dir)) fs32.mkdirSync(dir, { recursive: true });
|
|
13697
|
+
fs32.writeFileSync(GLOBAL_CONFIG_PATH2, JSON.stringify(data, null, 2) + "\n");
|
|
12962
13698
|
}
|
|
12963
13699
|
function handleApproverList() {
|
|
12964
13700
|
const config = getConfig();
|
|
@@ -13001,9 +13737,9 @@ function handleApproverSet(args) {
|
|
|
13001
13737
|
}
|
|
13002
13738
|
function handleAuditGet(args) {
|
|
13003
13739
|
const limit = Math.min(typeof args.limit === "number" ? args.limit : 20, 100);
|
|
13004
|
-
const auditPath =
|
|
13005
|
-
if (!
|
|
13006
|
-
const lines =
|
|
13740
|
+
const auditPath = path35.join(os28.homedir(), ".node9", "audit.log");
|
|
13741
|
+
if (!fs32.existsSync(auditPath)) return "No audit log found.";
|
|
13742
|
+
const lines = fs32.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
|
|
13007
13743
|
const recent = lines.slice(-limit);
|
|
13008
13744
|
const entries = recent.map((line) => {
|
|
13009
13745
|
try {
|
|
@@ -13301,7 +14037,7 @@ function registerMcpPinCommand(program2) {
|
|
|
13301
14037
|
process.exit(1);
|
|
13302
14038
|
}
|
|
13303
14039
|
const label = pins.servers[serverKey].label;
|
|
13304
|
-
|
|
14040
|
+
removePin2(serverKey);
|
|
13305
14041
|
console.log(chalk18.green(`
|
|
13306
14042
|
\u{1F513} Pin removed for ${chalk18.cyan(serverKey)}`));
|
|
13307
14043
|
console.log(chalk18.gray(` Server: ${label}`));
|
|
@@ -13314,7 +14050,7 @@ function registerMcpPinCommand(program2) {
|
|
|
13314
14050
|
return;
|
|
13315
14051
|
}
|
|
13316
14052
|
const count = result.ok ? Object.keys(result.pins.servers).length : "?";
|
|
13317
|
-
|
|
14053
|
+
clearAllPins2();
|
|
13318
14054
|
console.log(chalk18.green(`
|
|
13319
14055
|
\u{1F513} Cleared ${count} MCP pin(s).`));
|
|
13320
14056
|
console.log(chalk18.gray(" Next connection to each server will re-pin.\n"));
|
|
@@ -13465,9 +14201,9 @@ init_config();
|
|
|
13465
14201
|
init_policy();
|
|
13466
14202
|
init_dlp();
|
|
13467
14203
|
import chalk21 from "chalk";
|
|
13468
|
-
import
|
|
13469
|
-
import
|
|
13470
|
-
import
|
|
14204
|
+
import fs33 from "fs";
|
|
14205
|
+
import path36 from "path";
|
|
14206
|
+
import os29 from "os";
|
|
13471
14207
|
var CLAUDE_PRICING2 = {
|
|
13472
14208
|
"claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
|
|
13473
14209
|
"claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
|
|
@@ -13535,7 +14271,7 @@ function buildRuleSources() {
|
|
|
13535
14271
|
return sources;
|
|
13536
14272
|
}
|
|
13537
14273
|
function scanClaudeHistory(startDate) {
|
|
13538
|
-
const projectsDir =
|
|
14274
|
+
const projectsDir = path36.join(os29.homedir(), ".claude", "projects");
|
|
13539
14275
|
const result = {
|
|
13540
14276
|
filesScanned: 0,
|
|
13541
14277
|
sessions: 0,
|
|
@@ -13547,25 +14283,25 @@ function scanClaudeHistory(startDate) {
|
|
|
13547
14283
|
firstDate: null,
|
|
13548
14284
|
lastDate: null
|
|
13549
14285
|
};
|
|
13550
|
-
if (!
|
|
14286
|
+
if (!fs33.existsSync(projectsDir)) return result;
|
|
13551
14287
|
let projDirs;
|
|
13552
14288
|
try {
|
|
13553
|
-
projDirs =
|
|
14289
|
+
projDirs = fs33.readdirSync(projectsDir);
|
|
13554
14290
|
} catch {
|
|
13555
14291
|
return result;
|
|
13556
14292
|
}
|
|
13557
14293
|
const ruleSources = buildRuleSources();
|
|
13558
14294
|
for (const proj of projDirs) {
|
|
13559
|
-
const projPath =
|
|
14295
|
+
const projPath = path36.join(projectsDir, proj);
|
|
13560
14296
|
try {
|
|
13561
|
-
if (!
|
|
14297
|
+
if (!fs33.statSync(projPath).isDirectory()) continue;
|
|
13562
14298
|
} catch {
|
|
13563
14299
|
continue;
|
|
13564
14300
|
}
|
|
13565
|
-
const projLabel = decodeURIComponent(proj).replace(
|
|
14301
|
+
const projLabel = decodeURIComponent(proj).replace(os29.homedir(), "~").slice(0, 40);
|
|
13566
14302
|
let files;
|
|
13567
14303
|
try {
|
|
13568
|
-
files =
|
|
14304
|
+
files = fs33.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
|
|
13569
14305
|
} catch {
|
|
13570
14306
|
continue;
|
|
13571
14307
|
}
|
|
@@ -13574,7 +14310,7 @@ function scanClaudeHistory(startDate) {
|
|
|
13574
14310
|
result.sessions++;
|
|
13575
14311
|
let raw;
|
|
13576
14312
|
try {
|
|
13577
|
-
raw =
|
|
14313
|
+
raw = fs33.readFileSync(path36.join(projPath, file), "utf-8");
|
|
13578
14314
|
} catch {
|
|
13579
14315
|
continue;
|
|
13580
14316
|
}
|
|
@@ -13615,6 +14351,9 @@ function scanClaudeHistory(startDate) {
|
|
|
13615
14351
|
if (toolNameLower === "bash" || toolNameLower === "execute_bash") {
|
|
13616
14352
|
result.bashCalls++;
|
|
13617
14353
|
}
|
|
14354
|
+
const rawCmd = String(input.command ?? "").trimStart();
|
|
14355
|
+
if (/^node9\s+(scan|explain|report|tail|dlp|status|sessions|audit)\b/.test(rawCmd))
|
|
14356
|
+
continue;
|
|
13618
14357
|
const dlpMatch = scanArgs(input);
|
|
13619
14358
|
if (dlpMatch) {
|
|
13620
14359
|
const isDupe = result.dlpFindings.some(
|
|
@@ -13632,6 +14371,7 @@ function scanClaudeHistory(startDate) {
|
|
|
13632
14371
|
}
|
|
13633
14372
|
for (const source of ruleSources) {
|
|
13634
14373
|
const { rule } = source;
|
|
14374
|
+
if (rule.verdict === "allow") continue;
|
|
13635
14375
|
if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
|
|
13636
14376
|
if (!evaluateSmartConditions(input, rule)) continue;
|
|
13637
14377
|
const inputPreview = preview(input, 120);
|
|
@@ -13667,8 +14407,8 @@ function registerScanCommand(program2) {
|
|
|
13667
14407
|
console.log("");
|
|
13668
14408
|
console.log(chalk21.cyan.bold("\u{1F50D} node9 scan") + chalk21.dim(" \u2014 what would node9 catch?"));
|
|
13669
14409
|
console.log("");
|
|
13670
|
-
const projectsDir =
|
|
13671
|
-
if (!
|
|
14410
|
+
const projectsDir = path36.join(os29.homedir(), ".claude", "projects");
|
|
14411
|
+
if (!fs33.existsSync(projectsDir)) {
|
|
13672
14412
|
console.log(chalk21.yellow(" No Claude history found at ~/.claude/projects/"));
|
|
13673
14413
|
console.log(chalk21.gray(" Install Claude Code, run a few sessions, then try again.\n"));
|
|
13674
14414
|
return;
|
|
@@ -13780,8 +14520,8 @@ function registerScanCommand(program2) {
|
|
|
13780
14520
|
);
|
|
13781
14521
|
console.log("");
|
|
13782
14522
|
}
|
|
13783
|
-
const auditLog =
|
|
13784
|
-
if (
|
|
14523
|
+
const auditLog = path36.join(os29.homedir(), ".node9", "audit.log");
|
|
14524
|
+
if (fs33.existsSync(auditLog)) {
|
|
13785
14525
|
console.log(chalk21.green(" \u2705 node9 is active \u2014 future sessions are protected."));
|
|
13786
14526
|
console.log(
|
|
13787
14527
|
chalk21.dim(" Run ") + chalk21.cyan("node9 report") + chalk21.dim(" to see live stats.")
|
|
@@ -13798,9 +14538,9 @@ function registerScanCommand(program2) {
|
|
|
13798
14538
|
|
|
13799
14539
|
// src/cli/commands/sessions.ts
|
|
13800
14540
|
import chalk22 from "chalk";
|
|
13801
|
-
import
|
|
13802
|
-
import
|
|
13803
|
-
import
|
|
14541
|
+
import fs34 from "fs";
|
|
14542
|
+
import path37 from "path";
|
|
14543
|
+
import os30 from "os";
|
|
13804
14544
|
var CLAUDE_PRICING3 = {
|
|
13805
14545
|
"claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
|
|
13806
14546
|
"claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
|
|
@@ -13825,10 +14565,10 @@ function encodeProjectPath(projectPath) {
|
|
|
13825
14565
|
}
|
|
13826
14566
|
function sessionJsonlPath(projectPath, sessionId) {
|
|
13827
14567
|
const encoded = encodeProjectPath(projectPath);
|
|
13828
|
-
return
|
|
14568
|
+
return path37.join(os30.homedir(), ".claude", "projects", encoded, `${sessionId}.jsonl`);
|
|
13829
14569
|
}
|
|
13830
14570
|
function projectLabel(projectPath) {
|
|
13831
|
-
return projectPath.replace(
|
|
14571
|
+
return projectPath.replace(os30.homedir(), "~");
|
|
13832
14572
|
}
|
|
13833
14573
|
function parseHistoryLines(lines) {
|
|
13834
14574
|
const entries = [];
|
|
@@ -13897,10 +14637,10 @@ function parseSessionLines(lines) {
|
|
|
13897
14637
|
return { toolCalls, costUSD, hasSnapshot, modifiedFiles };
|
|
13898
14638
|
}
|
|
13899
14639
|
function loadAuditEntries(auditPath) {
|
|
13900
|
-
const aPath = auditPath ??
|
|
14640
|
+
const aPath = auditPath ?? path37.join(os30.homedir(), ".node9", "audit.log");
|
|
13901
14641
|
let raw;
|
|
13902
14642
|
try {
|
|
13903
|
-
raw =
|
|
14643
|
+
raw = fs34.readFileSync(aPath, "utf-8");
|
|
13904
14644
|
} catch {
|
|
13905
14645
|
return [];
|
|
13906
14646
|
}
|
|
@@ -13936,10 +14676,10 @@ function auditEntriesInWindow(entries, windowStart, windowEnd) {
|
|
|
13936
14676
|
return result;
|
|
13937
14677
|
}
|
|
13938
14678
|
function buildSessions(days, historyPath) {
|
|
13939
|
-
const hPath = historyPath ??
|
|
14679
|
+
const hPath = historyPath ?? path37.join(os30.homedir(), ".claude", "history.jsonl");
|
|
13940
14680
|
let historyRaw;
|
|
13941
14681
|
try {
|
|
13942
|
-
historyRaw =
|
|
14682
|
+
historyRaw = fs34.readFileSync(hPath, "utf-8");
|
|
13943
14683
|
} catch {
|
|
13944
14684
|
return [];
|
|
13945
14685
|
}
|
|
@@ -13964,7 +14704,7 @@ function buildSessions(days, historyPath) {
|
|
|
13964
14704
|
const jsonlFile = sessionJsonlPath(entry.project, entry.sessionId);
|
|
13965
14705
|
let sessionLines = [];
|
|
13966
14706
|
try {
|
|
13967
|
-
sessionLines =
|
|
14707
|
+
sessionLines = fs34.readFileSync(jsonlFile, "utf-8").split("\n");
|
|
13968
14708
|
} catch {
|
|
13969
14709
|
}
|
|
13970
14710
|
const { toolCalls, costUSD, hasSnapshot, modifiedFiles } = parseSessionLines(sessionLines);
|
|
@@ -14215,8 +14955,8 @@ function registerSessionsCommand(program2) {
|
|
|
14215
14955
|
console.log("");
|
|
14216
14956
|
console.log(chalk22.cyan.bold("\u{1F4CB} node9 sessions") + chalk22.dim(" \u2014 what your AI agent did"));
|
|
14217
14957
|
console.log("");
|
|
14218
|
-
const historyPath =
|
|
14219
|
-
if (!
|
|
14958
|
+
const historyPath = path37.join(os30.homedir(), ".claude", "history.jsonl");
|
|
14959
|
+
if (!fs34.existsSync(historyPath)) {
|
|
14220
14960
|
console.log(chalk22.yellow(" No Claude session history found at ~/.claude/history.jsonl"));
|
|
14221
14961
|
console.log(chalk22.gray(" Install Claude Code, run a few sessions, then try again.\n"));
|
|
14222
14962
|
return;
|
|
@@ -14246,22 +14986,233 @@ function registerSessionsCommand(program2) {
|
|
|
14246
14986
|
});
|
|
14247
14987
|
}
|
|
14248
14988
|
|
|
14989
|
+
// src/cli/commands/skill-pin.ts
|
|
14990
|
+
import chalk23 from "chalk";
|
|
14991
|
+
import fs35 from "fs";
|
|
14992
|
+
import os31 from "os";
|
|
14993
|
+
import path38 from "path";
|
|
14994
|
+
function wipeSkillSessions() {
|
|
14995
|
+
try {
|
|
14996
|
+
fs35.rmSync(path38.join(os31.homedir(), ".node9", "skill-sessions"), {
|
|
14997
|
+
recursive: true,
|
|
14998
|
+
force: true
|
|
14999
|
+
});
|
|
15000
|
+
} catch {
|
|
15001
|
+
}
|
|
15002
|
+
}
|
|
15003
|
+
function registerSkillPinCommand(program2) {
|
|
15004
|
+
const skillCmd = program2.command("skill").description("Manage skill pinning (supply chain & update drift defense, AST 02 + AST 07)");
|
|
15005
|
+
const pinSubCmd = skillCmd.command("pin").description("Manage pinned skill roots");
|
|
15006
|
+
pinSubCmd.command("list").description("Show all pinned skill roots and their content hashes").action(() => {
|
|
15007
|
+
const result = readSkillPinsSafe();
|
|
15008
|
+
if (!result.ok) {
|
|
15009
|
+
if (result.reason === "missing") {
|
|
15010
|
+
console.log(chalk23.gray("\nNo skill roots are pinned yet."));
|
|
15011
|
+
console.log(
|
|
15012
|
+
chalk23.gray("Pins are created automatically on the first tool call of each session.\n")
|
|
15013
|
+
);
|
|
15014
|
+
return;
|
|
15015
|
+
}
|
|
15016
|
+
console.error(chalk23.red(`
|
|
15017
|
+
\u274C Pin file is corrupt: ${result.detail}`));
|
|
15018
|
+
console.error(chalk23.yellow(" Run: node9 skill pin reset\n"));
|
|
15019
|
+
process.exit(1);
|
|
15020
|
+
}
|
|
15021
|
+
const entries = Object.entries(result.pins.roots);
|
|
15022
|
+
if (entries.length === 0) {
|
|
15023
|
+
console.log(chalk23.gray("\nNo skill roots are pinned yet.\n"));
|
|
15024
|
+
return;
|
|
15025
|
+
}
|
|
15026
|
+
console.log(chalk23.bold("\n\u{1F512} Pinned Skill Roots\n"));
|
|
15027
|
+
for (const [key, entry] of entries) {
|
|
15028
|
+
const missing = entry.exists ? "" : chalk23.yellow(" (not present at pin time)");
|
|
15029
|
+
console.log(` ${chalk23.cyan(key)} ${chalk23.gray(entry.rootPath)}${missing}`);
|
|
15030
|
+
console.log(` Files (${entry.fileCount})`);
|
|
15031
|
+
console.log(` Hash: ${chalk23.gray(entry.contentHash.slice(0, 16))}...`);
|
|
15032
|
+
console.log(` Pinned: ${chalk23.gray(entry.pinnedAt)}
|
|
15033
|
+
`);
|
|
15034
|
+
}
|
|
15035
|
+
});
|
|
15036
|
+
pinSubCmd.command("update <rootKey>").description("Remove a pin so the next session re-pins with current state").action((rootKey) => {
|
|
15037
|
+
let pins;
|
|
15038
|
+
try {
|
|
15039
|
+
pins = readSkillPins();
|
|
15040
|
+
} catch {
|
|
15041
|
+
console.error(chalk23.red("\n\u274C Pin file is corrupt."));
|
|
15042
|
+
console.error(chalk23.yellow(" Run: node9 skill pin reset\n"));
|
|
15043
|
+
process.exit(1);
|
|
15044
|
+
}
|
|
15045
|
+
if (!pins.roots[rootKey]) {
|
|
15046
|
+
console.error(chalk23.red(`
|
|
15047
|
+
\u274C No pin found for root key "${rootKey}"
|
|
15048
|
+
`));
|
|
15049
|
+
console.error(`Run ${chalk23.cyan("node9 skill pin list")} to see pinned roots.
|
|
15050
|
+
`);
|
|
15051
|
+
process.exit(1);
|
|
15052
|
+
}
|
|
15053
|
+
const rootPath = pins.roots[rootKey].rootPath;
|
|
15054
|
+
removePin(rootKey);
|
|
15055
|
+
wipeSkillSessions();
|
|
15056
|
+
console.log(chalk23.green(`
|
|
15057
|
+
\u{1F513} Pin removed for ${chalk23.cyan(rootKey)}`));
|
|
15058
|
+
console.log(chalk23.gray(` ${rootPath}`));
|
|
15059
|
+
console.log(chalk23.gray(" Next session will re-pin with current state.\n"));
|
|
15060
|
+
});
|
|
15061
|
+
pinSubCmd.command("reset").description("Clear all skill pins and wipe session verification flags").action(() => {
|
|
15062
|
+
const result = readSkillPinsSafe();
|
|
15063
|
+
if (!result.ok && result.reason === "missing") {
|
|
15064
|
+
wipeSkillSessions();
|
|
15065
|
+
console.log(chalk23.gray("\nNo pins to clear.\n"));
|
|
15066
|
+
return;
|
|
15067
|
+
}
|
|
15068
|
+
const count = result.ok ? Object.keys(result.pins.roots).length : "?";
|
|
15069
|
+
clearAllPins();
|
|
15070
|
+
wipeSkillSessions();
|
|
15071
|
+
console.log(chalk23.green(`
|
|
15072
|
+
\u{1F513} Cleared ${count} skill pin(s).`));
|
|
15073
|
+
console.log(chalk23.gray(" Next session will re-pin with current state.\n"));
|
|
15074
|
+
});
|
|
15075
|
+
}
|
|
15076
|
+
|
|
15077
|
+
// src/cli/commands/dlp.ts
|
|
15078
|
+
import chalk24 from "chalk";
|
|
15079
|
+
import fs36 from "fs";
|
|
15080
|
+
import path39 from "path";
|
|
15081
|
+
import os32 from "os";
|
|
15082
|
+
var AUDIT_LOG = path39.join(os32.homedir(), ".node9", "audit.log");
|
|
15083
|
+
var RESOLVED_FILE = path39.join(os32.homedir(), ".node9", "dlp-resolved.json");
|
|
15084
|
+
var ANSI_RE = /\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g;
|
|
15085
|
+
function stripAnsi(s) {
|
|
15086
|
+
return s.replace(ANSI_RE, "");
|
|
15087
|
+
}
|
|
15088
|
+
function loadResolved() {
|
|
15089
|
+
try {
|
|
15090
|
+
const raw = JSON.parse(fs36.readFileSync(RESOLVED_FILE, "utf-8"));
|
|
15091
|
+
return new Set(raw);
|
|
15092
|
+
} catch {
|
|
15093
|
+
return /* @__PURE__ */ new Set();
|
|
15094
|
+
}
|
|
15095
|
+
}
|
|
15096
|
+
function saveResolved(resolved) {
|
|
15097
|
+
try {
|
|
15098
|
+
fs36.writeFileSync(RESOLVED_FILE, JSON.stringify([...resolved], null, 2), { mode: 384 });
|
|
15099
|
+
} catch {
|
|
15100
|
+
}
|
|
15101
|
+
}
|
|
15102
|
+
function loadDlpFindings() {
|
|
15103
|
+
if (!fs36.existsSync(AUDIT_LOG)) return [];
|
|
15104
|
+
return fs36.readFileSync(AUDIT_LOG, "utf-8").split("\n").flatMap((line) => {
|
|
15105
|
+
if (!line.trim()) return [];
|
|
15106
|
+
try {
|
|
15107
|
+
const e = JSON.parse(line);
|
|
15108
|
+
return e.source === "response-dlp" ? [e] : [];
|
|
15109
|
+
} catch {
|
|
15110
|
+
return [];
|
|
15111
|
+
}
|
|
15112
|
+
});
|
|
15113
|
+
}
|
|
15114
|
+
function entryKey(e) {
|
|
15115
|
+
return `${e.ts}:${e.dlpPattern}:${e.dlpSample}`;
|
|
15116
|
+
}
|
|
15117
|
+
function fmtDate3(ts) {
|
|
15118
|
+
try {
|
|
15119
|
+
return new Date(ts).toLocaleDateString("en-US", {
|
|
15120
|
+
month: "short",
|
|
15121
|
+
day: "numeric",
|
|
15122
|
+
year: "numeric"
|
|
15123
|
+
});
|
|
15124
|
+
} catch {
|
|
15125
|
+
return ts.slice(0, 10);
|
|
15126
|
+
}
|
|
15127
|
+
}
|
|
15128
|
+
function registerDlpCommand(program2) {
|
|
15129
|
+
const cmd = program2.command("dlp").description("Show secrets detected in Claude response text and mark them resolved");
|
|
15130
|
+
cmd.command("resolve").description("Mark all current DLP findings as resolved").action(() => {
|
|
15131
|
+
const findings = loadDlpFindings();
|
|
15132
|
+
if (findings.length === 0) {
|
|
15133
|
+
console.log(chalk24.green("\n \u2705 No response-DLP findings to resolve.\n"));
|
|
15134
|
+
return;
|
|
15135
|
+
}
|
|
15136
|
+
const resolved = loadResolved();
|
|
15137
|
+
for (const e of findings) resolved.add(entryKey(e));
|
|
15138
|
+
saveResolved(resolved);
|
|
15139
|
+
console.log(
|
|
15140
|
+
chalk24.green(
|
|
15141
|
+
`
|
|
15142
|
+
\u2705 ${findings.length} finding${findings.length !== 1 ? "s" : ""} marked as resolved.
|
|
15143
|
+
`
|
|
15144
|
+
)
|
|
15145
|
+
);
|
|
15146
|
+
});
|
|
15147
|
+
cmd.action(() => {
|
|
15148
|
+
const findings = loadDlpFindings();
|
|
15149
|
+
const resolved = loadResolved();
|
|
15150
|
+
const open = findings.filter((e) => !resolved.has(entryKey(e)));
|
|
15151
|
+
const resolvedCount = findings.length - open.length;
|
|
15152
|
+
console.log("");
|
|
15153
|
+
console.log(
|
|
15154
|
+
chalk24.bold.cyan("\u{1F510} node9 dlp") + chalk24.dim(" \u2014 secrets found in Claude response text")
|
|
15155
|
+
);
|
|
15156
|
+
console.log("");
|
|
15157
|
+
if (open.length === 0) {
|
|
15158
|
+
if (resolvedCount > 0) {
|
|
15159
|
+
console.log(chalk24.green(` \u2705 No open findings \xB7 ${resolvedCount} previously resolved`));
|
|
15160
|
+
} else {
|
|
15161
|
+
console.log(
|
|
15162
|
+
chalk24.green(" \u2705 No findings \u2014 Claude has not leaked secrets in response text")
|
|
15163
|
+
);
|
|
15164
|
+
}
|
|
15165
|
+
console.log("");
|
|
15166
|
+
return;
|
|
15167
|
+
}
|
|
15168
|
+
console.log(
|
|
15169
|
+
chalk24.bgRed.white.bold(` \u26A0\uFE0F ${open.length} open finding${open.length !== 1 ? "s" : ""} `) + chalk24.dim(resolvedCount > 0 ? ` (${resolvedCount} resolved)` : "")
|
|
15170
|
+
);
|
|
15171
|
+
console.log("");
|
|
15172
|
+
console.log(
|
|
15173
|
+
chalk24.dim(" These secrets were included in Claude's response text \u2014 NOT blocked.")
|
|
15174
|
+
);
|
|
15175
|
+
console.log(chalk24.dim(" Rotate each affected key immediately.\n"));
|
|
15176
|
+
for (const e of open) {
|
|
15177
|
+
console.log(
|
|
15178
|
+
" " + chalk24.red("\u25CF") + " " + chalk24.white(e.dlpPattern ?? "Secret") + chalk24.dim(" " + fmtDate3(e.ts))
|
|
15179
|
+
);
|
|
15180
|
+
if (e.dlpSample) {
|
|
15181
|
+
console.log(" " + chalk24.dim("Sample: ") + chalk24.yellow(stripAnsi(e.dlpSample)));
|
|
15182
|
+
}
|
|
15183
|
+
if (e.project) {
|
|
15184
|
+
console.log(" " + chalk24.dim("Project: ") + chalk24.dim(stripAnsi(e.project)));
|
|
15185
|
+
}
|
|
15186
|
+
console.log("");
|
|
15187
|
+
}
|
|
15188
|
+
console.log(" " + chalk24.bold("Next steps:"));
|
|
15189
|
+
console.log(" " + chalk24.cyan("1.") + " Rotate any exposed keys shown above");
|
|
15190
|
+
console.log(
|
|
15191
|
+
" " + chalk24.cyan("2.") + " Run " + chalk24.white("node9 dlp resolve") + " to acknowledge"
|
|
15192
|
+
);
|
|
15193
|
+
console.log(
|
|
15194
|
+
" " + chalk24.cyan("3.") + " Run " + chalk24.white("node9 report") + " for full audit history"
|
|
15195
|
+
);
|
|
15196
|
+
console.log("");
|
|
15197
|
+
});
|
|
15198
|
+
}
|
|
15199
|
+
|
|
14249
15200
|
// src/cli.ts
|
|
14250
15201
|
var { version } = JSON.parse(
|
|
14251
|
-
|
|
15202
|
+
fs39.readFileSync(path42.join(__dirname, "../package.json"), "utf-8")
|
|
14252
15203
|
);
|
|
14253
15204
|
var program = new Command();
|
|
14254
15205
|
program.name("node9").description("The Sudo Command for AI Agents").version(version);
|
|
14255
15206
|
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) => {
|
|
14256
15207
|
const DEFAULT_API_URL2 = "https://api.node9.ai/api/v1/intercept";
|
|
14257
|
-
const credPath =
|
|
14258
|
-
if (!
|
|
14259
|
-
|
|
15208
|
+
const credPath = path42.join(os35.homedir(), ".node9", "credentials.json");
|
|
15209
|
+
if (!fs39.existsSync(path42.dirname(credPath)))
|
|
15210
|
+
fs39.mkdirSync(path42.dirname(credPath), { recursive: true });
|
|
14260
15211
|
const profileName = options.profile || "default";
|
|
14261
15212
|
let existingCreds = {};
|
|
14262
15213
|
try {
|
|
14263
|
-
if (
|
|
14264
|
-
const raw = JSON.parse(
|
|
15214
|
+
if (fs39.existsSync(credPath)) {
|
|
15215
|
+
const raw = JSON.parse(fs39.readFileSync(credPath, "utf-8"));
|
|
14265
15216
|
if (raw.apiKey) {
|
|
14266
15217
|
existingCreds = {
|
|
14267
15218
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL2 }
|
|
@@ -14273,13 +15224,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
14273
15224
|
} catch {
|
|
14274
15225
|
}
|
|
14275
15226
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL2 };
|
|
14276
|
-
|
|
15227
|
+
fs39.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
14277
15228
|
if (profileName === "default") {
|
|
14278
|
-
const configPath =
|
|
15229
|
+
const configPath = path42.join(os35.homedir(), ".node9", "config.json");
|
|
14279
15230
|
let config = {};
|
|
14280
15231
|
try {
|
|
14281
|
-
if (
|
|
14282
|
-
config = JSON.parse(
|
|
15232
|
+
if (fs39.existsSync(configPath))
|
|
15233
|
+
config = JSON.parse(fs39.readFileSync(configPath, "utf-8"));
|
|
14283
15234
|
} catch {
|
|
14284
15235
|
}
|
|
14285
15236
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -14294,47 +15245,61 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
14294
15245
|
approvers.cloud = false;
|
|
14295
15246
|
}
|
|
14296
15247
|
s.approvers = approvers;
|
|
14297
|
-
if (!
|
|
14298
|
-
|
|
14299
|
-
|
|
15248
|
+
if (!fs39.existsSync(path42.dirname(configPath)))
|
|
15249
|
+
fs39.mkdirSync(path42.dirname(configPath), { recursive: true });
|
|
15250
|
+
fs39.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
14300
15251
|
}
|
|
14301
15252
|
if (options.profile && profileName !== "default") {
|
|
14302
|
-
console.log(
|
|
14303
|
-
console.log(
|
|
15253
|
+
console.log(chalk26.green(`\u2705 Profile "${profileName}" saved`));
|
|
15254
|
+
console.log(chalk26.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
14304
15255
|
} else if (options.local) {
|
|
14305
|
-
console.log(
|
|
14306
|
-
console.log(
|
|
15256
|
+
console.log(chalk26.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
15257
|
+
console.log(chalk26.gray(` All decisions stay on this machine.`));
|
|
14307
15258
|
} else {
|
|
14308
|
-
console.log(
|
|
14309
|
-
console.log(
|
|
15259
|
+
console.log(chalk26.green(`\u2705 Logged in \u2014 agent mode`));
|
|
15260
|
+
console.log(chalk26.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
14310
15261
|
}
|
|
14311
15262
|
});
|
|
14312
|
-
program.command("addto").description("Integrate Node9 with an AI agent").addHelpText(
|
|
15263
|
+
program.command("addto").description("Integrate Node9 with an AI agent").addHelpText(
|
|
15264
|
+
"after",
|
|
15265
|
+
"\n Supported targets: claude gemini cursor codex windsurf vscode hud"
|
|
15266
|
+
).argument(
|
|
15267
|
+
"<target>",
|
|
15268
|
+
"The agent to protect: claude | gemini | cursor | codex | windsurf | vscode | hud"
|
|
15269
|
+
).action(async (target) => {
|
|
14313
15270
|
if (target === "gemini") return await setupGemini();
|
|
14314
15271
|
if (target === "claude") return await setupClaude();
|
|
14315
15272
|
if (target === "cursor") return await setupCursor();
|
|
15273
|
+
if (target === "codex") return await setupCodex();
|
|
14316
15274
|
if (target === "windsurf") return await setupWindsurf();
|
|
14317
15275
|
if (target === "vscode") return await setupVSCode();
|
|
14318
15276
|
if (target === "hud") return setupHud();
|
|
14319
15277
|
console.error(
|
|
14320
|
-
|
|
14321
|
-
`Unknown target: "${target}". Supported: claude, gemini, cursor, windsurf, vscode, hud`
|
|
15278
|
+
chalk26.red(
|
|
15279
|
+
`Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hud`
|
|
14322
15280
|
)
|
|
14323
15281
|
);
|
|
14324
15282
|
process.exit(1);
|
|
14325
15283
|
});
|
|
14326
|
-
program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText(
|
|
15284
|
+
program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText(
|
|
15285
|
+
"after",
|
|
15286
|
+
"\n Supported targets: claude gemini cursor codex windsurf vscode hud"
|
|
15287
|
+
).argument(
|
|
15288
|
+
"[target]",
|
|
15289
|
+
"The agent to protect: claude | gemini | cursor | codex | windsurf | vscode | hud"
|
|
15290
|
+
).action(async (target) => {
|
|
14327
15291
|
if (!target) {
|
|
14328
|
-
console.log(
|
|
14329
|
-
console.log(" Usage: " +
|
|
15292
|
+
console.log(chalk26.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
15293
|
+
console.log(" Usage: " + chalk26.white("node9 setup <target>") + "\n");
|
|
14330
15294
|
console.log(" Targets:");
|
|
14331
|
-
console.log(" " +
|
|
14332
|
-
console.log(" " +
|
|
14333
|
-
console.log(" " +
|
|
14334
|
-
console.log(" " +
|
|
14335
|
-
console.log(" " +
|
|
15295
|
+
console.log(" " + chalk26.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
15296
|
+
console.log(" " + chalk26.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
15297
|
+
console.log(" " + chalk26.green("cursor") + " \u2014 Cursor (MCP proxy)");
|
|
15298
|
+
console.log(" " + chalk26.green("codex") + " \u2014 OpenAI Codex CLI (MCP proxy)");
|
|
15299
|
+
console.log(" " + chalk26.green("windsurf") + " \u2014 Windsurf (MCP proxy)");
|
|
15300
|
+
console.log(" " + chalk26.green("vscode") + " \u2014 VSCode / Copilot (MCP proxy)");
|
|
14336
15301
|
process.stdout.write(
|
|
14337
|
-
" " +
|
|
15302
|
+
" " + chalk26.green("hud") + " \u2014 Claude Code security statusline\n"
|
|
14338
15303
|
);
|
|
14339
15304
|
console.log("");
|
|
14340
15305
|
return;
|
|
@@ -14343,61 +15308,67 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
14343
15308
|
if (t === "gemini") return await setupGemini();
|
|
14344
15309
|
if (t === "claude") return await setupClaude();
|
|
14345
15310
|
if (t === "cursor") return await setupCursor();
|
|
15311
|
+
if (t === "codex") return await setupCodex();
|
|
14346
15312
|
if (t === "windsurf") return await setupWindsurf();
|
|
14347
15313
|
if (t === "vscode") return await setupVSCode();
|
|
14348
15314
|
if (t === "hud") return setupHud();
|
|
14349
15315
|
console.error(
|
|
14350
|
-
|
|
14351
|
-
`Unknown target: "${target}". Supported: claude, gemini, cursor, windsurf, vscode, hud`
|
|
15316
|
+
chalk26.red(
|
|
15317
|
+
`Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hud`
|
|
14352
15318
|
)
|
|
14353
15319
|
);
|
|
14354
15320
|
process.exit(1);
|
|
14355
15321
|
});
|
|
14356
|
-
program.command("removefrom").description("Remove Node9 hooks from an AI agent configuration").addHelpText(
|
|
15322
|
+
program.command("removefrom").description("Remove Node9 hooks from an AI agent configuration").addHelpText(
|
|
15323
|
+
"after",
|
|
15324
|
+
"\n Supported targets: claude gemini cursor codex windsurf vscode hud"
|
|
15325
|
+
).argument(
|
|
14357
15326
|
"<target>",
|
|
14358
|
-
"The agent to remove from: claude | gemini | cursor | windsurf | vscode | hud"
|
|
15327
|
+
"The agent to remove from: claude | gemini | cursor | codex | windsurf | vscode | hud"
|
|
14359
15328
|
).action((target) => {
|
|
14360
15329
|
let fn;
|
|
14361
15330
|
if (target === "claude") fn = teardownClaude;
|
|
14362
15331
|
else if (target === "gemini") fn = teardownGemini;
|
|
14363
15332
|
else if (target === "cursor") fn = teardownCursor;
|
|
15333
|
+
else if (target === "codex") fn = teardownCodex;
|
|
14364
15334
|
else if (target === "windsurf") fn = teardownWindsurf;
|
|
14365
15335
|
else if (target === "vscode") fn = teardownVSCode;
|
|
14366
15336
|
else if (target === "hud") fn = teardownHud;
|
|
14367
15337
|
else {
|
|
14368
15338
|
console.error(
|
|
14369
|
-
|
|
14370
|
-
`Unknown target: "${target}". Supported: claude, gemini, cursor, windsurf, vscode, hud`
|
|
15339
|
+
chalk26.red(
|
|
15340
|
+
`Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hud`
|
|
14371
15341
|
)
|
|
14372
15342
|
);
|
|
14373
15343
|
process.exit(1);
|
|
14374
15344
|
}
|
|
14375
|
-
console.log(
|
|
15345
|
+
console.log(chalk26.cyan(`
|
|
14376
15346
|
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
14377
15347
|
`));
|
|
14378
15348
|
try {
|
|
14379
15349
|
fn();
|
|
14380
15350
|
} catch (err2) {
|
|
14381
|
-
console.error(
|
|
15351
|
+
console.error(chalk26.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
14382
15352
|
process.exit(1);
|
|
14383
15353
|
}
|
|
14384
|
-
console.log(
|
|
15354
|
+
console.log(chalk26.gray("\n Restart the agent for changes to take effect."));
|
|
14385
15355
|
});
|
|
14386
15356
|
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) => {
|
|
14387
|
-
console.log(
|
|
14388
|
-
console.log(
|
|
15357
|
+
console.log(chalk26.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
15358
|
+
console.log(chalk26.bold("Stopping daemon..."));
|
|
14389
15359
|
try {
|
|
14390
15360
|
stopDaemon();
|
|
14391
|
-
console.log(
|
|
15361
|
+
console.log(chalk26.green(" \u2705 Daemon stopped"));
|
|
14392
15362
|
} catch {
|
|
14393
|
-
console.log(
|
|
15363
|
+
console.log(chalk26.blue(" \u2139\uFE0F Daemon was not running"));
|
|
14394
15364
|
}
|
|
14395
|
-
console.log(
|
|
15365
|
+
console.log(chalk26.bold("\nRemoving hooks..."));
|
|
14396
15366
|
let teardownFailed = false;
|
|
14397
15367
|
for (const [label, fn] of [
|
|
14398
15368
|
["Claude", teardownClaude],
|
|
14399
15369
|
["Gemini", teardownGemini],
|
|
14400
15370
|
["Cursor", teardownCursor],
|
|
15371
|
+
["Codex", teardownCodex],
|
|
14401
15372
|
["Windsurf", teardownWindsurf],
|
|
14402
15373
|
["VSCode", teardownVSCode]
|
|
14403
15374
|
]) {
|
|
@@ -14406,45 +15377,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
14406
15377
|
} catch (err2) {
|
|
14407
15378
|
teardownFailed = true;
|
|
14408
15379
|
console.error(
|
|
14409
|
-
|
|
15380
|
+
chalk26.red(
|
|
14410
15381
|
` \u26A0\uFE0F Failed to remove ${label} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
14411
15382
|
)
|
|
14412
15383
|
);
|
|
14413
15384
|
}
|
|
14414
15385
|
}
|
|
14415
15386
|
if (options.purge) {
|
|
14416
|
-
const node9Dir =
|
|
14417
|
-
if (
|
|
15387
|
+
const node9Dir = path42.join(os35.homedir(), ".node9");
|
|
15388
|
+
if (fs39.existsSync(node9Dir)) {
|
|
14418
15389
|
const confirmed = await confirm2({
|
|
14419
15390
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
14420
15391
|
default: false
|
|
14421
15392
|
});
|
|
14422
15393
|
if (confirmed) {
|
|
14423
|
-
|
|
14424
|
-
if (
|
|
15394
|
+
fs39.rmSync(node9Dir, { recursive: true });
|
|
15395
|
+
if (fs39.existsSync(node9Dir)) {
|
|
14425
15396
|
console.error(
|
|
14426
|
-
|
|
15397
|
+
chalk26.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
14427
15398
|
);
|
|
14428
15399
|
} else {
|
|
14429
|
-
console.log(
|
|
15400
|
+
console.log(chalk26.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
14430
15401
|
}
|
|
14431
15402
|
} else {
|
|
14432
|
-
console.log(
|
|
15403
|
+
console.log(chalk26.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
14433
15404
|
}
|
|
14434
15405
|
} else {
|
|
14435
|
-
console.log(
|
|
15406
|
+
console.log(chalk26.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
14436
15407
|
}
|
|
14437
15408
|
} else {
|
|
14438
15409
|
console.log(
|
|
14439
|
-
|
|
15410
|
+
chalk26.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
14440
15411
|
);
|
|
14441
15412
|
}
|
|
14442
15413
|
if (teardownFailed) {
|
|
14443
|
-
console.error(
|
|
15414
|
+
console.error(chalk26.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
14444
15415
|
process.exit(1);
|
|
14445
15416
|
}
|
|
14446
|
-
console.log(
|
|
14447
|
-
console.log(
|
|
15417
|
+
console.log(chalk26.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
|
|
15418
|
+
console.log(chalk26.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
|
|
14448
15419
|
});
|
|
14449
15420
|
registerDoctorCommand(program, version);
|
|
14450
15421
|
program.command("explain").description(
|
|
@@ -14457,7 +15428,7 @@ program.command("explain").description(
|
|
|
14457
15428
|
try {
|
|
14458
15429
|
args = JSON.parse(trimmed);
|
|
14459
15430
|
} catch {
|
|
14460
|
-
console.error(
|
|
15431
|
+
console.error(chalk26.red(`
|
|
14461
15432
|
\u274C Invalid JSON: ${trimmed}
|
|
14462
15433
|
`));
|
|
14463
15434
|
process.exit(1);
|
|
@@ -14468,54 +15439,54 @@ program.command("explain").description(
|
|
|
14468
15439
|
}
|
|
14469
15440
|
const result = await explainPolicy(tool, args);
|
|
14470
15441
|
console.log("");
|
|
14471
|
-
console.log(
|
|
15442
|
+
console.log(chalk26.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
14472
15443
|
console.log("");
|
|
14473
|
-
console.log(` ${
|
|
15444
|
+
console.log(` ${chalk26.bold("Tool:")} ${chalk26.white(result.tool)}`);
|
|
14474
15445
|
if (argsRaw) {
|
|
14475
15446
|
const preview2 = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
14476
|
-
console.log(` ${
|
|
15447
|
+
console.log(` ${chalk26.bold("Input:")} ${chalk26.gray(preview2)}`);
|
|
14477
15448
|
}
|
|
14478
15449
|
console.log("");
|
|
14479
|
-
console.log(
|
|
15450
|
+
console.log(chalk26.bold("Config Sources (Waterfall):"));
|
|
14480
15451
|
for (const tier of result.waterfall) {
|
|
14481
|
-
const num3 =
|
|
15452
|
+
const num3 = chalk26.gray(` ${tier.tier}.`);
|
|
14482
15453
|
const label = tier.label.padEnd(16);
|
|
14483
15454
|
let statusStr;
|
|
14484
15455
|
if (tier.tier === 1) {
|
|
14485
|
-
statusStr =
|
|
15456
|
+
statusStr = chalk26.gray(tier.note ?? "");
|
|
14486
15457
|
} else if (tier.status === "active") {
|
|
14487
|
-
const loc = tier.path ?
|
|
14488
|
-
const note = tier.note ?
|
|
14489
|
-
statusStr =
|
|
15458
|
+
const loc = tier.path ? chalk26.gray(tier.path) : "";
|
|
15459
|
+
const note = tier.note ? chalk26.gray(`(${tier.note})`) : "";
|
|
15460
|
+
statusStr = chalk26.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
|
|
14490
15461
|
} else {
|
|
14491
|
-
statusStr =
|
|
15462
|
+
statusStr = chalk26.gray("\u25CB " + (tier.note ?? "not found"));
|
|
14492
15463
|
}
|
|
14493
|
-
console.log(`${num3} ${
|
|
15464
|
+
console.log(`${num3} ${chalk26.white(label)} ${statusStr}`);
|
|
14494
15465
|
}
|
|
14495
15466
|
console.log("");
|
|
14496
|
-
console.log(
|
|
15467
|
+
console.log(chalk26.bold("Policy Evaluation:"));
|
|
14497
15468
|
for (const step of result.steps) {
|
|
14498
15469
|
const isFinal = step.isFinal;
|
|
14499
15470
|
let icon;
|
|
14500
|
-
if (step.outcome === "allow") icon =
|
|
14501
|
-
else if (step.outcome === "review") icon =
|
|
14502
|
-
else if (step.outcome === "skip") icon =
|
|
14503
|
-
else icon =
|
|
15471
|
+
if (step.outcome === "allow") icon = chalk26.green(" \u2705");
|
|
15472
|
+
else if (step.outcome === "review") icon = chalk26.red(" \u{1F534}");
|
|
15473
|
+
else if (step.outcome === "skip") icon = chalk26.gray(" \u2500 ");
|
|
15474
|
+
else icon = chalk26.gray(" \u25CB ");
|
|
14504
15475
|
const name = step.name.padEnd(18);
|
|
14505
|
-
const nameStr = isFinal ?
|
|
14506
|
-
const detail = isFinal ?
|
|
14507
|
-
const arrow = isFinal ?
|
|
15476
|
+
const nameStr = isFinal ? chalk26.white.bold(name) : chalk26.white(name);
|
|
15477
|
+
const detail = isFinal ? chalk26.white(step.detail) : chalk26.gray(step.detail);
|
|
15478
|
+
const arrow = isFinal ? chalk26.yellow(" \u2190 STOP") : "";
|
|
14508
15479
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
14509
15480
|
}
|
|
14510
15481
|
console.log("");
|
|
14511
15482
|
if (result.decision === "allow") {
|
|
14512
|
-
console.log(
|
|
15483
|
+
console.log(chalk26.green.bold(" Decision: \u2705 ALLOW") + chalk26.gray(" \u2014 no approval needed"));
|
|
14513
15484
|
} else {
|
|
14514
15485
|
console.log(
|
|
14515
|
-
|
|
15486
|
+
chalk26.red.bold(" Decision: \u{1F534} REVIEW") + chalk26.gray(" \u2014 human approval required")
|
|
14516
15487
|
);
|
|
14517
15488
|
if (result.blockedByLabel) {
|
|
14518
|
-
console.log(
|
|
15489
|
+
console.log(chalk26.gray(` Reason: ${result.blockedByLabel}`));
|
|
14519
15490
|
}
|
|
14520
15491
|
}
|
|
14521
15492
|
console.log("");
|
|
@@ -14530,7 +15501,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
|
|
|
14530
15501
|
try {
|
|
14531
15502
|
await startTail2(options);
|
|
14532
15503
|
} catch (err2) {
|
|
14533
|
-
console.error(
|
|
15504
|
+
console.error(chalk26.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
14534
15505
|
process.exit(1);
|
|
14535
15506
|
}
|
|
14536
15507
|
});
|
|
@@ -14538,6 +15509,7 @@ registerWatchCommand(program);
|
|
|
14538
15509
|
registerMcpGatewayCommand(program);
|
|
14539
15510
|
registerMcpServerCommand(program);
|
|
14540
15511
|
registerMcpPinCommand(program);
|
|
15512
|
+
registerSkillPinCommand(program);
|
|
14541
15513
|
registerCheckCommand(program);
|
|
14542
15514
|
registerLogCommand(program);
|
|
14543
15515
|
program.command("hud").description("Render node9 security statusline (spawned by Claude Code statusLine)").addHelpText(
|
|
@@ -14562,14 +15534,14 @@ Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
|
|
|
14562
15534
|
Run "node9 addto claude" to register it as the statusLine.`
|
|
14563
15535
|
).argument("[subcommand]", 'Optional: "debug on" / "debug off" to toggle stdin logging').argument("[state]", 'on|off \u2014 used with "debug" subcommand').action(async (subcommand, state) => {
|
|
14564
15536
|
if (subcommand === "debug") {
|
|
14565
|
-
const flagFile =
|
|
15537
|
+
const flagFile = path42.join(os35.homedir(), ".node9", "hud-debug");
|
|
14566
15538
|
if (state === "on") {
|
|
14567
|
-
|
|
14568
|
-
|
|
15539
|
+
fs39.mkdirSync(path42.dirname(flagFile), { recursive: true });
|
|
15540
|
+
fs39.writeFileSync(flagFile, "");
|
|
14569
15541
|
console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
|
|
14570
15542
|
console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
|
|
14571
15543
|
} else if (state === "off") {
|
|
14572
|
-
if (
|
|
15544
|
+
if (fs39.existsSync(flagFile)) fs39.unlinkSync(flagFile);
|
|
14573
15545
|
console.log("HUD debug logging disabled.");
|
|
14574
15546
|
} else {
|
|
14575
15547
|
console.error("Usage: node9 hud debug on|off");
|
|
@@ -14584,7 +15556,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
14584
15556
|
const ms = parseDuration(options.duration);
|
|
14585
15557
|
if (ms === null) {
|
|
14586
15558
|
console.error(
|
|
14587
|
-
|
|
15559
|
+
chalk26.red(`
|
|
14588
15560
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
14589
15561
|
`)
|
|
14590
15562
|
);
|
|
@@ -14592,20 +15564,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
14592
15564
|
}
|
|
14593
15565
|
pauseNode9(ms, options.duration);
|
|
14594
15566
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
14595
|
-
console.log(
|
|
15567
|
+
console.log(chalk26.yellow(`
|
|
14596
15568
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
14597
|
-
console.log(
|
|
14598
|
-
console.log(
|
|
15569
|
+
console.log(chalk26.gray(` All tool calls will be allowed without review.`));
|
|
15570
|
+
console.log(chalk26.gray(` Run "node9 resume" to re-enable early.
|
|
14599
15571
|
`));
|
|
14600
15572
|
});
|
|
14601
15573
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
14602
15574
|
const { paused } = checkPause();
|
|
14603
15575
|
if (!paused) {
|
|
14604
|
-
console.log(
|
|
15576
|
+
console.log(chalk26.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
14605
15577
|
return;
|
|
14606
15578
|
}
|
|
14607
15579
|
resumeNode9();
|
|
14608
|
-
console.log(
|
|
15580
|
+
console.log(chalk26.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
14609
15581
|
});
|
|
14610
15582
|
var HOOK_BASED_AGENTS = {
|
|
14611
15583
|
claude: "claude",
|
|
@@ -14618,15 +15590,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
14618
15590
|
if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
|
|
14619
15591
|
const target = HOOK_BASED_AGENTS[firstArg2];
|
|
14620
15592
|
console.error(
|
|
14621
|
-
|
|
15593
|
+
chalk26.yellow(`
|
|
14622
15594
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
14623
15595
|
);
|
|
14624
|
-
console.error(
|
|
15596
|
+
console.error(chalk26.white(`
|
|
14625
15597
|
"${target}" uses its own hook system. Use:`));
|
|
14626
15598
|
console.error(
|
|
14627
|
-
|
|
15599
|
+
chalk26.green(` node9 addto ${target} `) + chalk26.gray("# one-time setup")
|
|
14628
15600
|
);
|
|
14629
|
-
console.error(
|
|
15601
|
+
console.error(chalk26.green(` ${target} `) + chalk26.gray("# run normally"));
|
|
14630
15602
|
process.exit(1);
|
|
14631
15603
|
}
|
|
14632
15604
|
const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
|
|
@@ -14643,7 +15615,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
14643
15615
|
}
|
|
14644
15616
|
);
|
|
14645
15617
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
14646
|
-
console.error(
|
|
15618
|
+
console.error(chalk26.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
14647
15619
|
const daemonReady = await autoStartDaemonAndWait();
|
|
14648
15620
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
14649
15621
|
}
|
|
@@ -14656,12 +15628,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
14656
15628
|
}
|
|
14657
15629
|
if (!result.approved) {
|
|
14658
15630
|
console.error(
|
|
14659
|
-
|
|
15631
|
+
chalk26.red(`
|
|
14660
15632
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
14661
15633
|
);
|
|
14662
15634
|
process.exit(1);
|
|
14663
15635
|
}
|
|
14664
|
-
console.error(
|
|
15636
|
+
console.error(chalk26.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
14665
15637
|
await runProxy(fullCommand);
|
|
14666
15638
|
} else {
|
|
14667
15639
|
program.help();
|
|
@@ -14675,14 +15647,15 @@ registerSyncCommand(program);
|
|
|
14675
15647
|
registerAgentsCommand(program);
|
|
14676
15648
|
registerScanCommand(program);
|
|
14677
15649
|
registerSessionsCommand(program);
|
|
15650
|
+
registerDlpCommand(program);
|
|
14678
15651
|
if (process.argv[2] !== "daemon") {
|
|
14679
15652
|
process.on("unhandledRejection", (reason) => {
|
|
14680
15653
|
const isCheckHook = process.argv[2] === "check";
|
|
14681
15654
|
if (isCheckHook) {
|
|
14682
15655
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
14683
|
-
const logPath =
|
|
15656
|
+
const logPath = path42.join(os35.homedir(), ".node9", "hook-debug.log");
|
|
14684
15657
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
14685
|
-
|
|
15658
|
+
fs39.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
14686
15659
|
`);
|
|
14687
15660
|
}
|
|
14688
15661
|
process.exit(0);
|