@node9/proxy 1.9.3 → 1.10.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 +32 -3
- package/dist/cli.js +1372 -625
- package/dist/cli.mjs +1354 -607
- package/dist/index.js +128 -25
- package/dist/index.mjs +126 -23
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -57,6 +57,11 @@ __export(audit_exports, {
|
|
|
57
57
|
import fs from "fs";
|
|
58
58
|
import path from "path";
|
|
59
59
|
import os from "os";
|
|
60
|
+
function isTestCall(toolName, args) {
|
|
61
|
+
if (toolName !== "Bash" && toolName !== "bash") return false;
|
|
62
|
+
const cmd = args?.command;
|
|
63
|
+
return typeof cmd === "string" && TEST_COMMAND_RE.test(cmd);
|
|
64
|
+
}
|
|
60
65
|
function redactSecrets(text) {
|
|
61
66
|
if (!text) return text;
|
|
62
67
|
let redacted = text;
|
|
@@ -92,12 +97,14 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
|
|
|
92
97
|
}
|
|
93
98
|
function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
|
|
94
99
|
const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
|
|
100
|
+
const testRun = isTestCall(toolName, args) ? { testRun: true } : {};
|
|
95
101
|
appendToLog(LOCAL_AUDIT_LOG, {
|
|
96
102
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
97
103
|
tool: toolName,
|
|
98
104
|
...argsField,
|
|
99
105
|
decision,
|
|
100
106
|
checkedBy,
|
|
107
|
+
...testRun,
|
|
101
108
|
agent: meta?.agent,
|
|
102
109
|
mcpServer: meta?.mcpServer,
|
|
103
110
|
hostname: os.hostname()
|
|
@@ -110,13 +117,14 @@ function appendConfigAudit(entry) {
|
|
|
110
117
|
hostname: os.hostname()
|
|
111
118
|
});
|
|
112
119
|
}
|
|
113
|
-
var LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG;
|
|
120
|
+
var LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG, TEST_COMMAND_RE;
|
|
114
121
|
var init_audit = __esm({
|
|
115
122
|
"src/audit/index.ts"() {
|
|
116
123
|
"use strict";
|
|
117
124
|
init_hasher();
|
|
118
125
|
LOCAL_AUDIT_LOG = path.join(os.homedir(), ".node9", "audit.log");
|
|
119
126
|
HOOK_DEBUG_LOG = path.join(os.homedir(), ".node9", "hook-debug.log");
|
|
127
|
+
TEST_COMMAND_RE = /(?:^|\s)(npm\s+(?:run\s+)?test|npx\s+(?:vitest|jest|mocha)|yarn\s+(?:run\s+)?test|pnpm\s+(?:run\s+)?test|vitest|jest|mocha|pytest|py\.test|cargo\s+test|go\s+test|bundle\s+exec\s+rspec|rspec|phpunit|dotnet\s+test)\b/i;
|
|
120
128
|
}
|
|
121
129
|
});
|
|
122
130
|
|
|
@@ -139,8 +147,8 @@ function sanitizeConfig(raw) {
|
|
|
139
147
|
}
|
|
140
148
|
}
|
|
141
149
|
const lines = result.error.issues.map((issue) => {
|
|
142
|
-
const
|
|
143
|
-
return ` \u2022 ${
|
|
150
|
+
const path35 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
151
|
+
return ` \u2022 ${path35}: ${issue.message}`;
|
|
144
152
|
});
|
|
145
153
|
return {
|
|
146
154
|
sanitized,
|
|
@@ -223,7 +231,8 @@ var init_config_schema = __esm({
|
|
|
223
231
|
slackEnabled: z.boolean().optional(),
|
|
224
232
|
enableTrustSessions: z.boolean().optional(),
|
|
225
233
|
allowGlobalPause: z.boolean().optional(),
|
|
226
|
-
auditHashArgs: z.boolean().optional()
|
|
234
|
+
auditHashArgs: z.boolean().optional(),
|
|
235
|
+
agentPolicy: z.enum(["require_approval", "block_on_rules"]).optional()
|
|
227
236
|
}).optional(),
|
|
228
237
|
policy: z.object({
|
|
229
238
|
sandboxPaths: z.array(z.string()).optional(),
|
|
@@ -239,6 +248,11 @@ var init_config_schema = __esm({
|
|
|
239
248
|
dlp: z.object({
|
|
240
249
|
enabled: z.boolean().optional(),
|
|
241
250
|
scanIgnoredTools: z.boolean().optional()
|
|
251
|
+
}).optional(),
|
|
252
|
+
loopDetection: z.object({
|
|
253
|
+
enabled: z.boolean().optional(),
|
|
254
|
+
threshold: z.number().min(2).optional(),
|
|
255
|
+
windowSeconds: z.number().min(10).optional()
|
|
242
256
|
}).optional()
|
|
243
257
|
}).optional(),
|
|
244
258
|
environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
@@ -536,7 +550,8 @@ function getConfig(cwd) {
|
|
|
536
550
|
onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
|
|
537
551
|
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
538
552
|
},
|
|
539
|
-
dlp: { ...DEFAULT_CONFIG.policy.dlp }
|
|
553
|
+
dlp: { ...DEFAULT_CONFIG.policy.dlp },
|
|
554
|
+
loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection }
|
|
540
555
|
};
|
|
541
556
|
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
542
557
|
const applyLayer = (source) => {
|
|
@@ -575,6 +590,13 @@ function getConfig(cwd) {
|
|
|
575
590
|
if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
|
|
576
591
|
if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
|
|
577
592
|
}
|
|
593
|
+
if (p.loopDetection) {
|
|
594
|
+
const ld = p.loopDetection;
|
|
595
|
+
if (ld.enabled !== void 0) mergedPolicy.loopDetection.enabled = ld.enabled;
|
|
596
|
+
if (ld.threshold !== void 0) mergedPolicy.loopDetection.threshold = ld.threshold;
|
|
597
|
+
if (ld.windowSeconds !== void 0)
|
|
598
|
+
mergedPolicy.loopDetection.windowSeconds = ld.windowSeconds;
|
|
599
|
+
}
|
|
578
600
|
const envs = source.environments || {};
|
|
579
601
|
for (const [envName, envConfig] of Object.entries(envs)) {
|
|
580
602
|
if (envConfig && typeof envConfig === "object") {
|
|
@@ -878,7 +900,8 @@ var init_config = __esm({
|
|
|
878
900
|
description: "The AI wants to download a script from the internet and run it immediately, without you seeing what it contains. This is one of the most common ways malware gets installed."
|
|
879
901
|
}
|
|
880
902
|
],
|
|
881
|
-
dlp: { enabled: true, scanIgnoredTools: true }
|
|
903
|
+
dlp: { enabled: true, scanIgnoredTools: true },
|
|
904
|
+
loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 }
|
|
882
905
|
},
|
|
883
906
|
environments: {}
|
|
884
907
|
};
|
|
@@ -1687,9 +1710,9 @@ function matchesPattern(text, patterns) {
|
|
|
1687
1710
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1688
1711
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1689
1712
|
}
|
|
1690
|
-
function getNestedValue(obj,
|
|
1713
|
+
function getNestedValue(obj, path35) {
|
|
1691
1714
|
if (!obj || typeof obj !== "object") return null;
|
|
1692
|
-
return
|
|
1715
|
+
return path35.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1693
1716
|
}
|
|
1694
1717
|
function shouldSnapshot(toolName, args, config) {
|
|
1695
1718
|
if (!config.settings.enableUndo) return false;
|
|
@@ -2769,11 +2792,12 @@ ${smartTruncate(str, 500)}`
|
|
|
2769
2792
|
function escapePango(text) {
|
|
2770
2793
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2771
2794
|
}
|
|
2772
|
-
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
|
|
2795
|
+
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription) {
|
|
2773
2796
|
const lines = [];
|
|
2774
2797
|
if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
|
|
2775
2798
|
lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
|
|
2776
2799
|
lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
|
|
2800
|
+
if (ruleDescription) lines.push(`\u2139 ${ruleDescription}`);
|
|
2777
2801
|
lines.push("");
|
|
2778
2802
|
lines.push(formattedArgs);
|
|
2779
2803
|
if (allowCount >= 3) {
|
|
@@ -2786,7 +2810,7 @@ function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
2786
2810
|
}
|
|
2787
2811
|
return lines.join("\n");
|
|
2788
2812
|
}
|
|
2789
|
-
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
|
|
2813
|
+
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1, ruleDescription) {
|
|
2790
2814
|
const lines = [];
|
|
2791
2815
|
if (locked) {
|
|
2792
2816
|
lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
|
|
@@ -2796,6 +2820,7 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
2796
2820
|
`<b>\u{1F916} ${escapePango(agent || "AI Agent")}</b> | <b>\u{1F527} <tt>${escapePango(toolName)}</tt></b>`
|
|
2797
2821
|
);
|
|
2798
2822
|
lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
|
|
2823
|
+
if (ruleDescription) lines.push(`<i>\u2139 ${escapePango(ruleDescription)}</i>`);
|
|
2799
2824
|
lines.push("");
|
|
2800
2825
|
lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
|
|
2801
2826
|
if (allowCount >= 3) {
|
|
@@ -2812,7 +2837,7 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
2812
2837
|
}
|
|
2813
2838
|
return lines.join("\n");
|
|
2814
2839
|
}
|
|
2815
|
-
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1) {
|
|
2840
|
+
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1, ruleDescription) {
|
|
2816
2841
|
if (isTestEnv()) return "deny";
|
|
2817
2842
|
const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
|
|
2818
2843
|
const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
|
|
@@ -2823,7 +2848,8 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
|
|
|
2823
2848
|
agent,
|
|
2824
2849
|
explainableLabel,
|
|
2825
2850
|
locked,
|
|
2826
|
-
allowCount
|
|
2851
|
+
allowCount,
|
|
2852
|
+
ruleDescription
|
|
2827
2853
|
);
|
|
2828
2854
|
return new Promise((resolve) => {
|
|
2829
2855
|
let childProcess = null;
|
|
@@ -2857,7 +2883,8 @@ end run`;
|
|
|
2857
2883
|
agent,
|
|
2858
2884
|
explainableLabel,
|
|
2859
2885
|
locked,
|
|
2860
|
-
allowCount
|
|
2886
|
+
allowCount,
|
|
2887
|
+
ruleDescription
|
|
2861
2888
|
);
|
|
2862
2889
|
const argsList = [
|
|
2863
2890
|
locked ? "--info" : "--question",
|
|
@@ -2931,7 +2958,7 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
2931
2958
|
}).catch(() => {
|
|
2932
2959
|
});
|
|
2933
2960
|
}
|
|
2934
|
-
async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
2961
|
+
async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPolicy, forceReview) {
|
|
2935
2962
|
const controller = new AbortController();
|
|
2936
2963
|
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
2937
2964
|
if (!creds.apiKey) throw new Error("Node9 API Key is missing");
|
|
@@ -2976,7 +3003,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
|
2976
3003
|
platform: os9.platform()
|
|
2977
3004
|
},
|
|
2978
3005
|
...riskMetadata && { riskMetadata },
|
|
2979
|
-
...ciContext && { ciContext }
|
|
3006
|
+
...ciContext && { ciContext },
|
|
3007
|
+
...agentPolicy && { policy: agentPolicy },
|
|
3008
|
+
...forceReview && { forceReview: true }
|
|
2980
3009
|
}),
|
|
2981
3010
|
signal: controller.signal
|
|
2982
3011
|
});
|
|
@@ -3056,6 +3085,58 @@ var init_cloud = __esm({
|
|
|
3056
3085
|
}
|
|
3057
3086
|
});
|
|
3058
3087
|
|
|
3088
|
+
// src/loop-detector.ts
|
|
3089
|
+
import fs11 from "fs";
|
|
3090
|
+
import path14 from "path";
|
|
3091
|
+
import os10 from "os";
|
|
3092
|
+
import crypto2 from "crypto";
|
|
3093
|
+
function loopStateFile() {
|
|
3094
|
+
return path14.join(os10.homedir(), ".node9", "loop-state.json");
|
|
3095
|
+
}
|
|
3096
|
+
function computeArgsHash(args) {
|
|
3097
|
+
const str = JSON.stringify(args ?? "");
|
|
3098
|
+
return crypto2.createHash("sha256").update(str).digest("hex").slice(0, 16);
|
|
3099
|
+
}
|
|
3100
|
+
function readState() {
|
|
3101
|
+
try {
|
|
3102
|
+
if (!fs11.existsSync(loopStateFile())) return [];
|
|
3103
|
+
const raw = fs11.readFileSync(loopStateFile(), "utf-8");
|
|
3104
|
+
const parsed = JSON.parse(raw);
|
|
3105
|
+
if (!Array.isArray(parsed)) return [];
|
|
3106
|
+
return parsed;
|
|
3107
|
+
} catch {
|
|
3108
|
+
return [];
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
function writeState(records) {
|
|
3112
|
+
const dir = path14.dirname(loopStateFile());
|
|
3113
|
+
if (!fs11.existsSync(dir)) fs11.mkdirSync(dir, { recursive: true });
|
|
3114
|
+
const tmpPath = `${loopStateFile()}.${os10.hostname()}.${process.pid}.tmp`;
|
|
3115
|
+
fs11.writeFileSync(tmpPath, JSON.stringify(records));
|
|
3116
|
+
fs11.renameSync(tmpPath, loopStateFile());
|
|
3117
|
+
}
|
|
3118
|
+
function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
3119
|
+
try {
|
|
3120
|
+
const hash = computeArgsHash(args);
|
|
3121
|
+
const now = Date.now();
|
|
3122
|
+
const cutoff = now - windowMs;
|
|
3123
|
+
const records = readState().filter((r) => r.ts >= cutoff);
|
|
3124
|
+
records.push({ t: tool, h: hash, ts: now });
|
|
3125
|
+
const count = records.filter((r) => r.t === tool && r.h === hash).length;
|
|
3126
|
+
writeState(records.slice(-MAX_RECORDS));
|
|
3127
|
+
return { looping: count >= threshold, count };
|
|
3128
|
+
} catch {
|
|
3129
|
+
return { looping: false, count: 0 };
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
var MAX_RECORDS;
|
|
3133
|
+
var init_loop_detector = __esm({
|
|
3134
|
+
"src/loop-detector.ts"() {
|
|
3135
|
+
"use strict";
|
|
3136
|
+
MAX_RECORDS = 500;
|
|
3137
|
+
}
|
|
3138
|
+
});
|
|
3139
|
+
|
|
3059
3140
|
// src/auth/orchestrator.ts
|
|
3060
3141
|
import { randomUUID } from "crypto";
|
|
3061
3142
|
function isWriteTool(toolName) {
|
|
@@ -3144,6 +3225,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3144
3225
|
let explainableLabel = "Local Config";
|
|
3145
3226
|
let policyMatchedField;
|
|
3146
3227
|
let policyMatchedWord;
|
|
3228
|
+
let policyRuleDescription;
|
|
3147
3229
|
let riskMetadata;
|
|
3148
3230
|
let statefulRecoveryCommand;
|
|
3149
3231
|
let localSmartRuleMatched = false;
|
|
@@ -3237,6 +3319,21 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3237
3319
|
return { approved: true, checkedBy: "audit" };
|
|
3238
3320
|
}
|
|
3239
3321
|
if (!taintWarning && !isIgnoredTool(toolName)) {
|
|
3322
|
+
const ld = config.policy.loopDetection;
|
|
3323
|
+
if (ld.enabled) {
|
|
3324
|
+
const loopResult = recordAndCheck(toolName, args, ld.threshold, ld.windowSeconds * 1e3);
|
|
3325
|
+
if (loopResult.looping) {
|
|
3326
|
+
const reason = `It looks like you've called "${toolName}" ${loopResult.count} times with identical arguments in the last ${ld.windowSeconds}s. Are you stuck? Step back and reconsider your approach \u2014 what are you actually trying to accomplish, and is there a different way to get there?`;
|
|
3327
|
+
if (!isManual)
|
|
3328
|
+
appendLocalAudit(toolName, args, "deny", "loop-detected", meta, hashAuditArgs);
|
|
3329
|
+
return {
|
|
3330
|
+
approved: false,
|
|
3331
|
+
reason,
|
|
3332
|
+
blockedBy: "loop-detection",
|
|
3333
|
+
blockedByLabel: "\u{1F504} Loop Detected"
|
|
3334
|
+
};
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3240
3337
|
if (getActiveTrustSession(toolName)) {
|
|
3241
3338
|
if (approvers.cloud && creds?.apiKey)
|
|
3242
3339
|
await auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
@@ -3292,6 +3389,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3292
3389
|
policyMatchedField = policyResult.matchedField;
|
|
3293
3390
|
policyMatchedWord = policyResult.matchedWord;
|
|
3294
3391
|
if (policyResult.ruleName) localSmartRuleMatched = true;
|
|
3392
|
+
if (policyResult.ruleDescription) policyRuleDescription = policyResult.ruleDescription;
|
|
3393
|
+
else if (policyResult.reason) policyRuleDescription = policyResult.reason;
|
|
3295
3394
|
riskMetadata = computeRiskMetadata(
|
|
3296
3395
|
args,
|
|
3297
3396
|
policyResult.tier ?? 6,
|
|
@@ -3300,6 +3399,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3300
3399
|
policyMatchedWord,
|
|
3301
3400
|
policyResult.ruleName
|
|
3302
3401
|
);
|
|
3402
|
+
if (policyRuleDescription) riskMetadata.ruleDescription = policyRuleDescription.slice(0, 200);
|
|
3303
3403
|
const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
|
|
3304
3404
|
if (persistent === "allow") {
|
|
3305
3405
|
if (approvers.cloud && creds?.apiKey)
|
|
@@ -3334,9 +3434,18 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3334
3434
|
}
|
|
3335
3435
|
let cloudRequestId = null;
|
|
3336
3436
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
3337
|
-
|
|
3437
|
+
const forceReview = localSmartRuleMatched === true || options?.localSmartRuleMatched === true || void 0;
|
|
3438
|
+
if (cloudEnforced) {
|
|
3338
3439
|
try {
|
|
3339
|
-
const initResult = await initNode9SaaS(
|
|
3440
|
+
const initResult = await initNode9SaaS(
|
|
3441
|
+
toolName,
|
|
3442
|
+
args,
|
|
3443
|
+
creds,
|
|
3444
|
+
meta,
|
|
3445
|
+
riskMetadata,
|
|
3446
|
+
config.settings.agentPolicy,
|
|
3447
|
+
forceReview
|
|
3448
|
+
);
|
|
3340
3449
|
if (!initResult.pending) {
|
|
3341
3450
|
if (initResult.shadowMode) {
|
|
3342
3451
|
return { approved: true, checkedBy: "cloud" };
|
|
@@ -3351,9 +3460,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3351
3460
|
};
|
|
3352
3461
|
}
|
|
3353
3462
|
}
|
|
3354
|
-
if (
|
|
3355
|
-
cloudRequestId = initResult.requestId || null;
|
|
3356
|
-
}
|
|
3463
|
+
if (initResult.pending) cloudRequestId = initResult.requestId || null;
|
|
3357
3464
|
if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
|
|
3358
3465
|
} catch {
|
|
3359
3466
|
}
|
|
@@ -3410,7 +3517,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3410
3517
|
}
|
|
3411
3518
|
}
|
|
3412
3519
|
}
|
|
3413
|
-
if (cloudEnforced && cloudRequestId
|
|
3520
|
+
if (cloudEnforced && cloudRequestId) {
|
|
3414
3521
|
racePromises.push(
|
|
3415
3522
|
(async () => {
|
|
3416
3523
|
try {
|
|
@@ -3442,7 +3549,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3442
3549
|
signal,
|
|
3443
3550
|
policyMatchedField,
|
|
3444
3551
|
policyMatchedWord,
|
|
3445
|
-
daemonAllowCount
|
|
3552
|
+
daemonAllowCount,
|
|
3553
|
+
riskMetadata?.ruleDescription
|
|
3446
3554
|
);
|
|
3447
3555
|
if (decision === "always_allow") {
|
|
3448
3556
|
writeTrustSession(toolName, 36e5);
|
|
@@ -3555,7 +3663,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
3555
3663
|
hashAuditArgs
|
|
3556
3664
|
);
|
|
3557
3665
|
}
|
|
3558
|
-
|
|
3666
|
+
const enrichedResult = !finalResult.approved && policyRuleDescription && !finalResult.ruleDescription ? { ...finalResult, ruleDescription: policyRuleDescription } : finalResult;
|
|
3667
|
+
return enrichedResult;
|
|
3559
3668
|
}
|
|
3560
3669
|
var WRITE_TOOLS;
|
|
3561
3670
|
var init_orchestrator = __esm({
|
|
@@ -3570,6 +3679,7 @@ var init_orchestrator = __esm({
|
|
|
3570
3679
|
init_state();
|
|
3571
3680
|
init_daemon();
|
|
3572
3681
|
init_cloud();
|
|
3682
|
+
init_loop_detector();
|
|
3573
3683
|
WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
3574
3684
|
"write",
|
|
3575
3685
|
"write_file",
|
|
@@ -5307,8 +5417,8 @@ var init_suggestion_tracker = __esm({
|
|
|
5307
5417
|
});
|
|
5308
5418
|
|
|
5309
5419
|
// src/daemon/taint-store.ts
|
|
5310
|
-
import
|
|
5311
|
-
import
|
|
5420
|
+
import fs13 from "fs";
|
|
5421
|
+
import path16 from "path";
|
|
5312
5422
|
var DEFAULT_TTL_MS, TaintStore;
|
|
5313
5423
|
var init_taint_store = __esm({
|
|
5314
5424
|
"src/daemon/taint-store.ts"() {
|
|
@@ -5377,9 +5487,9 @@ var init_taint_store = __esm({
|
|
|
5377
5487
|
/** Resolve to absolute path, falling back to path.resolve if file doesn't exist yet. */
|
|
5378
5488
|
_resolve(filePath) {
|
|
5379
5489
|
try {
|
|
5380
|
-
return
|
|
5490
|
+
return fs13.realpathSync.native(path16.resolve(filePath));
|
|
5381
5491
|
} catch {
|
|
5382
|
-
return
|
|
5492
|
+
return path16.resolve(filePath);
|
|
5383
5493
|
}
|
|
5384
5494
|
}
|
|
5385
5495
|
};
|
|
@@ -5496,15 +5606,15 @@ var init_session_history = __esm({
|
|
|
5496
5606
|
|
|
5497
5607
|
// src/daemon/state.ts
|
|
5498
5608
|
import net2 from "net";
|
|
5499
|
-
import
|
|
5500
|
-
import
|
|
5501
|
-
import
|
|
5609
|
+
import fs14 from "fs";
|
|
5610
|
+
import path17 from "path";
|
|
5611
|
+
import os12 from "os";
|
|
5502
5612
|
import { spawn as spawn2 } from "child_process";
|
|
5503
5613
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
5504
5614
|
function loadInsightCounts() {
|
|
5505
5615
|
try {
|
|
5506
|
-
if (!
|
|
5507
|
-
const data = JSON.parse(
|
|
5616
|
+
if (!fs14.existsSync(INSIGHT_COUNTS_FILE)) return;
|
|
5617
|
+
const data = JSON.parse(fs14.readFileSync(INSIGHT_COUNTS_FILE, "utf-8"));
|
|
5508
5618
|
for (const [tool, count] of Object.entries(data)) {
|
|
5509
5619
|
if (typeof count === "number" && count > 0) insightCounts.set(tool, count);
|
|
5510
5620
|
}
|
|
@@ -5543,23 +5653,23 @@ function markRejectionHandlerRegistered() {
|
|
|
5543
5653
|
daemonRejectionHandlerRegistered = true;
|
|
5544
5654
|
}
|
|
5545
5655
|
function atomicWriteSync2(filePath, data, options) {
|
|
5546
|
-
const dir =
|
|
5547
|
-
if (!
|
|
5656
|
+
const dir = path17.dirname(filePath);
|
|
5657
|
+
if (!fs14.existsSync(dir)) fs14.mkdirSync(dir, { recursive: true });
|
|
5548
5658
|
const tmpPath = `${filePath}.${randomUUID3()}.tmp`;
|
|
5549
5659
|
try {
|
|
5550
|
-
|
|
5660
|
+
fs14.writeFileSync(tmpPath, data, options);
|
|
5551
5661
|
} catch (err2) {
|
|
5552
5662
|
try {
|
|
5553
|
-
|
|
5663
|
+
fs14.unlinkSync(tmpPath);
|
|
5554
5664
|
} catch {
|
|
5555
5665
|
}
|
|
5556
5666
|
throw err2;
|
|
5557
5667
|
}
|
|
5558
5668
|
try {
|
|
5559
|
-
|
|
5669
|
+
fs14.renameSync(tmpPath, filePath);
|
|
5560
5670
|
} catch (err2) {
|
|
5561
5671
|
try {
|
|
5562
|
-
|
|
5672
|
+
fs14.unlinkSync(tmpPath);
|
|
5563
5673
|
} catch {
|
|
5564
5674
|
}
|
|
5565
5675
|
throw err2;
|
|
@@ -5583,16 +5693,16 @@ function appendAuditLog(data) {
|
|
|
5583
5693
|
decision: data.decision,
|
|
5584
5694
|
source: "daemon"
|
|
5585
5695
|
};
|
|
5586
|
-
const dir =
|
|
5587
|
-
if (!
|
|
5588
|
-
|
|
5696
|
+
const dir = path17.dirname(AUDIT_LOG_FILE);
|
|
5697
|
+
if (!fs14.existsSync(dir)) fs14.mkdirSync(dir, { recursive: true });
|
|
5698
|
+
fs14.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
|
|
5589
5699
|
} catch {
|
|
5590
5700
|
}
|
|
5591
5701
|
}
|
|
5592
5702
|
function getAuditHistory(limit = 20) {
|
|
5593
5703
|
try {
|
|
5594
|
-
if (!
|
|
5595
|
-
const lines =
|
|
5704
|
+
if (!fs14.existsSync(AUDIT_LOG_FILE)) return [];
|
|
5705
|
+
const lines = fs14.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
|
|
5596
5706
|
if (lines.length === 1 && lines[0] === "") return [];
|
|
5597
5707
|
return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
|
|
5598
5708
|
} catch {
|
|
@@ -5601,19 +5711,19 @@ function getAuditHistory(limit = 20) {
|
|
|
5601
5711
|
}
|
|
5602
5712
|
function getOrgName() {
|
|
5603
5713
|
try {
|
|
5604
|
-
if (
|
|
5714
|
+
if (fs14.existsSync(CREDENTIALS_FILE)) return "Node9 Cloud";
|
|
5605
5715
|
} catch {
|
|
5606
5716
|
}
|
|
5607
5717
|
return null;
|
|
5608
5718
|
}
|
|
5609
5719
|
function hasStoredSlackKey() {
|
|
5610
|
-
return
|
|
5720
|
+
return fs14.existsSync(CREDENTIALS_FILE);
|
|
5611
5721
|
}
|
|
5612
5722
|
function writeGlobalSetting(key, value) {
|
|
5613
5723
|
let config = {};
|
|
5614
5724
|
try {
|
|
5615
|
-
if (
|
|
5616
|
-
config = JSON.parse(
|
|
5725
|
+
if (fs14.existsSync(GLOBAL_CONFIG_FILE)) {
|
|
5726
|
+
config = JSON.parse(fs14.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
|
|
5617
5727
|
}
|
|
5618
5728
|
} catch {
|
|
5619
5729
|
}
|
|
@@ -5625,8 +5735,8 @@ function writeTrustEntry(toolName, durationMs) {
|
|
|
5625
5735
|
try {
|
|
5626
5736
|
let trust = { entries: [] };
|
|
5627
5737
|
try {
|
|
5628
|
-
if (
|
|
5629
|
-
trust = JSON.parse(
|
|
5738
|
+
if (fs14.existsSync(TRUST_FILE2))
|
|
5739
|
+
trust = JSON.parse(fs14.readFileSync(TRUST_FILE2, "utf-8"));
|
|
5630
5740
|
} catch {
|
|
5631
5741
|
}
|
|
5632
5742
|
trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
|
|
@@ -5637,8 +5747,8 @@ function writeTrustEntry(toolName, durationMs) {
|
|
|
5637
5747
|
}
|
|
5638
5748
|
function readPersistentDecisions() {
|
|
5639
5749
|
try {
|
|
5640
|
-
if (
|
|
5641
|
-
return JSON.parse(
|
|
5750
|
+
if (fs14.existsSync(DECISIONS_FILE)) {
|
|
5751
|
+
return JSON.parse(fs14.readFileSync(DECISIONS_FILE, "utf-8"));
|
|
5642
5752
|
}
|
|
5643
5753
|
} catch {
|
|
5644
5754
|
}
|
|
@@ -5675,7 +5785,7 @@ function estimateToolCost(tool, args) {
|
|
|
5675
5785
|
const filePath = a.file_path ?? a.path;
|
|
5676
5786
|
if (filePath) {
|
|
5677
5787
|
try {
|
|
5678
|
-
const bytes =
|
|
5788
|
+
const bytes = fs14.statSync(filePath).size;
|
|
5679
5789
|
return bytes / BYTES_PER_TOKEN / 1e6 * INPUT_PRICE_PER_1M;
|
|
5680
5790
|
} catch {
|
|
5681
5791
|
}
|
|
@@ -5733,7 +5843,7 @@ function abandonPending() {
|
|
|
5733
5843
|
});
|
|
5734
5844
|
if (autoStarted) {
|
|
5735
5845
|
try {
|
|
5736
|
-
|
|
5846
|
+
fs14.unlinkSync(DAEMON_PID_FILE);
|
|
5737
5847
|
} catch {
|
|
5738
5848
|
}
|
|
5739
5849
|
setTimeout(() => {
|
|
@@ -5744,7 +5854,7 @@ function abandonPending() {
|
|
|
5744
5854
|
}
|
|
5745
5855
|
function startActivitySocket() {
|
|
5746
5856
|
try {
|
|
5747
|
-
|
|
5857
|
+
fs14.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
5748
5858
|
} catch {
|
|
5749
5859
|
}
|
|
5750
5860
|
const ACTIVITY_MAX_BYTES = 1024 * 1024;
|
|
@@ -5825,7 +5935,7 @@ function startActivitySocket() {
|
|
|
5825
5935
|
unixServer.listen(ACTIVITY_SOCKET_PATH2);
|
|
5826
5936
|
process.on("exit", () => {
|
|
5827
5937
|
try {
|
|
5828
|
-
|
|
5938
|
+
fs14.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
5829
5939
|
} catch {
|
|
5830
5940
|
}
|
|
5831
5941
|
});
|
|
@@ -5839,14 +5949,14 @@ var init_state2 = __esm({
|
|
|
5839
5949
|
init_taint_store();
|
|
5840
5950
|
init_session_counters();
|
|
5841
5951
|
init_session_history();
|
|
5842
|
-
homeDir =
|
|
5843
|
-
DAEMON_PID_FILE =
|
|
5844
|
-
DECISIONS_FILE =
|
|
5845
|
-
AUDIT_LOG_FILE =
|
|
5846
|
-
TRUST_FILE2 =
|
|
5847
|
-
GLOBAL_CONFIG_FILE =
|
|
5848
|
-
CREDENTIALS_FILE =
|
|
5849
|
-
INSIGHT_COUNTS_FILE =
|
|
5952
|
+
homeDir = os12.homedir();
|
|
5953
|
+
DAEMON_PID_FILE = path17.join(homeDir, ".node9", "daemon.pid");
|
|
5954
|
+
DECISIONS_FILE = path17.join(homeDir, ".node9", "decisions.json");
|
|
5955
|
+
AUDIT_LOG_FILE = path17.join(homeDir, ".node9", "audit.log");
|
|
5956
|
+
TRUST_FILE2 = path17.join(homeDir, ".node9", "trust.json");
|
|
5957
|
+
GLOBAL_CONFIG_FILE = path17.join(homeDir, ".node9", "config.json");
|
|
5958
|
+
CREDENTIALS_FILE = path17.join(homeDir, ".node9", "credentials.json");
|
|
5959
|
+
INSIGHT_COUNTS_FILE = path17.join(homeDir, ".node9", "insight-counts.json");
|
|
5850
5960
|
pending = /* @__PURE__ */ new Map();
|
|
5851
5961
|
sseClients = /* @__PURE__ */ new Set();
|
|
5852
5962
|
suggestionTracker = new SuggestionTracker(3);
|
|
@@ -5864,7 +5974,7 @@ var init_state2 = __esm({
|
|
|
5864
5974
|
"2h": 2 * 60 * 6e4
|
|
5865
5975
|
};
|
|
5866
5976
|
autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
5867
|
-
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
5977
|
+
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path17.join(os12.tmpdir(), "node9-activity.sock");
|
|
5868
5978
|
ACTIVITY_RING_SIZE = 100;
|
|
5869
5979
|
activityRing = [];
|
|
5870
5980
|
SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
@@ -5886,14 +5996,14 @@ var init_state2 = __esm({
|
|
|
5886
5996
|
});
|
|
5887
5997
|
|
|
5888
5998
|
// src/config/patch.ts
|
|
5889
|
-
import
|
|
5890
|
-
import
|
|
5891
|
-
import
|
|
5999
|
+
import fs15 from "fs";
|
|
6000
|
+
import path18 from "path";
|
|
6001
|
+
import os13 from "os";
|
|
5892
6002
|
function patchConfig(configPath, patch) {
|
|
5893
6003
|
let config = {};
|
|
5894
6004
|
try {
|
|
5895
|
-
if (
|
|
5896
|
-
config = JSON.parse(
|
|
6005
|
+
if (fs15.existsSync(configPath)) {
|
|
6006
|
+
config = JSON.parse(fs15.readFileSync(configPath, "utf8"));
|
|
5897
6007
|
}
|
|
5898
6008
|
} catch {
|
|
5899
6009
|
throw new Error(`Cannot read config at ${configPath} \u2014 file may be corrupted`);
|
|
@@ -5912,23 +6022,23 @@ function patchConfig(configPath, patch) {
|
|
|
5912
6022
|
ignored.push(patch.toolName);
|
|
5913
6023
|
}
|
|
5914
6024
|
}
|
|
5915
|
-
const dir =
|
|
5916
|
-
|
|
6025
|
+
const dir = path18.dirname(configPath);
|
|
6026
|
+
fs15.mkdirSync(dir, { recursive: true });
|
|
5917
6027
|
const tmp = configPath + ".node9-tmp";
|
|
5918
6028
|
try {
|
|
5919
|
-
|
|
6029
|
+
fs15.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
|
|
5920
6030
|
} catch (err2) {
|
|
5921
6031
|
try {
|
|
5922
|
-
|
|
6032
|
+
fs15.unlinkSync(tmp);
|
|
5923
6033
|
} catch {
|
|
5924
6034
|
}
|
|
5925
6035
|
throw err2;
|
|
5926
6036
|
}
|
|
5927
6037
|
try {
|
|
5928
|
-
|
|
6038
|
+
fs15.renameSync(tmp, configPath);
|
|
5929
6039
|
} catch (err2) {
|
|
5930
6040
|
try {
|
|
5931
|
-
|
|
6041
|
+
fs15.unlinkSync(tmp);
|
|
5932
6042
|
} catch {
|
|
5933
6043
|
}
|
|
5934
6044
|
throw err2;
|
|
@@ -5938,18 +6048,185 @@ var GLOBAL_CONFIG_PATH;
|
|
|
5938
6048
|
var init_patch = __esm({
|
|
5939
6049
|
"src/config/patch.ts"() {
|
|
5940
6050
|
"use strict";
|
|
5941
|
-
GLOBAL_CONFIG_PATH =
|
|
6051
|
+
GLOBAL_CONFIG_PATH = path18.join(os13.homedir(), ".node9", "config.json");
|
|
6052
|
+
}
|
|
6053
|
+
});
|
|
6054
|
+
|
|
6055
|
+
// src/costSync.ts
|
|
6056
|
+
import fs16 from "fs";
|
|
6057
|
+
import path19 from "path";
|
|
6058
|
+
import os14 from "os";
|
|
6059
|
+
function normalizeModel(raw) {
|
|
6060
|
+
return raw.replace(/-\d{8}$/, "");
|
|
6061
|
+
}
|
|
6062
|
+
function pricingFor(model) {
|
|
6063
|
+
const norm = normalizeModel(model);
|
|
6064
|
+
if (PRICING[norm]) return PRICING[norm];
|
|
6065
|
+
let best = null;
|
|
6066
|
+
for (const key of Object.keys(PRICING)) {
|
|
6067
|
+
if (norm.startsWith(key) && (best === null || key.length > best.length)) best = key;
|
|
6068
|
+
}
|
|
6069
|
+
return best ? PRICING[best] : null;
|
|
6070
|
+
}
|
|
6071
|
+
function parseJSONLFile(filePath) {
|
|
6072
|
+
let content;
|
|
6073
|
+
try {
|
|
6074
|
+
content = fs16.readFileSync(filePath, "utf8");
|
|
6075
|
+
} catch {
|
|
6076
|
+
return /* @__PURE__ */ new Map();
|
|
6077
|
+
}
|
|
6078
|
+
const daily = /* @__PURE__ */ new Map();
|
|
6079
|
+
for (const line of content.split("\n")) {
|
|
6080
|
+
if (!line.trim()) continue;
|
|
6081
|
+
let row;
|
|
6082
|
+
try {
|
|
6083
|
+
row = JSON.parse(line);
|
|
6084
|
+
} catch {
|
|
6085
|
+
continue;
|
|
6086
|
+
}
|
|
6087
|
+
if (row["type"] !== "assistant") continue;
|
|
6088
|
+
const msg = row["message"];
|
|
6089
|
+
if (!msg?.["usage"] || typeof msg["model"] !== "string") continue;
|
|
6090
|
+
const usage = msg["usage"];
|
|
6091
|
+
const model = msg["model"];
|
|
6092
|
+
const timestamp = row["timestamp"];
|
|
6093
|
+
if (typeof timestamp !== "string" || timestamp.length < 10) continue;
|
|
6094
|
+
const date = timestamp.slice(0, 10);
|
|
6095
|
+
const p = pricingFor(model);
|
|
6096
|
+
if (!p) continue;
|
|
6097
|
+
const inp = Number(usage["input_tokens"] ?? 0);
|
|
6098
|
+
const out = Number(usage["output_tokens"] ?? 0);
|
|
6099
|
+
const cw = Number(usage["cache_creation_input_tokens"] ?? 0);
|
|
6100
|
+
const cr = Number(usage["cache_read_input_tokens"] ?? 0);
|
|
6101
|
+
const cost = inp * p[0] + out * p[1] + cw * p[2] + cr * p[3];
|
|
6102
|
+
const norm = normalizeModel(model);
|
|
6103
|
+
const key = `${date}::${norm}`;
|
|
6104
|
+
const prev = daily.get(key);
|
|
6105
|
+
if (prev) {
|
|
6106
|
+
prev.costUSD += cost;
|
|
6107
|
+
prev.inputTokens += inp;
|
|
6108
|
+
prev.outputTokens += out;
|
|
6109
|
+
prev.cacheWriteTokens += cw;
|
|
6110
|
+
prev.cacheReadTokens += cr;
|
|
6111
|
+
} else {
|
|
6112
|
+
daily.set(key, {
|
|
6113
|
+
date,
|
|
6114
|
+
model: norm,
|
|
6115
|
+
costUSD: cost,
|
|
6116
|
+
inputTokens: inp,
|
|
6117
|
+
outputTokens: out,
|
|
6118
|
+
cacheWriteTokens: cw,
|
|
6119
|
+
cacheReadTokens: cr
|
|
6120
|
+
});
|
|
6121
|
+
}
|
|
6122
|
+
}
|
|
6123
|
+
return daily;
|
|
6124
|
+
}
|
|
6125
|
+
function collectEntries() {
|
|
6126
|
+
const projectsDir = path19.join(os14.homedir(), ".claude", "projects");
|
|
6127
|
+
if (!fs16.existsSync(projectsDir)) return [];
|
|
6128
|
+
const combined = /* @__PURE__ */ new Map();
|
|
6129
|
+
let dirs;
|
|
6130
|
+
try {
|
|
6131
|
+
dirs = fs16.readdirSync(projectsDir);
|
|
6132
|
+
} catch {
|
|
6133
|
+
return [];
|
|
6134
|
+
}
|
|
6135
|
+
for (const dir of dirs) {
|
|
6136
|
+
const dirPath = path19.join(projectsDir, dir);
|
|
6137
|
+
try {
|
|
6138
|
+
if (!fs16.statSync(dirPath).isDirectory()) continue;
|
|
6139
|
+
} catch {
|
|
6140
|
+
continue;
|
|
6141
|
+
}
|
|
6142
|
+
let files;
|
|
6143
|
+
try {
|
|
6144
|
+
files = fs16.readdirSync(dirPath).filter((f) => f.endsWith(".jsonl"));
|
|
6145
|
+
} catch {
|
|
6146
|
+
continue;
|
|
6147
|
+
}
|
|
6148
|
+
for (const file of files) {
|
|
6149
|
+
const entries = parseJSONLFile(path19.join(dirPath, file));
|
|
6150
|
+
for (const [key, e] of entries) {
|
|
6151
|
+
const prev = combined.get(key);
|
|
6152
|
+
if (prev) {
|
|
6153
|
+
prev.costUSD += e.costUSD;
|
|
6154
|
+
prev.inputTokens += e.inputTokens;
|
|
6155
|
+
prev.outputTokens += e.outputTokens;
|
|
6156
|
+
prev.cacheWriteTokens += e.cacheWriteTokens;
|
|
6157
|
+
prev.cacheReadTokens += e.cacheReadTokens;
|
|
6158
|
+
} else {
|
|
6159
|
+
combined.set(key, { ...e });
|
|
6160
|
+
}
|
|
6161
|
+
}
|
|
6162
|
+
}
|
|
6163
|
+
}
|
|
6164
|
+
return [...combined.values()];
|
|
6165
|
+
}
|
|
6166
|
+
async function syncCost() {
|
|
6167
|
+
const creds = getCredentials();
|
|
6168
|
+
if (!creds?.apiKey || !creds?.apiUrl) return;
|
|
6169
|
+
const entries = collectEntries();
|
|
6170
|
+
if (entries.length === 0) return;
|
|
6171
|
+
let username = "unknown";
|
|
6172
|
+
try {
|
|
6173
|
+
username = os14.userInfo().username;
|
|
6174
|
+
} catch {
|
|
6175
|
+
}
|
|
6176
|
+
const machineId = `${os14.hostname()}:${username}`;
|
|
6177
|
+
try {
|
|
6178
|
+
const res = await fetch(`${creds.apiUrl}/cost-sync`, {
|
|
6179
|
+
method: "POST",
|
|
6180
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
|
|
6181
|
+
body: JSON.stringify({ machineId, entries }),
|
|
6182
|
+
signal: AbortSignal.timeout(15e3)
|
|
6183
|
+
});
|
|
6184
|
+
if (!res.ok) {
|
|
6185
|
+
fs16.appendFileSync(HOOK_DEBUG_LOG, `[cost-sync] HTTP ${res.status}
|
|
6186
|
+
`);
|
|
6187
|
+
}
|
|
6188
|
+
} catch (err2) {
|
|
6189
|
+
fs16.appendFileSync(HOOK_DEBUG_LOG, `[cost-sync] ${err2.message}
|
|
6190
|
+
`);
|
|
6191
|
+
}
|
|
6192
|
+
}
|
|
6193
|
+
function startCostSync() {
|
|
6194
|
+
syncCost().catch(() => {
|
|
6195
|
+
});
|
|
6196
|
+
const timer = setInterval(() => {
|
|
6197
|
+
syncCost().catch(() => {
|
|
6198
|
+
});
|
|
6199
|
+
}, SYNC_INTERVAL_MS);
|
|
6200
|
+
timer.unref();
|
|
6201
|
+
}
|
|
6202
|
+
var SYNC_INTERVAL_MS, PRICING;
|
|
6203
|
+
var init_costSync = __esm({
|
|
6204
|
+
"src/costSync.ts"() {
|
|
6205
|
+
"use strict";
|
|
6206
|
+
init_config();
|
|
6207
|
+
init_audit();
|
|
6208
|
+
SYNC_INTERVAL_MS = 10 * 60 * 1e3;
|
|
6209
|
+
PRICING = {
|
|
6210
|
+
"claude-opus-4": [5e-6, 25e-6, 625e-8, 5e-7],
|
|
6211
|
+
"claude-sonnet-4": [3e-6, 15e-6, 375e-8, 3e-7],
|
|
6212
|
+
"claude-haiku-4": [8e-7, 4e-6, 1e-6, 8e-8],
|
|
6213
|
+
"claude-3-7-sonnet": [3e-6, 15e-6, 375e-8, 3e-7],
|
|
6214
|
+
"claude-3-5-sonnet": [3e-6, 15e-6, 375e-8, 3e-7],
|
|
6215
|
+
"claude-3-5-haiku": [8e-7, 4e-6, 1e-6, 8e-8],
|
|
6216
|
+
"claude-3-haiku": [25e-8, 125e-8, 3e-7, 3e-8]
|
|
6217
|
+
};
|
|
5942
6218
|
}
|
|
5943
6219
|
});
|
|
5944
6220
|
|
|
5945
6221
|
// src/daemon/server.ts
|
|
5946
6222
|
import http from "http";
|
|
5947
|
-
import
|
|
5948
|
-
import
|
|
6223
|
+
import fs17 from "fs";
|
|
6224
|
+
import path20 from "path";
|
|
5949
6225
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
5950
6226
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
5951
6227
|
import chalk2 from "chalk";
|
|
5952
6228
|
function startDaemon() {
|
|
6229
|
+
startCostSync();
|
|
5953
6230
|
loadInsightCounts();
|
|
5954
6231
|
const csrfToken = randomUUID4();
|
|
5955
6232
|
const internalToken = randomUUID4();
|
|
@@ -5965,7 +6242,7 @@ function startDaemon() {
|
|
|
5965
6242
|
idleTimer = setTimeout(() => {
|
|
5966
6243
|
if (autoStarted) {
|
|
5967
6244
|
try {
|
|
5968
|
-
|
|
6245
|
+
fs17.unlinkSync(DAEMON_PID_FILE);
|
|
5969
6246
|
} catch {
|
|
5970
6247
|
}
|
|
5971
6248
|
}
|
|
@@ -6128,7 +6405,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
6128
6405
|
status: "pending"
|
|
6129
6406
|
});
|
|
6130
6407
|
}
|
|
6131
|
-
const projectCwd = typeof cwd === "string" &&
|
|
6408
|
+
const projectCwd = typeof cwd === "string" && path20.isAbsolute(cwd) ? cwd : void 0;
|
|
6132
6409
|
const projectConfig = getConfig(projectCwd);
|
|
6133
6410
|
const browserEnabled = projectConfig.settings.approvers?.browser !== false;
|
|
6134
6411
|
const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
|
|
@@ -6518,8 +6795,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
6518
6795
|
const body = await readBody(req);
|
|
6519
6796
|
const data = body ? JSON.parse(body) : {};
|
|
6520
6797
|
const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
|
|
6521
|
-
const node9Dir =
|
|
6522
|
-
if (!
|
|
6798
|
+
const node9Dir = path20.dirname(GLOBAL_CONFIG_PATH);
|
|
6799
|
+
if (!path20.resolve(configPath).startsWith(node9Dir + path20.sep)) {
|
|
6523
6800
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6524
6801
|
return res.end(
|
|
6525
6802
|
JSON.stringify({ error: "configPath must be within the node9 config directory" })
|
|
@@ -6630,14 +6907,14 @@ data: ${JSON.stringify(item.data)}
|
|
|
6630
6907
|
server.on("error", (e) => {
|
|
6631
6908
|
if (e.code === "EADDRINUSE") {
|
|
6632
6909
|
try {
|
|
6633
|
-
if (
|
|
6634
|
-
const { pid } = JSON.parse(
|
|
6910
|
+
if (fs17.existsSync(DAEMON_PID_FILE)) {
|
|
6911
|
+
const { pid } = JSON.parse(fs17.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
6635
6912
|
process.kill(pid, 0);
|
|
6636
6913
|
return process.exit(0);
|
|
6637
6914
|
}
|
|
6638
6915
|
} catch {
|
|
6639
6916
|
try {
|
|
6640
|
-
|
|
6917
|
+
fs17.unlinkSync(DAEMON_PID_FILE);
|
|
6641
6918
|
} catch {
|
|
6642
6919
|
}
|
|
6643
6920
|
server.listen(DAEMON_PORT, DAEMON_HOST);
|
|
@@ -6705,32 +6982,33 @@ var init_server = __esm({
|
|
|
6705
6982
|
init_state2();
|
|
6706
6983
|
init_patch();
|
|
6707
6984
|
init_config_schema();
|
|
6985
|
+
init_costSync();
|
|
6708
6986
|
}
|
|
6709
6987
|
});
|
|
6710
6988
|
|
|
6711
6989
|
// src/daemon/index.ts
|
|
6712
|
-
import
|
|
6990
|
+
import fs18 from "fs";
|
|
6713
6991
|
import chalk3 from "chalk";
|
|
6714
6992
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
6715
6993
|
function stopDaemon() {
|
|
6716
|
-
if (!
|
|
6994
|
+
if (!fs18.existsSync(DAEMON_PID_FILE)) return console.log(chalk3.yellow("Not running."));
|
|
6717
6995
|
try {
|
|
6718
|
-
const { pid } = JSON.parse(
|
|
6996
|
+
const { pid } = JSON.parse(fs18.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
6719
6997
|
process.kill(pid, "SIGTERM");
|
|
6720
6998
|
console.log(chalk3.green("\u2705 Stopped."));
|
|
6721
6999
|
} catch {
|
|
6722
7000
|
console.log(chalk3.gray("Cleaned up stale PID file."));
|
|
6723
7001
|
} finally {
|
|
6724
7002
|
try {
|
|
6725
|
-
|
|
7003
|
+
fs18.unlinkSync(DAEMON_PID_FILE);
|
|
6726
7004
|
} catch {
|
|
6727
7005
|
}
|
|
6728
7006
|
}
|
|
6729
7007
|
}
|
|
6730
7008
|
function daemonStatus() {
|
|
6731
|
-
if (
|
|
7009
|
+
if (fs18.existsSync(DAEMON_PID_FILE)) {
|
|
6732
7010
|
try {
|
|
6733
|
-
const { pid } = JSON.parse(
|
|
7011
|
+
const { pid } = JSON.parse(fs18.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
6734
7012
|
process.kill(pid, 0);
|
|
6735
7013
|
console.log(chalk3.green("Node9 daemon: running"));
|
|
6736
7014
|
return;
|
|
@@ -6764,10 +7042,10 @@ __export(tail_exports, {
|
|
|
6764
7042
|
startTail: () => startTail
|
|
6765
7043
|
});
|
|
6766
7044
|
import http2 from "http";
|
|
6767
|
-
import
|
|
6768
|
-
import
|
|
6769
|
-
import
|
|
6770
|
-
import
|
|
7045
|
+
import chalk19 from "chalk";
|
|
7046
|
+
import fs29 from "fs";
|
|
7047
|
+
import os25 from "os";
|
|
7048
|
+
import path32 from "path";
|
|
6771
7049
|
import readline5 from "readline";
|
|
6772
7050
|
import { spawn as spawn10, execSync as execSync3 } from "child_process";
|
|
6773
7051
|
function getIcon(tool) {
|
|
@@ -6777,44 +7055,64 @@ function getIcon(tool) {
|
|
|
6777
7055
|
}
|
|
6778
7056
|
return "\u{1F6E0}\uFE0F";
|
|
6779
7057
|
}
|
|
7058
|
+
function visibleLength(s) {
|
|
7059
|
+
return s.replace(/\x1B\[[0-9;]*m/g, "").length;
|
|
7060
|
+
}
|
|
7061
|
+
function wrappedLineCount(text) {
|
|
7062
|
+
const cols = process.stdout.columns;
|
|
7063
|
+
if (!cols) return 1;
|
|
7064
|
+
const len = visibleLength(text);
|
|
7065
|
+
return Math.max(1, Math.ceil(len / cols));
|
|
7066
|
+
}
|
|
6780
7067
|
function formatBase(activity) {
|
|
6781
7068
|
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
6782
7069
|
const icon = getIcon(activity.tool);
|
|
6783
7070
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
6784
|
-
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(
|
|
7071
|
+
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os25.homedir(), "~");
|
|
6785
7072
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
6786
|
-
return `${
|
|
7073
|
+
return `${chalk19.gray(time)} ${icon} ${chalk19.white.bold(toolName)} ${chalk19.dim(argsPreview)}`;
|
|
6787
7074
|
}
|
|
6788
7075
|
function renderResult(activity, result) {
|
|
6789
7076
|
const base = formatBase(activity);
|
|
6790
7077
|
let status;
|
|
6791
7078
|
if (result.status === "allow") {
|
|
6792
|
-
status =
|
|
7079
|
+
status = chalk19.green("\u2713 ALLOW");
|
|
6793
7080
|
} else if (result.status === "dlp") {
|
|
6794
|
-
status =
|
|
7081
|
+
status = chalk19.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
6795
7082
|
} else {
|
|
6796
|
-
status =
|
|
7083
|
+
status = chalk19.red("\u2717 BLOCK");
|
|
6797
7084
|
}
|
|
6798
7085
|
const cost = result.costEstimate ?? activity.costEstimate;
|
|
6799
|
-
const costSuffix = cost == null ? "" :
|
|
7086
|
+
const costSuffix = cost == null ? "" : chalk19.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
|
|
6800
7087
|
if (process.stdout.isTTY) {
|
|
6801
|
-
|
|
6802
|
-
|
|
7088
|
+
if (pendingShownForId === activity.id && pendingWrappedLines > 1) {
|
|
7089
|
+
readline5.moveCursor(process.stdout, 0, -(pendingWrappedLines - 1));
|
|
7090
|
+
readline5.cursorTo(process.stdout, 0);
|
|
7091
|
+
process.stdout.write(ERASE_DOWN);
|
|
7092
|
+
} else {
|
|
7093
|
+
readline5.clearLine(process.stdout, 0);
|
|
7094
|
+
readline5.cursorTo(process.stdout, 0);
|
|
7095
|
+
}
|
|
7096
|
+
pendingShownForId = null;
|
|
7097
|
+
pendingWrappedLines = 0;
|
|
6803
7098
|
}
|
|
6804
7099
|
console.log(`${base} ${status}${costSuffix}`);
|
|
6805
7100
|
}
|
|
6806
7101
|
function renderPending(activity) {
|
|
6807
7102
|
if (!process.stdout.isTTY) return;
|
|
6808
|
-
|
|
7103
|
+
const line = `${formatBase(activity)} ${chalk19.yellow("\u25CF \u2026")}`;
|
|
7104
|
+
pendingShownForId = activity.id;
|
|
7105
|
+
pendingWrappedLines = wrappedLineCount(line);
|
|
7106
|
+
process.stdout.write(`${line}\r`);
|
|
6809
7107
|
}
|
|
6810
7108
|
async function ensureDaemon() {
|
|
6811
7109
|
let pidPort = null;
|
|
6812
|
-
if (
|
|
7110
|
+
if (fs29.existsSync(PID_FILE)) {
|
|
6813
7111
|
try {
|
|
6814
|
-
const { port } = JSON.parse(
|
|
7112
|
+
const { port } = JSON.parse(fs29.readFileSync(PID_FILE, "utf-8"));
|
|
6815
7113
|
pidPort = port;
|
|
6816
7114
|
} catch {
|
|
6817
|
-
console.error(
|
|
7115
|
+
console.error(chalk19.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
6818
7116
|
}
|
|
6819
7117
|
}
|
|
6820
7118
|
const checkPort = pidPort ?? DAEMON_PORT;
|
|
@@ -6825,7 +7123,7 @@ async function ensureDaemon() {
|
|
|
6825
7123
|
if (res.ok) return checkPort;
|
|
6826
7124
|
} catch {
|
|
6827
7125
|
}
|
|
6828
|
-
console.log(
|
|
7126
|
+
console.log(chalk19.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
6829
7127
|
const child = spawn10(process.execPath, [process.argv[1], "daemon"], {
|
|
6830
7128
|
detached: true,
|
|
6831
7129
|
stdio: "ignore",
|
|
@@ -6842,7 +7140,7 @@ async function ensureDaemon() {
|
|
|
6842
7140
|
} catch {
|
|
6843
7141
|
}
|
|
6844
7142
|
}
|
|
6845
|
-
console.error(
|
|
7143
|
+
console.error(chalk19.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
6846
7144
|
process.exit(1);
|
|
6847
7145
|
}
|
|
6848
7146
|
function postDecisionHttp(id, decision, csrfToken, port, opts) {
|
|
@@ -6888,6 +7186,9 @@ function buildCardLines(req, localCount = 0) {
|
|
|
6888
7186
|
`${CYAN}\u2551${RESET2} Tool: ${BOLD2}${req.toolName}${RESET2}`,
|
|
6889
7187
|
`${CYAN}\u2551${RESET2} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET2}`
|
|
6890
7188
|
];
|
|
7189
|
+
if (req.riskMetadata?.ruleDescription) {
|
|
7190
|
+
lines.push(`${CYAN}\u2551${RESET2} ${YELLOW}\u2139 ${req.riskMetadata.ruleDescription}${RESET2}`);
|
|
7191
|
+
}
|
|
6891
7192
|
if (req.riskMetadata?.ruleName && blockedBy.includes("Taint")) {
|
|
6892
7193
|
lines.push(`${CYAN}\u2551${RESET2} ${YELLOW}\u26A0 ${req.riskMetadata.ruleName}${RESET2}`);
|
|
6893
7194
|
}
|
|
@@ -6931,9 +7232,9 @@ function buildRecoveryCardLines(req) {
|
|
|
6931
7232
|
];
|
|
6932
7233
|
}
|
|
6933
7234
|
function readApproversFromDisk() {
|
|
6934
|
-
const configPath =
|
|
7235
|
+
const configPath = path32.join(os25.homedir(), ".node9", "config.json");
|
|
6935
7236
|
try {
|
|
6936
|
-
const raw = JSON.parse(
|
|
7237
|
+
const raw = JSON.parse(fs29.readFileSync(configPath, "utf-8"));
|
|
6937
7238
|
const settings = raw.settings ?? {};
|
|
6938
7239
|
return settings.approvers ?? {};
|
|
6939
7240
|
} catch {
|
|
@@ -6944,20 +7245,20 @@ function approverStatusLine() {
|
|
|
6944
7245
|
const a = readApproversFromDisk();
|
|
6945
7246
|
const fmt = (label, key) => {
|
|
6946
7247
|
const on = a[key] !== false;
|
|
6947
|
-
return `[${key[0]}]${label.slice(1)} ${on ?
|
|
7248
|
+
return `[${key[0]}]${label.slice(1)} ${on ? chalk19.green("\u2713") : chalk19.dim("\u2717")}`;
|
|
6948
7249
|
};
|
|
6949
7250
|
return `${fmt("native", "native")} ${fmt("browser", "browser")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
|
|
6950
7251
|
}
|
|
6951
7252
|
function toggleApprover(channel) {
|
|
6952
|
-
const configPath =
|
|
7253
|
+
const configPath = path32.join(os25.homedir(), ".node9", "config.json");
|
|
6953
7254
|
try {
|
|
6954
|
-
const raw = JSON.parse(
|
|
7255
|
+
const raw = JSON.parse(fs29.readFileSync(configPath, "utf-8"));
|
|
6955
7256
|
const settings = raw.settings ?? {};
|
|
6956
7257
|
const approvers = settings.approvers ?? {};
|
|
6957
7258
|
approvers[channel] = approvers[channel] === false;
|
|
6958
7259
|
settings.approvers = approvers;
|
|
6959
7260
|
raw.settings = settings;
|
|
6960
|
-
|
|
7261
|
+
fs29.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
|
|
6961
7262
|
} catch (err2) {
|
|
6962
7263
|
process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
|
|
6963
7264
|
`);
|
|
@@ -6989,7 +7290,7 @@ async function startTail(options = {}) {
|
|
|
6989
7290
|
req2.end();
|
|
6990
7291
|
});
|
|
6991
7292
|
if (result.ok) {
|
|
6992
|
-
console.log(
|
|
7293
|
+
console.log(chalk19.green("\u2713 Flight Recorder buffer cleared."));
|
|
6993
7294
|
} else if (result.code === "ECONNREFUSED") {
|
|
6994
7295
|
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
6995
7296
|
} else if (result.code === "ETIMEDOUT") {
|
|
@@ -7033,7 +7334,7 @@ async function startTail(options = {}) {
|
|
|
7033
7334
|
const channel = name === "n" ? "native" : name === "b" ? "browser" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
|
|
7034
7335
|
if (channel) {
|
|
7035
7336
|
toggleApprover(channel);
|
|
7036
|
-
console.log(
|
|
7337
|
+
console.log(chalk19.dim(` Approvers: ${approverStatusLine()}`));
|
|
7037
7338
|
}
|
|
7038
7339
|
};
|
|
7039
7340
|
process.stdin.on("keypress", idleKeypressHandler);
|
|
@@ -7099,7 +7400,7 @@ async function startTail(options = {}) {
|
|
|
7099
7400
|
localAllowCounts.get(req2.toolName) ?? 0
|
|
7100
7401
|
)
|
|
7101
7402
|
);
|
|
7102
|
-
const decisionStamp = action === "always-allow" ?
|
|
7403
|
+
const decisionStamp = action === "always-allow" ? chalk19.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk19.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk19.green("\u2713 ALLOWED") : action === "redirect" ? chalk19.yellow("\u21A9 REDIRECT AI") : chalk19.red("\u2717 DENIED");
|
|
7103
7404
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
|
|
7104
7405
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
7105
7406
|
process.stdout.write(SHOW_CURSOR);
|
|
@@ -7127,8 +7428,8 @@ async function startTail(options = {}) {
|
|
|
7127
7428
|
}
|
|
7128
7429
|
postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err2) => {
|
|
7129
7430
|
try {
|
|
7130
|
-
|
|
7131
|
-
|
|
7431
|
+
fs29.appendFileSync(
|
|
7432
|
+
path32.join(os25.homedir(), ".node9", "hook-debug.log"),
|
|
7132
7433
|
`[tail] POST /decision failed: ${String(err2)}
|
|
7133
7434
|
`
|
|
7134
7435
|
);
|
|
@@ -7150,7 +7451,7 @@ async function startTail(options = {}) {
|
|
|
7150
7451
|
);
|
|
7151
7452
|
const stampedLines = buildCardLines(req2, priorCount);
|
|
7152
7453
|
if (externalDecision) {
|
|
7153
|
-
const source = externalDecision === "allow" ?
|
|
7454
|
+
const source = externalDecision === "allow" ? chalk19.green("\u2713 ALLOWED") : chalk19.red("\u2717 DENIED");
|
|
7154
7455
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
|
|
7155
7456
|
}
|
|
7156
7457
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
@@ -7209,16 +7510,16 @@ async function startTail(options = {}) {
|
|
|
7209
7510
|
}
|
|
7210
7511
|
} catch {
|
|
7211
7512
|
}
|
|
7212
|
-
console.log(
|
|
7213
|
-
\u{1F6F0}\uFE0F Node9 tail `) +
|
|
7513
|
+
console.log(chalk19.cyan.bold(`
|
|
7514
|
+
\u{1F6F0}\uFE0F Node9 tail `) + chalk19.dim(`\u2192 ${dashboardUrl}`));
|
|
7214
7515
|
if (canApprove) {
|
|
7215
|
-
console.log(
|
|
7216
|
-
console.log(
|
|
7516
|
+
console.log(chalk19.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
|
|
7517
|
+
console.log(chalk19.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
|
|
7217
7518
|
}
|
|
7218
7519
|
if (options.history) {
|
|
7219
|
-
console.log(
|
|
7520
|
+
console.log(chalk19.dim("Showing history + live events.\n"));
|
|
7220
7521
|
} else {
|
|
7221
|
-
console.log(
|
|
7522
|
+
console.log(chalk19.dim("Showing live events only. Use --history to include past.\n"));
|
|
7222
7523
|
}
|
|
7223
7524
|
process.on("SIGINT", () => {
|
|
7224
7525
|
exitIdleMode();
|
|
@@ -7228,13 +7529,13 @@ async function startTail(options = {}) {
|
|
|
7228
7529
|
readline5.clearLine(process.stdout, 0);
|
|
7229
7530
|
readline5.cursorTo(process.stdout, 0);
|
|
7230
7531
|
}
|
|
7231
|
-
console.log(
|
|
7532
|
+
console.log(chalk19.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
7232
7533
|
process.exit(0);
|
|
7233
7534
|
});
|
|
7234
7535
|
const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
|
|
7235
7536
|
const req = http2.get(sseUrl, (res) => {
|
|
7236
7537
|
if (res.statusCode !== 200) {
|
|
7237
|
-
console.error(
|
|
7538
|
+
console.error(chalk19.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
7238
7539
|
process.exit(1);
|
|
7239
7540
|
}
|
|
7240
7541
|
if (canApprove) enterIdleMode();
|
|
@@ -7265,7 +7566,7 @@ async function startTail(options = {}) {
|
|
|
7265
7566
|
readline5.clearLine(process.stdout, 0);
|
|
7266
7567
|
readline5.cursorTo(process.stdout, 0);
|
|
7267
7568
|
}
|
|
7268
|
-
console.log(
|
|
7569
|
+
console.log(chalk19.red("\n\u274C Daemon disconnected."));
|
|
7269
7570
|
process.exit(1);
|
|
7270
7571
|
});
|
|
7271
7572
|
});
|
|
@@ -7357,9 +7658,9 @@ async function startTail(options = {}) {
|
|
|
7357
7658
|
const hash = data.hash ?? "";
|
|
7358
7659
|
const summary = data.argsSummary ?? data.tool;
|
|
7359
7660
|
const fileCount = data.fileCount ?? 0;
|
|
7360
|
-
const files = fileCount > 0 ?
|
|
7661
|
+
const files = fileCount > 0 ? chalk19.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
|
|
7361
7662
|
process.stdout.write(
|
|
7362
|
-
`${
|
|
7663
|
+
`${chalk19.dim(time)} ${chalk19.cyan("\u{1F4F8} snapshot")} ${chalk19.dim(hash)} ${summary}${files}
|
|
7363
7664
|
`
|
|
7364
7665
|
);
|
|
7365
7666
|
return;
|
|
@@ -7376,19 +7677,19 @@ async function startTail(options = {}) {
|
|
|
7376
7677
|
}
|
|
7377
7678
|
req.on("error", (err2) => {
|
|
7378
7679
|
const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
|
|
7379
|
-
console.error(
|
|
7680
|
+
console.error(chalk19.red(`
|
|
7380
7681
|
\u274C ${msg}`));
|
|
7381
7682
|
process.exit(1);
|
|
7382
7683
|
});
|
|
7383
7684
|
}
|
|
7384
|
-
var PID_FILE, ICONS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, DIVIDER;
|
|
7685
|
+
var PID_FILE, ICONS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, pendingShownForId, pendingWrappedLines, DIVIDER;
|
|
7385
7686
|
var init_tail = __esm({
|
|
7386
7687
|
"src/tui/tail.ts"() {
|
|
7387
7688
|
"use strict";
|
|
7388
7689
|
init_daemon2();
|
|
7389
7690
|
init_daemon();
|
|
7390
7691
|
init_core();
|
|
7391
|
-
PID_FILE =
|
|
7692
|
+
PID_FILE = path32.join(os25.homedir(), ".node9", "daemon.pid");
|
|
7392
7693
|
ICONS = {
|
|
7393
7694
|
bash: "\u{1F4BB}",
|
|
7394
7695
|
shell: "\u{1F4BB}",
|
|
@@ -7416,6 +7717,8 @@ var init_tail = __esm({
|
|
|
7416
7717
|
HIDE_CURSOR = "\x1B[?25l";
|
|
7417
7718
|
SHOW_CURSOR = "\x1B[?25h";
|
|
7418
7719
|
ERASE_DOWN = "\x1B[J";
|
|
7720
|
+
pendingShownForId = null;
|
|
7721
|
+
pendingWrappedLines = 0;
|
|
7419
7722
|
DIVIDER = "\u2500".repeat(60);
|
|
7420
7723
|
}
|
|
7421
7724
|
});
|
|
@@ -7427,9 +7730,9 @@ __export(hud_exports, {
|
|
|
7427
7730
|
main: () => main,
|
|
7428
7731
|
renderEnvironmentLine: () => renderEnvironmentLine
|
|
7429
7732
|
});
|
|
7430
|
-
import
|
|
7431
|
-
import
|
|
7432
|
-
import
|
|
7733
|
+
import fs30 from "fs";
|
|
7734
|
+
import path33 from "path";
|
|
7735
|
+
import os26 from "os";
|
|
7433
7736
|
import http3 from "http";
|
|
7434
7737
|
async function readStdin() {
|
|
7435
7738
|
const chunks = [];
|
|
@@ -7488,10 +7791,10 @@ function bold(s) {
|
|
|
7488
7791
|
function color(c, s) {
|
|
7489
7792
|
return `${c}${s}${RESET3}`;
|
|
7490
7793
|
}
|
|
7491
|
-
function progressBar(
|
|
7492
|
-
const filled = Math.round(Math.min(
|
|
7794
|
+
function progressBar(pct2, warnAt = 70, critAt = 85) {
|
|
7795
|
+
const filled = Math.round(Math.min(pct2, 100) / 100 * BAR_WIDTH);
|
|
7493
7796
|
const bar = BAR_FILLED.repeat(filled) + BAR_EMPTY.repeat(BAR_WIDTH - filled);
|
|
7494
|
-
const c =
|
|
7797
|
+
const c = pct2 >= critAt ? RED2 : pct2 >= warnAt ? YELLOW2 : GREEN2;
|
|
7495
7798
|
return `${c}${bar}${RESET3}`;
|
|
7496
7799
|
}
|
|
7497
7800
|
function formatTimeLeft(resetsAt) {
|
|
@@ -7505,9 +7808,9 @@ function formatTimeLeft(resetsAt) {
|
|
|
7505
7808
|
return ` (${m}m left)`;
|
|
7506
7809
|
}
|
|
7507
7810
|
function safeReadJson(filePath) {
|
|
7508
|
-
if (!
|
|
7811
|
+
if (!fs30.existsSync(filePath)) return null;
|
|
7509
7812
|
try {
|
|
7510
|
-
return JSON.parse(
|
|
7813
|
+
return JSON.parse(fs30.readFileSync(filePath, "utf-8"));
|
|
7511
7814
|
} catch {
|
|
7512
7815
|
return null;
|
|
7513
7816
|
}
|
|
@@ -7528,12 +7831,12 @@ function countHooksInFile(filePath) {
|
|
|
7528
7831
|
return Object.keys(cfg.hooks).length;
|
|
7529
7832
|
}
|
|
7530
7833
|
function countRulesInDir(rulesDir) {
|
|
7531
|
-
if (!
|
|
7834
|
+
if (!fs30.existsSync(rulesDir)) return 0;
|
|
7532
7835
|
let count = 0;
|
|
7533
7836
|
try {
|
|
7534
|
-
for (const entry of
|
|
7837
|
+
for (const entry of fs30.readdirSync(rulesDir, { withFileTypes: true })) {
|
|
7535
7838
|
if (entry.isDirectory()) {
|
|
7536
|
-
count += countRulesInDir(
|
|
7839
|
+
count += countRulesInDir(path33.join(rulesDir, entry.name));
|
|
7537
7840
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
7538
7841
|
count++;
|
|
7539
7842
|
}
|
|
@@ -7544,46 +7847,46 @@ function countRulesInDir(rulesDir) {
|
|
|
7544
7847
|
}
|
|
7545
7848
|
function isSamePath(a, b) {
|
|
7546
7849
|
try {
|
|
7547
|
-
return
|
|
7850
|
+
return path33.resolve(a) === path33.resolve(b);
|
|
7548
7851
|
} catch {
|
|
7549
7852
|
return false;
|
|
7550
7853
|
}
|
|
7551
7854
|
}
|
|
7552
7855
|
function countConfigs(cwd) {
|
|
7553
|
-
const homeDir2 =
|
|
7554
|
-
const claudeDir =
|
|
7856
|
+
const homeDir2 = os26.homedir();
|
|
7857
|
+
const claudeDir = path33.join(homeDir2, ".claude");
|
|
7555
7858
|
let claudeMdCount = 0;
|
|
7556
7859
|
let rulesCount = 0;
|
|
7557
7860
|
let hooksCount = 0;
|
|
7558
7861
|
const userMcpServers = /* @__PURE__ */ new Set();
|
|
7559
7862
|
const projectMcpServers = /* @__PURE__ */ new Set();
|
|
7560
|
-
if (
|
|
7561
|
-
rulesCount += countRulesInDir(
|
|
7562
|
-
const userSettings =
|
|
7863
|
+
if (fs30.existsSync(path33.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
7864
|
+
rulesCount += countRulesInDir(path33.join(claudeDir, "rules"));
|
|
7865
|
+
const userSettings = path33.join(claudeDir, "settings.json");
|
|
7563
7866
|
for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
|
|
7564
7867
|
hooksCount += countHooksInFile(userSettings);
|
|
7565
|
-
const userClaudeJson =
|
|
7868
|
+
const userClaudeJson = path33.join(homeDir2, ".claude.json");
|
|
7566
7869
|
for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
|
|
7567
7870
|
for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
|
|
7568
7871
|
userMcpServers.delete(name);
|
|
7569
7872
|
}
|
|
7570
7873
|
if (cwd) {
|
|
7571
|
-
if (
|
|
7572
|
-
if (
|
|
7573
|
-
const projectClaudeDir =
|
|
7874
|
+
if (fs30.existsSync(path33.join(cwd, "CLAUDE.md"))) claudeMdCount++;
|
|
7875
|
+
if (fs30.existsSync(path33.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
|
|
7876
|
+
const projectClaudeDir = path33.join(cwd, ".claude");
|
|
7574
7877
|
const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
|
|
7575
7878
|
if (!overlapsUserScope) {
|
|
7576
|
-
if (
|
|
7577
|
-
rulesCount += countRulesInDir(
|
|
7578
|
-
const projSettings =
|
|
7879
|
+
if (fs30.existsSync(path33.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
7880
|
+
rulesCount += countRulesInDir(path33.join(projectClaudeDir, "rules"));
|
|
7881
|
+
const projSettings = path33.join(projectClaudeDir, "settings.json");
|
|
7579
7882
|
for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
|
|
7580
7883
|
hooksCount += countHooksInFile(projSettings);
|
|
7581
7884
|
}
|
|
7582
|
-
if (
|
|
7583
|
-
const localSettings =
|
|
7885
|
+
if (fs30.existsSync(path33.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
|
|
7886
|
+
const localSettings = path33.join(projectClaudeDir, "settings.local.json");
|
|
7584
7887
|
for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
|
|
7585
7888
|
hooksCount += countHooksInFile(localSettings);
|
|
7586
|
-
const mcpJsonServers = getMcpServerNames(
|
|
7889
|
+
const mcpJsonServers = getMcpServerNames(path33.join(cwd, ".mcp.json"));
|
|
7587
7890
|
const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
|
|
7588
7891
|
for (const name of disabledMcpJson) mcpJsonServers.delete(name);
|
|
7589
7892
|
for (const name of mcpJsonServers) projectMcpServers.add(name);
|
|
@@ -7616,12 +7919,12 @@ function readActiveShieldsHud() {
|
|
|
7616
7919
|
return shieldsCache.value;
|
|
7617
7920
|
}
|
|
7618
7921
|
try {
|
|
7619
|
-
const shieldsPath =
|
|
7620
|
-
if (!
|
|
7922
|
+
const shieldsPath = path33.join(os26.homedir(), ".node9", "shields.json");
|
|
7923
|
+
if (!fs30.existsSync(shieldsPath)) {
|
|
7621
7924
|
shieldsCache = { value: [], ts: now };
|
|
7622
7925
|
return [];
|
|
7623
7926
|
}
|
|
7624
|
-
const parsed = JSON.parse(
|
|
7927
|
+
const parsed = JSON.parse(fs30.readFileSync(shieldsPath, "utf-8"));
|
|
7625
7928
|
if (!Array.isArray(parsed.active)) {
|
|
7626
7929
|
shieldsCache = { value: [], ts: now };
|
|
7627
7930
|
return [];
|
|
@@ -7707,15 +8010,15 @@ function renderContextLine(stdin) {
|
|
|
7707
8010
|
}
|
|
7708
8011
|
const rl = stdin.rate_limits;
|
|
7709
8012
|
if (rl?.five_hour?.used_percentage !== void 0) {
|
|
7710
|
-
const
|
|
7711
|
-
const bar = progressBar(
|
|
8013
|
+
const pct2 = Math.round(rl.five_hour.used_percentage);
|
|
8014
|
+
const bar = progressBar(pct2, 60, 80);
|
|
7712
8015
|
const left = formatTimeLeft(rl.five_hour.resets_at);
|
|
7713
|
-
parts.push(`${dim("\u2502")} 5h ${bar} ${
|
|
8016
|
+
parts.push(`${dim("\u2502")} 5h ${bar} ${pct2}%${left}`);
|
|
7714
8017
|
}
|
|
7715
8018
|
if (rl?.seven_day?.used_percentage !== void 0) {
|
|
7716
|
-
const
|
|
7717
|
-
const bar = progressBar(
|
|
7718
|
-
parts.push(`${dim("\u2502")} 7d ${bar} ${
|
|
8019
|
+
const pct2 = Math.round(rl.seven_day.used_percentage);
|
|
8020
|
+
const bar = progressBar(pct2, 60, 80);
|
|
8021
|
+
parts.push(`${dim("\u2502")} 7d ${bar} ${pct2}%`);
|
|
7719
8022
|
}
|
|
7720
8023
|
if (parts.length === 0) return null;
|
|
7721
8024
|
return parts.join(" ");
|
|
@@ -7723,17 +8026,17 @@ function renderContextLine(stdin) {
|
|
|
7723
8026
|
async function main() {
|
|
7724
8027
|
try {
|
|
7725
8028
|
const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
|
|
7726
|
-
if (
|
|
8029
|
+
if (fs30.existsSync(path33.join(os26.homedir(), ".node9", "hud-debug"))) {
|
|
7727
8030
|
try {
|
|
7728
|
-
const logPath =
|
|
8031
|
+
const logPath = path33.join(os26.homedir(), ".node9", "hud-debug.log");
|
|
7729
8032
|
const MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
7730
8033
|
let size = 0;
|
|
7731
8034
|
try {
|
|
7732
|
-
size =
|
|
8035
|
+
size = fs30.statSync(logPath).size;
|
|
7733
8036
|
} catch {
|
|
7734
8037
|
}
|
|
7735
8038
|
if (size < MAX_LOG_SIZE) {
|
|
7736
|
-
|
|
8039
|
+
fs30.appendFileSync(
|
|
7737
8040
|
logPath,
|
|
7738
8041
|
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
|
|
7739
8042
|
);
|
|
@@ -7754,11 +8057,11 @@ async function main() {
|
|
|
7754
8057
|
try {
|
|
7755
8058
|
const cwd = stdin.cwd ?? process.cwd();
|
|
7756
8059
|
for (const configPath of [
|
|
7757
|
-
|
|
7758
|
-
|
|
8060
|
+
path33.join(cwd, "node9.config.json"),
|
|
8061
|
+
path33.join(os26.homedir(), ".node9", "config.json")
|
|
7759
8062
|
]) {
|
|
7760
|
-
if (!
|
|
7761
|
-
const cfg = JSON.parse(
|
|
8063
|
+
if (!fs30.existsSync(configPath)) continue;
|
|
8064
|
+
const cfg = JSON.parse(fs30.readFileSync(configPath, "utf-8"));
|
|
7762
8065
|
const hud = cfg.settings?.hud;
|
|
7763
8066
|
if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
|
|
7764
8067
|
}
|
|
@@ -7804,9 +8107,9 @@ init_core();
|
|
|
7804
8107
|
import { Command } from "commander";
|
|
7805
8108
|
|
|
7806
8109
|
// src/setup.ts
|
|
7807
|
-
import
|
|
7808
|
-
import
|
|
7809
|
-
import
|
|
8110
|
+
import fs12 from "fs";
|
|
8111
|
+
import path15 from "path";
|
|
8112
|
+
import os11 from "os";
|
|
7810
8113
|
import chalk from "chalk";
|
|
7811
8114
|
import { confirm } from "@inquirer/prompts";
|
|
7812
8115
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
@@ -7834,26 +8137,26 @@ function fullPathCommand(subcommand) {
|
|
|
7834
8137
|
}
|
|
7835
8138
|
function readJson(filePath) {
|
|
7836
8139
|
try {
|
|
7837
|
-
if (
|
|
7838
|
-
return JSON.parse(
|
|
8140
|
+
if (fs12.existsSync(filePath)) {
|
|
8141
|
+
return JSON.parse(fs12.readFileSync(filePath, "utf-8"));
|
|
7839
8142
|
}
|
|
7840
8143
|
} catch {
|
|
7841
8144
|
}
|
|
7842
8145
|
return null;
|
|
7843
8146
|
}
|
|
7844
8147
|
function writeJson(filePath, data) {
|
|
7845
|
-
const dir =
|
|
7846
|
-
if (!
|
|
7847
|
-
|
|
8148
|
+
const dir = path15.dirname(filePath);
|
|
8149
|
+
if (!fs12.existsSync(dir)) fs12.mkdirSync(dir, { recursive: true });
|
|
8150
|
+
fs12.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
7848
8151
|
}
|
|
7849
8152
|
function isNode9Hook(cmd) {
|
|
7850
8153
|
if (!cmd) return false;
|
|
7851
8154
|
return /(?:^|[\s/\\])node9 (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js (?:check|log)/.test(cmd);
|
|
7852
8155
|
}
|
|
7853
8156
|
function teardownClaude() {
|
|
7854
|
-
const homeDir2 =
|
|
7855
|
-
const hooksPath =
|
|
7856
|
-
const mcpPath =
|
|
8157
|
+
const homeDir2 = os11.homedir();
|
|
8158
|
+
const hooksPath = path15.join(homeDir2, ".claude", "settings.json");
|
|
8159
|
+
const mcpPath = path15.join(homeDir2, ".claude", ".mcp.json");
|
|
7857
8160
|
let changed = false;
|
|
7858
8161
|
const settings = readJson(hooksPath);
|
|
7859
8162
|
if (settings?.hooks) {
|
|
@@ -7901,8 +8204,8 @@ function teardownClaude() {
|
|
|
7901
8204
|
}
|
|
7902
8205
|
}
|
|
7903
8206
|
function teardownGemini() {
|
|
7904
|
-
const homeDir2 =
|
|
7905
|
-
const settingsPath =
|
|
8207
|
+
const homeDir2 = os11.homedir();
|
|
8208
|
+
const settingsPath = path15.join(homeDir2, ".gemini", "settings.json");
|
|
7906
8209
|
const settings = readJson(settingsPath);
|
|
7907
8210
|
if (!settings) {
|
|
7908
8211
|
console.log(chalk.blue(" \u2139\uFE0F ~/.gemini/settings.json not found \u2014 nothing to remove"));
|
|
@@ -7945,8 +8248,8 @@ function teardownGemini() {
|
|
|
7945
8248
|
}
|
|
7946
8249
|
}
|
|
7947
8250
|
function teardownCursor() {
|
|
7948
|
-
const homeDir2 =
|
|
7949
|
-
const mcpPath =
|
|
8251
|
+
const homeDir2 = os11.homedir();
|
|
8252
|
+
const mcpPath = path15.join(homeDir2, ".cursor", "mcp.json");
|
|
7950
8253
|
const mcpConfig = readJson(mcpPath);
|
|
7951
8254
|
if (!mcpConfig?.mcpServers) {
|
|
7952
8255
|
console.log(chalk.blue(" \u2139\uFE0F ~/.cursor/mcp.json not found \u2014 nothing to remove"));
|
|
@@ -7977,9 +8280,9 @@ function teardownCursor() {
|
|
|
7977
8280
|
}
|
|
7978
8281
|
}
|
|
7979
8282
|
async function setupClaude() {
|
|
7980
|
-
const homeDir2 =
|
|
7981
|
-
const mcpPath =
|
|
7982
|
-
const hooksPath =
|
|
8283
|
+
const homeDir2 = os11.homedir();
|
|
8284
|
+
const mcpPath = path15.join(homeDir2, ".claude", ".mcp.json");
|
|
8285
|
+
const hooksPath = path15.join(homeDir2, ".claude", "settings.json");
|
|
7983
8286
|
const claudeConfig = readJson(mcpPath) ?? {};
|
|
7984
8287
|
const settings = readJson(hooksPath) ?? {};
|
|
7985
8288
|
const servers = claudeConfig.mcpServers ?? {};
|
|
@@ -8076,8 +8379,8 @@ async function setupClaude() {
|
|
|
8076
8379
|
}
|
|
8077
8380
|
}
|
|
8078
8381
|
async function setupGemini() {
|
|
8079
|
-
const homeDir2 =
|
|
8080
|
-
const settingsPath =
|
|
8382
|
+
const homeDir2 = os11.homedir();
|
|
8383
|
+
const settingsPath = path15.join(homeDir2, ".gemini", "settings.json");
|
|
8081
8384
|
const settings = readJson(settingsPath) ?? {};
|
|
8082
8385
|
const servers = settings.mcpServers ?? {};
|
|
8083
8386
|
let hooksChanged = false;
|
|
@@ -8172,10 +8475,10 @@ async function setupGemini() {
|
|
|
8172
8475
|
printDaemonTip();
|
|
8173
8476
|
}
|
|
8174
8477
|
}
|
|
8175
|
-
function detectAgents(homeDir2 =
|
|
8478
|
+
function detectAgents(homeDir2 = os11.homedir()) {
|
|
8176
8479
|
const exists = (p) => {
|
|
8177
8480
|
try {
|
|
8178
|
-
return
|
|
8481
|
+
return fs12.existsSync(p);
|
|
8179
8482
|
} catch (err2) {
|
|
8180
8483
|
const code = err2.code;
|
|
8181
8484
|
if (code !== "ENOENT") {
|
|
@@ -8186,15 +8489,15 @@ function detectAgents(homeDir2 = os10.homedir()) {
|
|
|
8186
8489
|
}
|
|
8187
8490
|
};
|
|
8188
8491
|
return {
|
|
8189
|
-
claude: exists(
|
|
8190
|
-
gemini: exists(
|
|
8191
|
-
cursor: exists(
|
|
8192
|
-
codex: exists(
|
|
8492
|
+
claude: exists(path15.join(homeDir2, ".claude")) || exists(path15.join(homeDir2, ".claude.json")),
|
|
8493
|
+
gemini: exists(path15.join(homeDir2, ".gemini")),
|
|
8494
|
+
cursor: exists(path15.join(homeDir2, ".cursor")),
|
|
8495
|
+
codex: exists(path15.join(homeDir2, ".codex"))
|
|
8193
8496
|
};
|
|
8194
8497
|
}
|
|
8195
8498
|
async function setupCursor() {
|
|
8196
|
-
const homeDir2 =
|
|
8197
|
-
const mcpPath =
|
|
8499
|
+
const homeDir2 = os11.homedir();
|
|
8500
|
+
const mcpPath = path15.join(homeDir2, ".cursor", "mcp.json");
|
|
8198
8501
|
const mcpConfig = readJson(mcpPath) ?? {};
|
|
8199
8502
|
const servers = mcpConfig.mcpServers ?? {};
|
|
8200
8503
|
let anythingChanged = false;
|
|
@@ -8260,21 +8563,21 @@ async function setupCursor() {
|
|
|
8260
8563
|
}
|
|
8261
8564
|
function readToml(filePath) {
|
|
8262
8565
|
try {
|
|
8263
|
-
if (
|
|
8264
|
-
return parseToml(
|
|
8566
|
+
if (fs12.existsSync(filePath)) {
|
|
8567
|
+
return parseToml(fs12.readFileSync(filePath, "utf-8"));
|
|
8265
8568
|
}
|
|
8266
8569
|
} catch {
|
|
8267
8570
|
}
|
|
8268
8571
|
return null;
|
|
8269
8572
|
}
|
|
8270
8573
|
function writeToml(filePath, data) {
|
|
8271
|
-
const dir =
|
|
8272
|
-
if (!
|
|
8273
|
-
|
|
8574
|
+
const dir = path15.dirname(filePath);
|
|
8575
|
+
if (!fs12.existsSync(dir)) fs12.mkdirSync(dir, { recursive: true });
|
|
8576
|
+
fs12.writeFileSync(filePath, stringifyToml(data));
|
|
8274
8577
|
}
|
|
8275
8578
|
async function setupCodex() {
|
|
8276
|
-
const homeDir2 =
|
|
8277
|
-
const configPath =
|
|
8579
|
+
const homeDir2 = os11.homedir();
|
|
8580
|
+
const configPath = path15.join(homeDir2, ".codex", "config.toml");
|
|
8278
8581
|
const config = readToml(configPath) ?? {};
|
|
8279
8582
|
const servers = config.mcp_servers ?? {};
|
|
8280
8583
|
let anythingChanged = false;
|
|
@@ -8339,8 +8642,8 @@ async function setupCodex() {
|
|
|
8339
8642
|
}
|
|
8340
8643
|
}
|
|
8341
8644
|
function setupHud() {
|
|
8342
|
-
const homeDir2 =
|
|
8343
|
-
const hooksPath =
|
|
8645
|
+
const homeDir2 = os11.homedir();
|
|
8646
|
+
const hooksPath = path15.join(homeDir2, ".claude", "settings.json");
|
|
8344
8647
|
const settings = readJson(hooksPath) ?? {};
|
|
8345
8648
|
const hudCommand = fullPathCommand("hud");
|
|
8346
8649
|
const statusLineObj = { type: "command", command: hudCommand };
|
|
@@ -8366,8 +8669,8 @@ function setupHud() {
|
|
|
8366
8669
|
console.log(chalk.gray(" Restart Claude Code to activate."));
|
|
8367
8670
|
}
|
|
8368
8671
|
function teardownHud() {
|
|
8369
|
-
const homeDir2 =
|
|
8370
|
-
const hooksPath =
|
|
8672
|
+
const homeDir2 = os11.homedir();
|
|
8673
|
+
const hooksPath = path15.join(homeDir2, ".claude", "settings.json");
|
|
8371
8674
|
const settings = readJson(hooksPath);
|
|
8372
8675
|
if (!settings) {
|
|
8373
8676
|
console.log(chalk.blue(" \u2139\uFE0F ~/.claude/settings.json not found \u2014 nothing to remove"));
|
|
@@ -8387,10 +8690,10 @@ function teardownHud() {
|
|
|
8387
8690
|
|
|
8388
8691
|
// src/cli.ts
|
|
8389
8692
|
init_daemon2();
|
|
8390
|
-
import
|
|
8391
|
-
import
|
|
8392
|
-
import
|
|
8393
|
-
import
|
|
8693
|
+
import chalk20 from "chalk";
|
|
8694
|
+
import fs31 from "fs";
|
|
8695
|
+
import path34 from "path";
|
|
8696
|
+
import os27 from "os";
|
|
8394
8697
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
8395
8698
|
|
|
8396
8699
|
// src/utils/duration.ts
|
|
@@ -8619,19 +8922,19 @@ init_daemon();
|
|
|
8619
8922
|
init_config();
|
|
8620
8923
|
init_policy();
|
|
8621
8924
|
import chalk5 from "chalk";
|
|
8622
|
-
import
|
|
8925
|
+
import fs20 from "fs";
|
|
8623
8926
|
import { spawn as spawn6 } from "child_process";
|
|
8624
|
-
import
|
|
8625
|
-
import
|
|
8927
|
+
import path22 from "path";
|
|
8928
|
+
import os16 from "os";
|
|
8626
8929
|
|
|
8627
8930
|
// src/undo.ts
|
|
8628
8931
|
import { spawnSync as spawnSync4, spawn as spawn5 } from "child_process";
|
|
8629
|
-
import
|
|
8630
|
-
import
|
|
8932
|
+
import crypto3 from "crypto";
|
|
8933
|
+
import fs19 from "fs";
|
|
8631
8934
|
import net3 from "net";
|
|
8632
|
-
import
|
|
8633
|
-
import
|
|
8634
|
-
var ACTIVITY_SOCKET_PATH3 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
8935
|
+
import path21 from "path";
|
|
8936
|
+
import os15 from "os";
|
|
8937
|
+
var ACTIVITY_SOCKET_PATH3 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path21.join(os15.tmpdir(), "node9-activity.sock");
|
|
8635
8938
|
function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
|
|
8636
8939
|
try {
|
|
8637
8940
|
const payload = JSON.stringify({
|
|
@@ -8651,22 +8954,22 @@ function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
|
|
|
8651
8954
|
} catch {
|
|
8652
8955
|
}
|
|
8653
8956
|
}
|
|
8654
|
-
var SNAPSHOT_STACK_PATH =
|
|
8655
|
-
var UNDO_LATEST_PATH =
|
|
8957
|
+
var SNAPSHOT_STACK_PATH = path21.join(os15.homedir(), ".node9", "snapshots.json");
|
|
8958
|
+
var UNDO_LATEST_PATH = path21.join(os15.homedir(), ".node9", "undo_latest.txt");
|
|
8656
8959
|
var MAX_SNAPSHOTS = 10;
|
|
8657
8960
|
var GIT_TIMEOUT = 15e3;
|
|
8658
8961
|
function readStack() {
|
|
8659
8962
|
try {
|
|
8660
|
-
if (
|
|
8661
|
-
return JSON.parse(
|
|
8963
|
+
if (fs19.existsSync(SNAPSHOT_STACK_PATH))
|
|
8964
|
+
return JSON.parse(fs19.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
|
|
8662
8965
|
} catch {
|
|
8663
8966
|
}
|
|
8664
8967
|
return [];
|
|
8665
8968
|
}
|
|
8666
8969
|
function writeStack(stack) {
|
|
8667
|
-
const dir =
|
|
8668
|
-
if (!
|
|
8669
|
-
|
|
8970
|
+
const dir = path21.dirname(SNAPSHOT_STACK_PATH);
|
|
8971
|
+
if (!fs19.existsSync(dir)) fs19.mkdirSync(dir, { recursive: true });
|
|
8972
|
+
fs19.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
8670
8973
|
}
|
|
8671
8974
|
function extractFilePath(args) {
|
|
8672
8975
|
if (!args || typeof args !== "object") return null;
|
|
@@ -8686,12 +8989,12 @@ function buildArgsSummary(tool, args) {
|
|
|
8686
8989
|
return "";
|
|
8687
8990
|
}
|
|
8688
8991
|
function findProjectRoot(filePath) {
|
|
8689
|
-
let dir =
|
|
8992
|
+
let dir = path21.dirname(filePath);
|
|
8690
8993
|
while (true) {
|
|
8691
|
-
if (
|
|
8994
|
+
if (fs19.existsSync(path21.join(dir, ".git")) || fs19.existsSync(path21.join(dir, "package.json"))) {
|
|
8692
8995
|
return dir;
|
|
8693
8996
|
}
|
|
8694
|
-
const parent =
|
|
8997
|
+
const parent = path21.dirname(dir);
|
|
8695
8998
|
if (parent === dir) return process.cwd();
|
|
8696
8999
|
dir = parent;
|
|
8697
9000
|
}
|
|
@@ -8699,7 +9002,7 @@ function findProjectRoot(filePath) {
|
|
|
8699
9002
|
function normalizeCwdForHash(cwd) {
|
|
8700
9003
|
let normalized;
|
|
8701
9004
|
try {
|
|
8702
|
-
normalized =
|
|
9005
|
+
normalized = fs19.realpathSync(cwd);
|
|
8703
9006
|
} catch {
|
|
8704
9007
|
normalized = cwd;
|
|
8705
9008
|
}
|
|
@@ -8708,17 +9011,17 @@ function normalizeCwdForHash(cwd) {
|
|
|
8708
9011
|
return normalized;
|
|
8709
9012
|
}
|
|
8710
9013
|
function getShadowRepoDir(cwd) {
|
|
8711
|
-
const hash =
|
|
8712
|
-
return
|
|
9014
|
+
const hash = crypto3.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
|
|
9015
|
+
return path21.join(os15.homedir(), ".node9", "snapshots", hash);
|
|
8713
9016
|
}
|
|
8714
9017
|
function cleanOrphanedIndexFiles(shadowDir) {
|
|
8715
9018
|
try {
|
|
8716
9019
|
const cutoff = Date.now() - 6e4;
|
|
8717
|
-
for (const f of
|
|
9020
|
+
for (const f of fs19.readdirSync(shadowDir)) {
|
|
8718
9021
|
if (f.startsWith("index_")) {
|
|
8719
|
-
const fp =
|
|
9022
|
+
const fp = path21.join(shadowDir, f);
|
|
8720
9023
|
try {
|
|
8721
|
-
if (
|
|
9024
|
+
if (fs19.statSync(fp).mtimeMs < cutoff) fs19.unlinkSync(fp);
|
|
8722
9025
|
} catch {
|
|
8723
9026
|
}
|
|
8724
9027
|
}
|
|
@@ -8730,7 +9033,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
|
|
|
8730
9033
|
const hardcoded = [".git", ".node9"];
|
|
8731
9034
|
const lines = [...hardcoded, ...ignorePaths].join("\n");
|
|
8732
9035
|
try {
|
|
8733
|
-
|
|
9036
|
+
fs19.writeFileSync(path21.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
|
|
8734
9037
|
} catch {
|
|
8735
9038
|
}
|
|
8736
9039
|
}
|
|
@@ -8743,25 +9046,25 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
8743
9046
|
timeout: 3e3
|
|
8744
9047
|
});
|
|
8745
9048
|
if (check.status === 0) {
|
|
8746
|
-
const ptPath =
|
|
9049
|
+
const ptPath = path21.join(shadowDir, "project-path.txt");
|
|
8747
9050
|
try {
|
|
8748
|
-
const stored =
|
|
9051
|
+
const stored = fs19.readFileSync(ptPath, "utf8").trim();
|
|
8749
9052
|
if (stored === normalizedCwd) return true;
|
|
8750
9053
|
if (process.env.NODE9_DEBUG === "1")
|
|
8751
9054
|
console.error(
|
|
8752
9055
|
`[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
|
|
8753
9056
|
);
|
|
8754
|
-
|
|
9057
|
+
fs19.rmSync(shadowDir, { recursive: true, force: true });
|
|
8755
9058
|
} catch {
|
|
8756
9059
|
try {
|
|
8757
|
-
|
|
9060
|
+
fs19.writeFileSync(ptPath, normalizedCwd, "utf8");
|
|
8758
9061
|
} catch {
|
|
8759
9062
|
}
|
|
8760
9063
|
return true;
|
|
8761
9064
|
}
|
|
8762
9065
|
}
|
|
8763
9066
|
try {
|
|
8764
|
-
|
|
9067
|
+
fs19.mkdirSync(shadowDir, { recursive: true });
|
|
8765
9068
|
} catch {
|
|
8766
9069
|
}
|
|
8767
9070
|
const init = spawnSync4("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
|
|
@@ -8770,7 +9073,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
8770
9073
|
if (process.env.NODE9_DEBUG === "1") console.error("[Node9] git init --bare failed:", reason);
|
|
8771
9074
|
return false;
|
|
8772
9075
|
}
|
|
8773
|
-
const configFile =
|
|
9076
|
+
const configFile = path21.join(shadowDir, "config");
|
|
8774
9077
|
spawnSync4("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
|
|
8775
9078
|
timeout: 3e3
|
|
8776
9079
|
});
|
|
@@ -8778,7 +9081,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
8778
9081
|
timeout: 3e3
|
|
8779
9082
|
});
|
|
8780
9083
|
try {
|
|
8781
|
-
|
|
9084
|
+
fs19.writeFileSync(path21.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
|
|
8782
9085
|
} catch {
|
|
8783
9086
|
}
|
|
8784
9087
|
return true;
|
|
@@ -8798,12 +9101,12 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
8798
9101
|
let indexFile = null;
|
|
8799
9102
|
try {
|
|
8800
9103
|
const rawFilePath = extractFilePath(args);
|
|
8801
|
-
const absFilePath = rawFilePath &&
|
|
9104
|
+
const absFilePath = rawFilePath && path21.isAbsolute(rawFilePath) ? rawFilePath : null;
|
|
8802
9105
|
const cwd = absFilePath ? findProjectRoot(absFilePath) : process.cwd();
|
|
8803
9106
|
const shadowDir = getShadowRepoDir(cwd);
|
|
8804
9107
|
if (!ensureShadowRepo(shadowDir, cwd)) return null;
|
|
8805
9108
|
writeShadowExcludes(shadowDir, ignorePaths);
|
|
8806
|
-
indexFile =
|
|
9109
|
+
indexFile = path21.join(shadowDir, `index_${process.pid}_${Date.now()}`);
|
|
8807
9110
|
const shadowEnv = {
|
|
8808
9111
|
...process.env,
|
|
8809
9112
|
GIT_DIR: shadowDir,
|
|
@@ -8875,7 +9178,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
8875
9178
|
writeStack(stack);
|
|
8876
9179
|
const entry = stack[stack.length - 1];
|
|
8877
9180
|
notifySnapshotTaken(commitHash.slice(0, 7), tool, entry.argsSummary, capturedFiles.length);
|
|
8878
|
-
|
|
9181
|
+
fs19.writeFileSync(UNDO_LATEST_PATH, commitHash);
|
|
8879
9182
|
if (shouldGc) {
|
|
8880
9183
|
spawn5("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
|
|
8881
9184
|
}
|
|
@@ -8886,7 +9189,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
8886
9189
|
} finally {
|
|
8887
9190
|
if (indexFile) {
|
|
8888
9191
|
try {
|
|
8889
|
-
|
|
9192
|
+
fs19.unlinkSync(indexFile);
|
|
8890
9193
|
} catch {
|
|
8891
9194
|
}
|
|
8892
9195
|
}
|
|
@@ -8962,9 +9265,9 @@ function applyUndo(hash, cwd) {
|
|
|
8962
9265
|
timeout: GIT_TIMEOUT
|
|
8963
9266
|
}).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
8964
9267
|
for (const file of [...tracked, ...untracked]) {
|
|
8965
|
-
const fullPath =
|
|
8966
|
-
if (!snapshotFiles.has(file) &&
|
|
8967
|
-
|
|
9268
|
+
const fullPath = path21.join(dir, file);
|
|
9269
|
+
if (!snapshotFiles.has(file) && fs19.existsSync(fullPath)) {
|
|
9270
|
+
fs19.unlinkSync(fullPath);
|
|
8968
9271
|
}
|
|
8969
9272
|
}
|
|
8970
9273
|
return true;
|
|
@@ -8988,9 +9291,9 @@ function registerCheckCommand(program2) {
|
|
|
8988
9291
|
} catch (err2) {
|
|
8989
9292
|
const tempConfig = getConfig();
|
|
8990
9293
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
8991
|
-
const logPath =
|
|
9294
|
+
const logPath = path22.join(os16.homedir(), ".node9", "hook-debug.log");
|
|
8992
9295
|
const errMsg = err2 instanceof Error ? err2.message : String(err2);
|
|
8993
|
-
|
|
9296
|
+
fs20.appendFileSync(
|
|
8994
9297
|
logPath,
|
|
8995
9298
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
8996
9299
|
RAW: ${raw}
|
|
@@ -9003,10 +9306,10 @@ RAW: ${raw}
|
|
|
9003
9306
|
if (config.settings.autoStartDaemon && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON) {
|
|
9004
9307
|
try {
|
|
9005
9308
|
const scriptPath = process.argv[1];
|
|
9006
|
-
if (typeof scriptPath !== "string" || !
|
|
9309
|
+
if (typeof scriptPath !== "string" || !path22.isAbsolute(scriptPath))
|
|
9007
9310
|
throw new Error("node9: argv[1] is not an absolute path");
|
|
9008
|
-
const resolvedScript =
|
|
9009
|
-
const expectedCli =
|
|
9311
|
+
const resolvedScript = fs20.realpathSync(scriptPath);
|
|
9312
|
+
const expectedCli = fs20.realpathSync(path22.resolve(__dirname, "../../cli.js"));
|
|
9010
9313
|
if (resolvedScript !== expectedCli)
|
|
9011
9314
|
throw new Error(
|
|
9012
9315
|
"node9: daemon spawn aborted \u2014 argv[1] does not resolve to the node9 CLI"
|
|
@@ -9032,10 +9335,10 @@ RAW: ${raw}
|
|
|
9032
9335
|
}
|
|
9033
9336
|
}
|
|
9034
9337
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
9035
|
-
const logPath =
|
|
9036
|
-
if (!
|
|
9037
|
-
|
|
9038
|
-
|
|
9338
|
+
const logPath = path22.join(os16.homedir(), ".node9", "hook-debug.log");
|
|
9339
|
+
if (!fs20.existsSync(path22.dirname(logPath)))
|
|
9340
|
+
fs20.mkdirSync(path22.dirname(logPath), { recursive: true });
|
|
9341
|
+
fs20.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
9039
9342
|
`);
|
|
9040
9343
|
}
|
|
9041
9344
|
const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
|
|
@@ -9048,8 +9351,8 @@ RAW: ${raw}
|
|
|
9048
9351
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
9049
9352
|
let ttyFd = null;
|
|
9050
9353
|
try {
|
|
9051
|
-
ttyFd =
|
|
9052
|
-
const writeTty = (line) =>
|
|
9354
|
+
ttyFd = fs20.openSync("/dev/tty", "w");
|
|
9355
|
+
const writeTty = (line) => fs20.writeSync(ttyFd, line + "\n");
|
|
9053
9356
|
if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
|
|
9054
9357
|
writeTty(chalk5.bgRed.white.bold(`
|
|
9055
9358
|
\u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
@@ -9068,7 +9371,7 @@ RAW: ${raw}
|
|
|
9068
9371
|
} finally {
|
|
9069
9372
|
if (ttyFd !== null)
|
|
9070
9373
|
try {
|
|
9071
|
-
|
|
9374
|
+
fs20.closeSync(ttyFd);
|
|
9072
9375
|
} catch {
|
|
9073
9376
|
}
|
|
9074
9377
|
}
|
|
@@ -9100,7 +9403,7 @@ RAW: ${raw}
|
|
|
9100
9403
|
if (shouldSnapshot(toolName, toolInput, config)) {
|
|
9101
9404
|
await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
|
|
9102
9405
|
}
|
|
9103
|
-
const safeCwdForAuth = typeof payload.cwd === "string" &&
|
|
9406
|
+
const safeCwdForAuth = typeof payload.cwd === "string" && path22.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
9104
9407
|
const result = await authorizeHeadless(toolName, toolInput, meta, {
|
|
9105
9408
|
cwd: safeCwdForAuth
|
|
9106
9409
|
});
|
|
@@ -9112,12 +9415,12 @@ RAW: ${raw}
|
|
|
9112
9415
|
}
|
|
9113
9416
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
9114
9417
|
try {
|
|
9115
|
-
const tty =
|
|
9116
|
-
|
|
9418
|
+
const tty = fs20.openSync("/dev/tty", "w");
|
|
9419
|
+
fs20.writeSync(
|
|
9117
9420
|
tty,
|
|
9118
9421
|
chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
|
|
9119
9422
|
);
|
|
9120
|
-
|
|
9423
|
+
fs20.closeSync(tty);
|
|
9121
9424
|
} catch {
|
|
9122
9425
|
}
|
|
9123
9426
|
const daemonReady = await autoStartDaemonAndWait();
|
|
@@ -9144,9 +9447,9 @@ RAW: ${raw}
|
|
|
9144
9447
|
});
|
|
9145
9448
|
} catch (err2) {
|
|
9146
9449
|
if (process.env.NODE9_DEBUG === "1") {
|
|
9147
|
-
const logPath =
|
|
9450
|
+
const logPath = path22.join(os16.homedir(), ".node9", "hook-debug.log");
|
|
9148
9451
|
const errMsg = err2 instanceof Error ? err2.message : String(err2);
|
|
9149
|
-
|
|
9452
|
+
fs20.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
9150
9453
|
`);
|
|
9151
9454
|
}
|
|
9152
9455
|
process.exit(0);
|
|
@@ -9183,9 +9486,9 @@ RAW: ${raw}
|
|
|
9183
9486
|
init_audit();
|
|
9184
9487
|
init_config();
|
|
9185
9488
|
init_policy();
|
|
9186
|
-
import
|
|
9187
|
-
import
|
|
9188
|
-
import
|
|
9489
|
+
import fs21 from "fs";
|
|
9490
|
+
import path23 from "path";
|
|
9491
|
+
import os17 from "os";
|
|
9189
9492
|
init_daemon();
|
|
9190
9493
|
|
|
9191
9494
|
// src/utils/cp-mv-parser.ts
|
|
@@ -9226,9 +9529,9 @@ function containsShellMetachar(token) {
|
|
|
9226
9529
|
}
|
|
9227
9530
|
|
|
9228
9531
|
// src/cli/commands/log.ts
|
|
9229
|
-
var
|
|
9532
|
+
var TEST_COMMAND_RE2 = /(?:^|\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;
|
|
9230
9533
|
function detectTestResult(command, output) {
|
|
9231
|
-
if (!
|
|
9534
|
+
if (!TEST_COMMAND_RE2.test(command)) return null;
|
|
9232
9535
|
const out = output.toLowerCase();
|
|
9233
9536
|
if (/\b(tests?\s+passed|all\s+tests?\s+passed|passing|test\s+suites?.*passed|ok\b|\d+\s+passed)/i.test(
|
|
9234
9537
|
out
|
|
@@ -9258,10 +9561,10 @@ function registerLogCommand(program2) {
|
|
|
9258
9561
|
decision: "allowed",
|
|
9259
9562
|
source: "post-hook"
|
|
9260
9563
|
};
|
|
9261
|
-
const logPath =
|
|
9262
|
-
if (!
|
|
9263
|
-
|
|
9264
|
-
|
|
9564
|
+
const logPath = path23.join(os17.homedir(), ".node9", "audit.log");
|
|
9565
|
+
if (!fs21.existsSync(path23.dirname(logPath)))
|
|
9566
|
+
fs21.mkdirSync(path23.dirname(logPath), { recursive: true });
|
|
9567
|
+
fs21.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
9265
9568
|
if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
|
|
9266
9569
|
const command = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
|
|
9267
9570
|
if (command) {
|
|
@@ -9277,16 +9580,24 @@ function registerLogCommand(program2) {
|
|
|
9277
9580
|
if (bashCommand && output) {
|
|
9278
9581
|
const testResult = detectTestResult(bashCommand, output);
|
|
9279
9582
|
if (testResult) {
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
ts: Date.now(),
|
|
9583
|
+
appendToLog(LOCAL_AUDIT_LOG, {
|
|
9584
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9283
9585
|
tool,
|
|
9284
|
-
|
|
9586
|
+
testResult,
|
|
9587
|
+
source: "test-result"
|
|
9285
9588
|
});
|
|
9589
|
+
if (isDaemonRunning()) {
|
|
9590
|
+
await notifyActivitySocket({
|
|
9591
|
+
id: "test-result",
|
|
9592
|
+
ts: Date.now(),
|
|
9593
|
+
tool,
|
|
9594
|
+
status: testResult === "pass" ? "test_pass" : "test_fail"
|
|
9595
|
+
});
|
|
9596
|
+
}
|
|
9286
9597
|
}
|
|
9287
9598
|
}
|
|
9288
9599
|
}
|
|
9289
|
-
const safeCwd = typeof payload.cwd === "string" &&
|
|
9600
|
+
const safeCwd = typeof payload.cwd === "string" && path23.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
9290
9601
|
const config = getConfig(safeCwd);
|
|
9291
9602
|
if (shouldSnapshot(tool, {}, config)) {
|
|
9292
9603
|
await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
|
|
@@ -9295,9 +9606,9 @@ function registerLogCommand(program2) {
|
|
|
9295
9606
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
9296
9607
|
process.stderr.write(`[Node9] audit log error: ${msg}
|
|
9297
9608
|
`);
|
|
9298
|
-
const debugPath =
|
|
9609
|
+
const debugPath = path23.join(os17.homedir(), ".node9", "hook-debug.log");
|
|
9299
9610
|
try {
|
|
9300
|
-
|
|
9611
|
+
fs21.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
|
|
9301
9612
|
`);
|
|
9302
9613
|
} catch {
|
|
9303
9614
|
}
|
|
@@ -9698,13 +10009,13 @@ function registerConfigShowCommand(program2) {
|
|
|
9698
10009
|
// src/cli/commands/doctor.ts
|
|
9699
10010
|
init_daemon();
|
|
9700
10011
|
import chalk7 from "chalk";
|
|
9701
|
-
import
|
|
9702
|
-
import
|
|
9703
|
-
import
|
|
10012
|
+
import fs22 from "fs";
|
|
10013
|
+
import path24 from "path";
|
|
10014
|
+
import os18 from "os";
|
|
9704
10015
|
import { execSync as execSync2 } from "child_process";
|
|
9705
10016
|
function registerDoctorCommand(program2, version2) {
|
|
9706
10017
|
program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
9707
|
-
const homeDir2 =
|
|
10018
|
+
const homeDir2 = os18.homedir();
|
|
9708
10019
|
let failures = 0;
|
|
9709
10020
|
function pass(msg) {
|
|
9710
10021
|
console.log(chalk7.green(" \u2705 ") + msg);
|
|
@@ -9753,10 +10064,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9753
10064
|
);
|
|
9754
10065
|
}
|
|
9755
10066
|
section("Configuration");
|
|
9756
|
-
const globalConfigPath =
|
|
9757
|
-
if (
|
|
10067
|
+
const globalConfigPath = path24.join(homeDir2, ".node9", "config.json");
|
|
10068
|
+
if (fs22.existsSync(globalConfigPath)) {
|
|
9758
10069
|
try {
|
|
9759
|
-
JSON.parse(
|
|
10070
|
+
JSON.parse(fs22.readFileSync(globalConfigPath, "utf-8"));
|
|
9760
10071
|
pass("~/.node9/config.json found and valid");
|
|
9761
10072
|
} catch {
|
|
9762
10073
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -9764,10 +10075,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9764
10075
|
} else {
|
|
9765
10076
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
9766
10077
|
}
|
|
9767
|
-
const projectConfigPath =
|
|
9768
|
-
if (
|
|
10078
|
+
const projectConfigPath = path24.join(process.cwd(), "node9.config.json");
|
|
10079
|
+
if (fs22.existsSync(projectConfigPath)) {
|
|
9769
10080
|
try {
|
|
9770
|
-
JSON.parse(
|
|
10081
|
+
JSON.parse(fs22.readFileSync(projectConfigPath, "utf-8"));
|
|
9771
10082
|
pass("node9.config.json found and valid (project)");
|
|
9772
10083
|
} catch {
|
|
9773
10084
|
fail(
|
|
@@ -9776,8 +10087,8 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9776
10087
|
);
|
|
9777
10088
|
}
|
|
9778
10089
|
}
|
|
9779
|
-
const credsPath =
|
|
9780
|
-
if (
|
|
10090
|
+
const credsPath = path24.join(homeDir2, ".node9", "credentials.json");
|
|
10091
|
+
if (fs22.existsSync(credsPath)) {
|
|
9781
10092
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
9782
10093
|
} else {
|
|
9783
10094
|
warn(
|
|
@@ -9786,10 +10097,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9786
10097
|
);
|
|
9787
10098
|
}
|
|
9788
10099
|
section("Agent Hooks");
|
|
9789
|
-
const claudeSettingsPath =
|
|
9790
|
-
if (
|
|
10100
|
+
const claudeSettingsPath = path24.join(homeDir2, ".claude", "settings.json");
|
|
10101
|
+
if (fs22.existsSync(claudeSettingsPath)) {
|
|
9791
10102
|
try {
|
|
9792
|
-
const cs = JSON.parse(
|
|
10103
|
+
const cs = JSON.parse(fs22.readFileSync(claudeSettingsPath, "utf-8"));
|
|
9793
10104
|
const hasHook = cs.hooks?.PreToolUse?.some(
|
|
9794
10105
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
9795
10106
|
);
|
|
@@ -9805,10 +10116,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9805
10116
|
} else {
|
|
9806
10117
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
9807
10118
|
}
|
|
9808
|
-
const geminiSettingsPath =
|
|
9809
|
-
if (
|
|
10119
|
+
const geminiSettingsPath = path24.join(homeDir2, ".gemini", "settings.json");
|
|
10120
|
+
if (fs22.existsSync(geminiSettingsPath)) {
|
|
9810
10121
|
try {
|
|
9811
|
-
const gs = JSON.parse(
|
|
10122
|
+
const gs = JSON.parse(fs22.readFileSync(geminiSettingsPath, "utf-8"));
|
|
9812
10123
|
const hasHook = gs.hooks?.BeforeTool?.some(
|
|
9813
10124
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
9814
10125
|
);
|
|
@@ -9824,10 +10135,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9824
10135
|
} else {
|
|
9825
10136
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
9826
10137
|
}
|
|
9827
|
-
const cursorHooksPath =
|
|
9828
|
-
if (
|
|
10138
|
+
const cursorHooksPath = path24.join(homeDir2, ".cursor", "hooks.json");
|
|
10139
|
+
if (fs22.existsSync(cursorHooksPath)) {
|
|
9829
10140
|
try {
|
|
9830
|
-
const cur = JSON.parse(
|
|
10141
|
+
const cur = JSON.parse(fs22.readFileSync(cursorHooksPath, "utf-8"));
|
|
9831
10142
|
const hasHook = cur.hooks?.preToolUse?.some(
|
|
9832
10143
|
(h) => h.command?.includes("node9") || h.command?.includes("cli.js")
|
|
9833
10144
|
);
|
|
@@ -9865,9 +10176,9 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9865
10176
|
|
|
9866
10177
|
// src/cli/commands/audit.ts
|
|
9867
10178
|
import chalk8 from "chalk";
|
|
9868
|
-
import
|
|
9869
|
-
import
|
|
9870
|
-
import
|
|
10179
|
+
import fs23 from "fs";
|
|
10180
|
+
import path25 from "path";
|
|
10181
|
+
import os19 from "os";
|
|
9871
10182
|
function formatRelativeTime(timestamp) {
|
|
9872
10183
|
const diff = Date.now() - new Date(timestamp).getTime();
|
|
9873
10184
|
const sec = Math.floor(diff / 1e3);
|
|
@@ -9880,14 +10191,14 @@ function formatRelativeTime(timestamp) {
|
|
|
9880
10191
|
}
|
|
9881
10192
|
function registerAuditCommand(program2) {
|
|
9882
10193
|
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) => {
|
|
9883
|
-
const logPath =
|
|
9884
|
-
if (!
|
|
10194
|
+
const logPath = path25.join(os19.homedir(), ".node9", "audit.log");
|
|
10195
|
+
if (!fs23.existsSync(logPath)) {
|
|
9885
10196
|
console.log(
|
|
9886
10197
|
chalk8.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
9887
10198
|
);
|
|
9888
10199
|
return;
|
|
9889
10200
|
}
|
|
9890
|
-
const raw =
|
|
10201
|
+
const raw = fs23.readFileSync(logPath, "utf-8");
|
|
9891
10202
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
9892
10203
|
let entries = lines.flatMap((line) => {
|
|
9893
10204
|
try {
|
|
@@ -9939,10 +10250,445 @@ function registerAuditCommand(program2) {
|
|
|
9939
10250
|
});
|
|
9940
10251
|
}
|
|
9941
10252
|
|
|
10253
|
+
// src/cli/commands/report.ts
|
|
10254
|
+
import chalk9 from "chalk";
|
|
10255
|
+
import fs24 from "fs";
|
|
10256
|
+
import path26 from "path";
|
|
10257
|
+
import os20 from "os";
|
|
10258
|
+
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;
|
|
10259
|
+
function buildTestTimestamps(allEntries) {
|
|
10260
|
+
const testTs = /* @__PURE__ */ new Set();
|
|
10261
|
+
for (const e of allEntries) {
|
|
10262
|
+
if (e.source !== "post-hook") continue;
|
|
10263
|
+
if (e.tool !== "Bash" && e.tool !== "bash") continue;
|
|
10264
|
+
const cmd = e.args?.command;
|
|
10265
|
+
if (typeof cmd === "string" && TEST_COMMAND_RE3.test(cmd)) {
|
|
10266
|
+
testTs.add(new Date(e.ts).getTime());
|
|
10267
|
+
}
|
|
10268
|
+
}
|
|
10269
|
+
return testTs;
|
|
10270
|
+
}
|
|
10271
|
+
function isTestEntry(entry, testTs) {
|
|
10272
|
+
if (entry.tool !== "Bash" && entry.tool !== "bash") return false;
|
|
10273
|
+
if (entry.testRun === true) return true;
|
|
10274
|
+
const cmd = entry.args?.command;
|
|
10275
|
+
if (typeof cmd === "string") return TEST_COMMAND_RE3.test(cmd);
|
|
10276
|
+
const t = new Date(entry.ts).getTime();
|
|
10277
|
+
for (const ts of testTs) {
|
|
10278
|
+
if (Math.abs(ts - t) <= 3e3) return true;
|
|
10279
|
+
}
|
|
10280
|
+
return false;
|
|
10281
|
+
}
|
|
10282
|
+
function getDateRange(period) {
|
|
10283
|
+
const now = /* @__PURE__ */ new Date();
|
|
10284
|
+
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
10285
|
+
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
|
|
10286
|
+
switch (period) {
|
|
10287
|
+
case "today":
|
|
10288
|
+
return { start: todayStart, end };
|
|
10289
|
+
case "7d": {
|
|
10290
|
+
const s = new Date(todayStart);
|
|
10291
|
+
s.setDate(s.getDate() - 6);
|
|
10292
|
+
return { start: s, end };
|
|
10293
|
+
}
|
|
10294
|
+
case "30d": {
|
|
10295
|
+
const s = new Date(todayStart);
|
|
10296
|
+
s.setDate(s.getDate() - 29);
|
|
10297
|
+
return { start: s, end };
|
|
10298
|
+
}
|
|
10299
|
+
case "month":
|
|
10300
|
+
return { start: new Date(now.getFullYear(), now.getMonth(), 1), end };
|
|
10301
|
+
}
|
|
10302
|
+
}
|
|
10303
|
+
function parseAuditLog(logPath) {
|
|
10304
|
+
if (!fs24.existsSync(logPath)) return [];
|
|
10305
|
+
const raw = fs24.readFileSync(logPath, "utf-8");
|
|
10306
|
+
return raw.split("\n").flatMap((line) => {
|
|
10307
|
+
if (!line.trim()) return [];
|
|
10308
|
+
try {
|
|
10309
|
+
return [JSON.parse(line)];
|
|
10310
|
+
} catch {
|
|
10311
|
+
return [];
|
|
10312
|
+
}
|
|
10313
|
+
});
|
|
10314
|
+
}
|
|
10315
|
+
function isAllow(decision) {
|
|
10316
|
+
return decision.startsWith("allow");
|
|
10317
|
+
}
|
|
10318
|
+
function isDlp(checkedBy) {
|
|
10319
|
+
return !!checkedBy?.includes("dlp");
|
|
10320
|
+
}
|
|
10321
|
+
function barStr(value, max, width) {
|
|
10322
|
+
if (max === 0 || width <= 0) return "\u2591".repeat(width);
|
|
10323
|
+
const filled = Math.max(1, Math.round(value / max * width));
|
|
10324
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
|
|
10325
|
+
}
|
|
10326
|
+
function colorBar(value, max, width) {
|
|
10327
|
+
const s = barStr(value, max, width);
|
|
10328
|
+
const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
|
|
10329
|
+
return chalk9.cyan(s.slice(0, filled)) + chalk9.dim(s.slice(filled));
|
|
10330
|
+
}
|
|
10331
|
+
function pct(num2, total) {
|
|
10332
|
+
if (total === 0) return "\u2013";
|
|
10333
|
+
return Math.round(num2 / total * 100) + "%";
|
|
10334
|
+
}
|
|
10335
|
+
function fmtDate(d) {
|
|
10336
|
+
const date = typeof d === "string" ? /* @__PURE__ */ new Date(d + "T12:00:00") : d;
|
|
10337
|
+
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
10338
|
+
}
|
|
10339
|
+
function num(n) {
|
|
10340
|
+
return n.toLocaleString();
|
|
10341
|
+
}
|
|
10342
|
+
function fmtCost(usd) {
|
|
10343
|
+
if (usd < 1e-3) return "< $0.001";
|
|
10344
|
+
if (usd < 1) return "$" + usd.toFixed(4);
|
|
10345
|
+
return "$" + usd.toFixed(2);
|
|
10346
|
+
}
|
|
10347
|
+
var CLAUDE_PRICING = {
|
|
10348
|
+
"claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
|
|
10349
|
+
"claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
|
|
10350
|
+
"claude-opus-4": { i: 15e-6, o: 75e-6, cw: 1875e-8, cr: 15e-7 },
|
|
10351
|
+
"claude-sonnet-4-6": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
10352
|
+
"claude-sonnet-4-5": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
10353
|
+
"claude-sonnet-4": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
10354
|
+
"claude-3-7-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
10355
|
+
"claude-3-5-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
10356
|
+
"claude-haiku-4-5": { i: 1e-6, o: 5e-6, cw: 125e-8, cr: 1e-7 },
|
|
10357
|
+
"claude-3-5-haiku": { i: 8e-7, o: 4e-6, cw: 1e-6, cr: 8e-8 }
|
|
10358
|
+
};
|
|
10359
|
+
function claudeModelPrice(model) {
|
|
10360
|
+
const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
|
|
10361
|
+
for (const [key, p] of Object.entries(CLAUDE_PRICING)) {
|
|
10362
|
+
if (base === key || base.startsWith(key + "-") || base.startsWith(key)) return p;
|
|
10363
|
+
}
|
|
10364
|
+
return null;
|
|
10365
|
+
}
|
|
10366
|
+
function loadClaudeCost(start, end) {
|
|
10367
|
+
const empty = {
|
|
10368
|
+
total: 0,
|
|
10369
|
+
byDay: /* @__PURE__ */ new Map(),
|
|
10370
|
+
byModel: /* @__PURE__ */ new Map(),
|
|
10371
|
+
inputTokens: 0,
|
|
10372
|
+
cacheReadTokens: 0
|
|
10373
|
+
};
|
|
10374
|
+
const projectsDir = path26.join(os20.homedir(), ".claude", "projects");
|
|
10375
|
+
if (!fs24.existsSync(projectsDir)) return empty;
|
|
10376
|
+
let dirs;
|
|
10377
|
+
try {
|
|
10378
|
+
dirs = fs24.readdirSync(projectsDir);
|
|
10379
|
+
} catch {
|
|
10380
|
+
return empty;
|
|
10381
|
+
}
|
|
10382
|
+
let total = 0;
|
|
10383
|
+
let inputTokens = 0;
|
|
10384
|
+
let cacheReadTokens = 0;
|
|
10385
|
+
const byDay = /* @__PURE__ */ new Map();
|
|
10386
|
+
const byModel = /* @__PURE__ */ new Map();
|
|
10387
|
+
for (const proj of dirs) {
|
|
10388
|
+
const projPath = path26.join(projectsDir, proj);
|
|
10389
|
+
let files;
|
|
10390
|
+
try {
|
|
10391
|
+
const stat = fs24.statSync(projPath);
|
|
10392
|
+
if (!stat.isDirectory()) continue;
|
|
10393
|
+
files = fs24.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
|
|
10394
|
+
} catch {
|
|
10395
|
+
continue;
|
|
10396
|
+
}
|
|
10397
|
+
for (const file of files) {
|
|
10398
|
+
try {
|
|
10399
|
+
const raw = fs24.readFileSync(path26.join(projPath, file), "utf-8");
|
|
10400
|
+
for (const line of raw.split("\n")) {
|
|
10401
|
+
if (!line.trim()) continue;
|
|
10402
|
+
let entry;
|
|
10403
|
+
try {
|
|
10404
|
+
entry = JSON.parse(line);
|
|
10405
|
+
} catch {
|
|
10406
|
+
continue;
|
|
10407
|
+
}
|
|
10408
|
+
if (entry.type !== "assistant") continue;
|
|
10409
|
+
if (!entry.timestamp) continue;
|
|
10410
|
+
const ts = new Date(entry.timestamp);
|
|
10411
|
+
if (ts < start || ts > end) continue;
|
|
10412
|
+
const usage = entry.message?.usage;
|
|
10413
|
+
const model = entry.message?.model;
|
|
10414
|
+
if (!usage || !model) continue;
|
|
10415
|
+
const p = claudeModelPrice(model);
|
|
10416
|
+
if (!p) continue;
|
|
10417
|
+
const inp = usage.input_tokens ?? 0;
|
|
10418
|
+
const out = usage.output_tokens ?? 0;
|
|
10419
|
+
const cw = usage.cache_creation_input_tokens ?? 0;
|
|
10420
|
+
const cr = usage.cache_read_input_tokens ?? 0;
|
|
10421
|
+
const cost = inp * p.i + out * p.o + cw * p.cw + cr * p.cr;
|
|
10422
|
+
total += cost;
|
|
10423
|
+
inputTokens += inp;
|
|
10424
|
+
cacheReadTokens += cr;
|
|
10425
|
+
const dateKey = entry.timestamp.slice(0, 10);
|
|
10426
|
+
byDay.set(dateKey, (byDay.get(dateKey) ?? 0) + cost);
|
|
10427
|
+
const normModel = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
|
|
10428
|
+
byModel.set(normModel, (byModel.get(normModel) ?? 0) + cost);
|
|
10429
|
+
}
|
|
10430
|
+
} catch {
|
|
10431
|
+
continue;
|
|
10432
|
+
}
|
|
10433
|
+
}
|
|
10434
|
+
}
|
|
10435
|
+
return { total, byDay, byModel, inputTokens, cacheReadTokens };
|
|
10436
|
+
}
|
|
10437
|
+
function registerReportCommand(program2) {
|
|
10438
|
+
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) => {
|
|
10439
|
+
const period = ["today", "7d", "30d", "month"].includes(
|
|
10440
|
+
options.period
|
|
10441
|
+
) ? options.period : "7d";
|
|
10442
|
+
const logPath = path26.join(os20.homedir(), ".node9", "audit.log");
|
|
10443
|
+
const allEntries = parseAuditLog(logPath);
|
|
10444
|
+
if (allEntries.length === 0) {
|
|
10445
|
+
console.log(
|
|
10446
|
+
chalk9.yellow("\n No audit data found. Run node9 with Claude Code to generate entries.\n")
|
|
10447
|
+
);
|
|
10448
|
+
return;
|
|
10449
|
+
}
|
|
10450
|
+
const { start, end } = getDateRange(period);
|
|
10451
|
+
const {
|
|
10452
|
+
total: costUSD,
|
|
10453
|
+
byDay: costByDay,
|
|
10454
|
+
byModel: costByModel,
|
|
10455
|
+
inputTokens: costInputTokens,
|
|
10456
|
+
cacheReadTokens: costCacheRead
|
|
10457
|
+
} = loadClaudeCost(start, end);
|
|
10458
|
+
const periodMs = end.getTime() - start.getTime();
|
|
10459
|
+
const priorEnd = new Date(start.getTime() - 1);
|
|
10460
|
+
const priorStart = new Date(start.getTime() - periodMs);
|
|
10461
|
+
const priorEntries = allEntries.filter((e) => {
|
|
10462
|
+
if (e.source === "post-hook") return false;
|
|
10463
|
+
const ts = new Date(e.ts);
|
|
10464
|
+
return ts >= priorStart && ts <= priorEnd;
|
|
10465
|
+
});
|
|
10466
|
+
const priorBlocked = priorEntries.filter((e) => !isAllow(e.decision)).length;
|
|
10467
|
+
const priorBlockRate = priorEntries.length > 0 ? priorBlocked / priorEntries.length : null;
|
|
10468
|
+
const excludeTests = options.tests === false;
|
|
10469
|
+
const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
|
|
10470
|
+
let filteredTestCount = 0;
|
|
10471
|
+
const entries = allEntries.filter((e) => {
|
|
10472
|
+
if (e.source === "post-hook") return false;
|
|
10473
|
+
const ts = new Date(e.ts);
|
|
10474
|
+
if (ts < start || ts > end) return false;
|
|
10475
|
+
if (excludeTests && isTestEntry(e, testTs)) {
|
|
10476
|
+
filteredTestCount++;
|
|
10477
|
+
return false;
|
|
10478
|
+
}
|
|
10479
|
+
return true;
|
|
10480
|
+
});
|
|
10481
|
+
if (entries.length === 0) {
|
|
10482
|
+
console.log(chalk9.yellow(`
|
|
10483
|
+
No activity for period "${period}".
|
|
10484
|
+
`));
|
|
10485
|
+
return;
|
|
10486
|
+
}
|
|
10487
|
+
let allowed = 0;
|
|
10488
|
+
let blocked = 0;
|
|
10489
|
+
let dlpHits = 0;
|
|
10490
|
+
let loopHits = 0;
|
|
10491
|
+
let testPasses = 0;
|
|
10492
|
+
let testFails = 0;
|
|
10493
|
+
const toolMap = /* @__PURE__ */ new Map();
|
|
10494
|
+
const blockMap = /* @__PURE__ */ new Map();
|
|
10495
|
+
const agentMap = /* @__PURE__ */ new Map();
|
|
10496
|
+
const mcpMap = /* @__PURE__ */ new Map();
|
|
10497
|
+
const dailyMap = /* @__PURE__ */ new Map();
|
|
10498
|
+
const hourMap = /* @__PURE__ */ new Map();
|
|
10499
|
+
for (const e of entries) {
|
|
10500
|
+
const allow = isAllow(e.decision);
|
|
10501
|
+
const dateKey = e.ts.slice(0, 10);
|
|
10502
|
+
if (allow) allowed++;
|
|
10503
|
+
else blocked++;
|
|
10504
|
+
if (isDlp(e.checkedBy)) dlpHits++;
|
|
10505
|
+
if (e.checkedBy === "loop-detected") loopHits++;
|
|
10506
|
+
const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
|
|
10507
|
+
t.calls++;
|
|
10508
|
+
if (!allow) t.blocked++;
|
|
10509
|
+
toolMap.set(e.tool, t);
|
|
10510
|
+
if (!allow && e.checkedBy) {
|
|
10511
|
+
blockMap.set(e.checkedBy, (blockMap.get(e.checkedBy) ?? 0) + 1);
|
|
10512
|
+
}
|
|
10513
|
+
if (e.agent) agentMap.set(e.agent, (agentMap.get(e.agent) ?? 0) + 1);
|
|
10514
|
+
if (e.mcpServer) mcpMap.set(e.mcpServer, (mcpMap.get(e.mcpServer) ?? 0) + 1);
|
|
10515
|
+
const hour = new Date(e.ts).getHours();
|
|
10516
|
+
hourMap.set(hour, (hourMap.get(hour) ?? 0) + 1);
|
|
10517
|
+
const d = dailyMap.get(dateKey) ?? { calls: 0, blocked: 0 };
|
|
10518
|
+
d.calls++;
|
|
10519
|
+
if (!allow) d.blocked++;
|
|
10520
|
+
dailyMap.set(dateKey, d);
|
|
10521
|
+
}
|
|
10522
|
+
for (const e of allEntries) {
|
|
10523
|
+
if (e.source !== "test-result") continue;
|
|
10524
|
+
const ts = new Date(e.ts);
|
|
10525
|
+
if (ts < start || ts > end) continue;
|
|
10526
|
+
if (e.testResult === "pass") testPasses++;
|
|
10527
|
+
else if (e.testResult === "fail") testFails++;
|
|
10528
|
+
}
|
|
10529
|
+
const total = entries.length;
|
|
10530
|
+
const topTools = [...toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).slice(0, 8);
|
|
10531
|
+
const topBlocks = [...blockMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6);
|
|
10532
|
+
const dailyList = [...dailyMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).slice(-14);
|
|
10533
|
+
const maxTool = Math.max(...topTools.map(([, v]) => v.calls), 1);
|
|
10534
|
+
const maxBlock = Math.max(...topBlocks.map(([, v]) => v), 1);
|
|
10535
|
+
const maxDaily = Math.max(...dailyList.map(([, v]) => v.calls), 1);
|
|
10536
|
+
const W = Math.min(process.stdout.columns || 80, 100);
|
|
10537
|
+
const INNER = W - 4;
|
|
10538
|
+
const COL = Math.floor(INNER / 2) - 1;
|
|
10539
|
+
const LABEL = 24;
|
|
10540
|
+
const BAR = Math.max(6, Math.min(14, COL - LABEL - 8));
|
|
10541
|
+
const TOOL_COUNT_W = Math.max(...topTools.map(([, v]) => num(v.calls).length), 1);
|
|
10542
|
+
const BLOCK_COUNT_W = Math.max(...topBlocks.map(([, v]) => num(v).length), 1);
|
|
10543
|
+
const line = chalk9.dim("\u2500".repeat(W - 2));
|
|
10544
|
+
const periodLabel = {
|
|
10545
|
+
today: "Today",
|
|
10546
|
+
"7d": "Last 7 Days",
|
|
10547
|
+
"30d": "Last 30 Days",
|
|
10548
|
+
month: "This Month"
|
|
10549
|
+
};
|
|
10550
|
+
console.log("");
|
|
10551
|
+
console.log(
|
|
10552
|
+
" " + chalk9.bold.cyan("\u{1F6E1} node9 Report") + chalk9.dim(" \xB7 ") + chalk9.white(periodLabel[period]) + chalk9.dim(` ${fmtDate(start)} \u2013 ${fmtDate(end)}`) + chalk9.dim(` ${num(total)} events`) + (excludeTests ? chalk9.dim(` \u2013tests (\u2013${filteredTestCount})`) : "")
|
|
10553
|
+
);
|
|
10554
|
+
console.log(" " + line);
|
|
10555
|
+
console.log("");
|
|
10556
|
+
const blockLabel = blocked > 0 ? chalk9.red(`\u{1F6D1} ${num(blocked)} blocked`) : chalk9.dim("\u{1F6D1} 0 blocked");
|
|
10557
|
+
const dlpLabel = dlpHits > 0 ? chalk9.yellow(`\u{1F6A8} ${dlpHits} DLP hits`) : chalk9.dim("\u{1F6A8} 0 DLP hits");
|
|
10558
|
+
const loopLabel = loopHits > 0 ? chalk9.yellow(`\u{1F504} ${loopHits} loops`) : chalk9.dim("\u{1F504} 0 loops");
|
|
10559
|
+
const currentRate = total > 0 ? blocked / total : 0;
|
|
10560
|
+
const trendLabel = (() => {
|
|
10561
|
+
if (priorBlockRate === null) return chalk9.dim(`${pct(blocked, total)} block rate`);
|
|
10562
|
+
const delta = Math.round((currentRate - priorBlockRate) * 100);
|
|
10563
|
+
const arrow = delta > 0 ? chalk9.red(`\u25B2${delta}%`) : delta < 0 ? chalk9.green(`\u25BC${Math.abs(delta)}%`) : chalk9.dim("\u2013");
|
|
10564
|
+
return chalk9.dim(`${pct(blocked, total)} block rate `) + arrow + chalk9.dim(" vs prior");
|
|
10565
|
+
})();
|
|
10566
|
+
const reads = toolMap.get("Read")?.calls ?? 0;
|
|
10567
|
+
const edits = (toolMap.get("Edit")?.calls ?? 0) + (toolMap.get("Write")?.calls ?? 0);
|
|
10568
|
+
const ratioLabel = reads > 0 ? chalk9.dim(`edit/read ${(edits / reads).toFixed(1)}`) : chalk9.dim("edit/read \u2013");
|
|
10569
|
+
const testLabel = testPasses + testFails > 0 ? chalk9.dim("tests ") + chalk9.green(`${testPasses}\u2713`) + (testFails > 0 ? " " + chalk9.red(`${testFails}\u2717`) : "") : chalk9.dim("tests \u2013");
|
|
10570
|
+
console.log(
|
|
10571
|
+
" " + chalk9.green(`\u2705 ${num(allowed)} allowed`) + " " + blockLabel + " " + dlpLabel + " " + loopLabel + " " + trendLabel
|
|
10572
|
+
);
|
|
10573
|
+
console.log(" " + ratioLabel + " " + testLabel);
|
|
10574
|
+
console.log("");
|
|
10575
|
+
const toolHeaderRaw = "Top Tools";
|
|
10576
|
+
const blockHeaderRaw = "Top Blocks";
|
|
10577
|
+
console.log(
|
|
10578
|
+
" " + chalk9.bold(toolHeaderRaw) + " ".repeat(COL - toolHeaderRaw.length) + " " + chalk9.bold(blockHeaderRaw)
|
|
10579
|
+
);
|
|
10580
|
+
console.log(" " + chalk9.dim("\u2500".repeat(COL)) + " " + chalk9.dim("\u2500".repeat(COL)));
|
|
10581
|
+
const rows = Math.max(topTools.length, topBlocks.length, 1);
|
|
10582
|
+
for (let i = 0; i < rows; i++) {
|
|
10583
|
+
let leftStyled = " ".repeat(COL);
|
|
10584
|
+
if (i < topTools.length) {
|
|
10585
|
+
const [tool, { calls }] = topTools[i];
|
|
10586
|
+
const label = tool.length > LABEL - 1 ? tool.slice(0, LABEL - 2) + "\u2026" : tool;
|
|
10587
|
+
const countStr = num(calls).padStart(TOOL_COUNT_W);
|
|
10588
|
+
const b = colorBar(calls, maxTool, BAR);
|
|
10589
|
+
const rawLen = LABEL + BAR + 1 + TOOL_COUNT_W;
|
|
10590
|
+
const pad = Math.max(0, COL - rawLen);
|
|
10591
|
+
leftStyled = chalk9.white(label.padEnd(LABEL)) + b + " " + chalk9.white(countStr) + " ".repeat(pad);
|
|
10592
|
+
}
|
|
10593
|
+
let rightStyled = "";
|
|
10594
|
+
if (i < topBlocks.length) {
|
|
10595
|
+
const [reason, count] = topBlocks[i];
|
|
10596
|
+
const label = reason.length > LABEL - 1 ? reason.slice(0, LABEL - 2) + "\u2026" : reason;
|
|
10597
|
+
const countStr = num(count).padStart(BLOCK_COUNT_W);
|
|
10598
|
+
const b = colorBar(count, maxBlock, BAR);
|
|
10599
|
+
rightStyled = chalk9.white(label.padEnd(LABEL)) + b + " " + chalk9.red(countStr);
|
|
10600
|
+
}
|
|
10601
|
+
console.log(" " + leftStyled + " " + rightStyled);
|
|
10602
|
+
}
|
|
10603
|
+
if (topBlocks.length === 0) {
|
|
10604
|
+
console.log(" " + " ".repeat(COL) + " " + chalk9.dim("nothing blocked \u2713"));
|
|
10605
|
+
}
|
|
10606
|
+
if (agentMap.size > 1) {
|
|
10607
|
+
console.log("");
|
|
10608
|
+
console.log(" " + chalk9.bold("Agents"));
|
|
10609
|
+
console.log(" " + chalk9.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
10610
|
+
const maxAgent = Math.max(...agentMap.values(), 1);
|
|
10611
|
+
for (const [agent, count] of [...agentMap.entries()].sort((a, b) => b[1] - a[1])) {
|
|
10612
|
+
const label = agent.slice(0, LABEL - 1);
|
|
10613
|
+
const b = colorBar(count, maxAgent, BAR);
|
|
10614
|
+
console.log(" " + chalk9.white(label.padEnd(LABEL)) + b + " " + chalk9.white(num(count)));
|
|
10615
|
+
}
|
|
10616
|
+
}
|
|
10617
|
+
if (mcpMap.size > 0) {
|
|
10618
|
+
console.log("");
|
|
10619
|
+
console.log(" " + chalk9.bold("MCP Servers"));
|
|
10620
|
+
console.log(" " + chalk9.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
10621
|
+
const maxMcp = Math.max(...mcpMap.values(), 1);
|
|
10622
|
+
for (const [server, count] of [...mcpMap.entries()].sort((a, b) => b[1] - a[1])) {
|
|
10623
|
+
const label = server.slice(0, LABEL - 1).padEnd(LABEL);
|
|
10624
|
+
const b = colorBar(count, maxMcp, BAR);
|
|
10625
|
+
console.log(" " + chalk9.white(label) + b + " " + chalk9.white(num(count)));
|
|
10626
|
+
}
|
|
10627
|
+
}
|
|
10628
|
+
if (hourMap.size > 0) {
|
|
10629
|
+
const BLOCKS = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
10630
|
+
const maxHour = Math.max(...hourMap.values(), 1);
|
|
10631
|
+
const bar = Array.from({ length: 24 }, (_, h) => {
|
|
10632
|
+
const v = hourMap.get(h) ?? 0;
|
|
10633
|
+
return BLOCKS[Math.round(v / maxHour * 8)];
|
|
10634
|
+
}).join("");
|
|
10635
|
+
console.log("");
|
|
10636
|
+
console.log(" " + chalk9.bold("Hour of Day") + chalk9.dim(" (local, 0h \u2013 23h)"));
|
|
10637
|
+
console.log(" " + chalk9.cyan(bar));
|
|
10638
|
+
console.log(" " + chalk9.dim("0h" + " ".repeat(10) + "12h" + " ".repeat(7) + "23h"));
|
|
10639
|
+
}
|
|
10640
|
+
if (dailyList.length > 1) {
|
|
10641
|
+
console.log("");
|
|
10642
|
+
console.log(" " + chalk9.bold("Daily Activity"));
|
|
10643
|
+
console.log(" " + chalk9.dim("\u2500".repeat(W - 2)));
|
|
10644
|
+
const DAY_BAR = Math.max(8, Math.min(30, W - 36));
|
|
10645
|
+
for (const [dateKey, { calls, blocked: db }] of dailyList) {
|
|
10646
|
+
const label = fmtDate(dateKey).padEnd(10);
|
|
10647
|
+
const b = colorBar(calls, maxDaily, DAY_BAR);
|
|
10648
|
+
const dayCost = costByDay.get(dateKey);
|
|
10649
|
+
const costNote = dayCost ? chalk9.magenta(` ${fmtCost(dayCost)}`) : "";
|
|
10650
|
+
const blockNote = db > 0 ? chalk9.red(` ${db} blocked`) : "";
|
|
10651
|
+
console.log(
|
|
10652
|
+
" " + chalk9.dim(label) + " " + b + " " + chalk9.white(num(calls)) + blockNote + costNote
|
|
10653
|
+
);
|
|
10654
|
+
}
|
|
10655
|
+
}
|
|
10656
|
+
if (costUSD > 0) {
|
|
10657
|
+
const periodDays = Math.max(1, Math.ceil((end.getTime() - start.getTime()) / 864e5));
|
|
10658
|
+
const avgPerDay = costUSD / periodDays;
|
|
10659
|
+
const cacheHitPct = costInputTokens + costCacheRead > 0 ? Math.round(costCacheRead / (costInputTokens + costCacheRead) * 100) : 0;
|
|
10660
|
+
const costHeaderRight = [
|
|
10661
|
+
chalk9.yellow(fmtCost(costUSD)),
|
|
10662
|
+
chalk9.dim(`avg ${fmtCost(avgPerDay)}/day`),
|
|
10663
|
+
cacheHitPct > 0 ? chalk9.dim(`${cacheHitPct}% cache hit`) : null
|
|
10664
|
+
].filter(Boolean).join(chalk9.dim(" \xB7 "));
|
|
10665
|
+
console.log("");
|
|
10666
|
+
console.log(" " + chalk9.bold("Cost") + " " + costHeaderRight);
|
|
10667
|
+
console.log(" " + chalk9.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
10668
|
+
const modelList = [...costByModel.entries()].sort((a, b) => b[1] - a[1]);
|
|
10669
|
+
const maxModelCost = Math.max(...modelList.map(([, v]) => v), 1e-9);
|
|
10670
|
+
const MODEL_LABEL = 22;
|
|
10671
|
+
const MODEL_BAR = Math.max(6, Math.min(20, W - MODEL_LABEL - 12));
|
|
10672
|
+
for (const [model, cost] of modelList) {
|
|
10673
|
+
const label = model.length > MODEL_LABEL - 1 ? model.slice(0, MODEL_LABEL - 2) + "\u2026" : model;
|
|
10674
|
+
const b = colorBar(cost, maxModelCost, MODEL_BAR);
|
|
10675
|
+
console.log(
|
|
10676
|
+
" " + chalk9.white(label.padEnd(MODEL_LABEL)) + b + " " + chalk9.yellow(fmtCost(cost))
|
|
10677
|
+
);
|
|
10678
|
+
}
|
|
10679
|
+
}
|
|
10680
|
+
console.log("");
|
|
10681
|
+
console.log(
|
|
10682
|
+
" " + chalk9.dim("node9 audit --deny") + chalk9.dim(" \xB7 ") + chalk9.dim("node9 report --period today|7d|30d|month --no-tests")
|
|
10683
|
+
);
|
|
10684
|
+
console.log("");
|
|
10685
|
+
});
|
|
10686
|
+
}
|
|
10687
|
+
|
|
9942
10688
|
// src/cli/commands/daemon-cmd.ts
|
|
9943
10689
|
init_daemon2();
|
|
9944
10690
|
init_daemon();
|
|
9945
|
-
import
|
|
10691
|
+
import chalk10 from "chalk";
|
|
9946
10692
|
import { spawn as spawn7 } from "child_process";
|
|
9947
10693
|
function registerDaemonCommand(program2) {
|
|
9948
10694
|
program2.command("daemon").description("Run the local approval server").argument("[action]", "start | stop | status (default: start)").option("-b, --background", "Start the daemon in the background (detached)").option("-o, --openui", "Start in background and open browser").option(
|
|
@@ -9955,7 +10701,7 @@ function registerDaemonCommand(program2) {
|
|
|
9955
10701
|
if (cmd === "status") return daemonStatus();
|
|
9956
10702
|
if (cmd !== "start" && action !== void 0) {
|
|
9957
10703
|
console.error(
|
|
9958
|
-
|
|
10704
|
+
chalk10.red(`Unknown daemon action: "${action}". Use: start | stop | status`)
|
|
9959
10705
|
);
|
|
9960
10706
|
process.exit(1);
|
|
9961
10707
|
}
|
|
@@ -9963,7 +10709,7 @@ function registerDaemonCommand(program2) {
|
|
|
9963
10709
|
process.env.NODE9_WATCH_MODE = "1";
|
|
9964
10710
|
setTimeout(() => {
|
|
9965
10711
|
openBrowserLocal();
|
|
9966
|
-
console.log(
|
|
10712
|
+
console.log(chalk10.cyan(`\u{1F6F0}\uFE0F Flight Recorder: http://${DAEMON_HOST}:${DAEMON_PORT}/`));
|
|
9967
10713
|
}, 600);
|
|
9968
10714
|
startDaemon();
|
|
9969
10715
|
return;
|
|
@@ -9971,7 +10717,7 @@ function registerDaemonCommand(program2) {
|
|
|
9971
10717
|
if (options.openui) {
|
|
9972
10718
|
if (isDaemonRunning()) {
|
|
9973
10719
|
openBrowserLocal();
|
|
9974
|
-
console.log(
|
|
10720
|
+
console.log(chalk10.green(`\u{1F310} Opened browser: http://${DAEMON_HOST}:${DAEMON_PORT}/`));
|
|
9975
10721
|
process.exit(0);
|
|
9976
10722
|
}
|
|
9977
10723
|
const child = spawn7(process.execPath, [process.argv[1], "daemon"], {
|
|
@@ -9984,7 +10730,7 @@ function registerDaemonCommand(program2) {
|
|
|
9984
10730
|
if (isDaemonRunning()) break;
|
|
9985
10731
|
}
|
|
9986
10732
|
openBrowserLocal();
|
|
9987
|
-
console.log(
|
|
10733
|
+
console.log(chalk10.green(`
|
|
9988
10734
|
\u{1F6E1}\uFE0F Node9 daemon started + browser opened`));
|
|
9989
10735
|
process.exit(0);
|
|
9990
10736
|
}
|
|
@@ -9994,7 +10740,7 @@ function registerDaemonCommand(program2) {
|
|
|
9994
10740
|
stdio: "ignore"
|
|
9995
10741
|
});
|
|
9996
10742
|
child.unref();
|
|
9997
|
-
console.log(
|
|
10743
|
+
console.log(chalk10.green(`
|
|
9998
10744
|
\u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
|
|
9999
10745
|
process.exit(0);
|
|
10000
10746
|
}
|
|
@@ -10006,13 +10752,13 @@ function registerDaemonCommand(program2) {
|
|
|
10006
10752
|
// src/cli/commands/status.ts
|
|
10007
10753
|
init_core();
|
|
10008
10754
|
init_daemon();
|
|
10009
|
-
import
|
|
10010
|
-
import
|
|
10011
|
-
import
|
|
10012
|
-
import
|
|
10755
|
+
import chalk11 from "chalk";
|
|
10756
|
+
import fs25 from "fs";
|
|
10757
|
+
import path27 from "path";
|
|
10758
|
+
import os21 from "os";
|
|
10013
10759
|
function readJson2(filePath) {
|
|
10014
10760
|
try {
|
|
10015
|
-
if (
|
|
10761
|
+
if (fs25.existsSync(filePath)) return JSON.parse(fs25.readFileSync(filePath, "utf-8"));
|
|
10016
10762
|
} catch {
|
|
10017
10763
|
}
|
|
10018
10764
|
return null;
|
|
@@ -10026,21 +10772,21 @@ function wrappedMcpServers(servers) {
|
|
|
10026
10772
|
return Object.entries(servers).filter(([, s]) => s.command === "node9" && Array.isArray(s.args) && s.args.length > 0).map(([name, s]) => `${name} \u2192 ${s.args.join(" ")}`);
|
|
10027
10773
|
}
|
|
10028
10774
|
function printAgentSection(label, hookPairs, wrapped) {
|
|
10029
|
-
console.log(
|
|
10775
|
+
console.log(chalk11.bold(` ${label}`));
|
|
10030
10776
|
for (const { name, present } of hookPairs) {
|
|
10031
10777
|
if (present) {
|
|
10032
|
-
console.log(
|
|
10778
|
+
console.log(chalk11.green(` \u2713 ${name}`));
|
|
10033
10779
|
} else {
|
|
10034
|
-
console.log(
|
|
10780
|
+
console.log(chalk11.red(` \u2717 ${name}`) + chalk11.gray(" (not wired)"));
|
|
10035
10781
|
}
|
|
10036
10782
|
}
|
|
10037
10783
|
if (wrapped.length > 0) {
|
|
10038
|
-
console.log(
|
|
10784
|
+
console.log(chalk11.cyan(` MCP proxied:`));
|
|
10039
10785
|
for (const entry of wrapped) {
|
|
10040
|
-
console.log(
|
|
10786
|
+
console.log(chalk11.gray(` \u2022 ${entry}`));
|
|
10041
10787
|
}
|
|
10042
10788
|
} else {
|
|
10043
|
-
console.log(
|
|
10789
|
+
console.log(chalk11.gray(` MCP proxied: none`));
|
|
10044
10790
|
}
|
|
10045
10791
|
}
|
|
10046
10792
|
function registerStatusCommand(program2) {
|
|
@@ -10051,58 +10797,58 @@ function registerStatusCommand(program2) {
|
|
|
10051
10797
|
const settings = mergedConfig.settings;
|
|
10052
10798
|
console.log("");
|
|
10053
10799
|
if (creds && settings.approvers.cloud) {
|
|
10054
|
-
console.log(
|
|
10800
|
+
console.log(chalk11.green(" \u25CF Agent mode") + chalk11.gray(" \u2014 cloud team policy enforced"));
|
|
10055
10801
|
} else if (creds && !settings.approvers.cloud) {
|
|
10056
10802
|
console.log(
|
|
10057
|
-
|
|
10803
|
+
chalk11.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + chalk11.gray(" \u2014 all decisions stay on this machine")
|
|
10058
10804
|
);
|
|
10059
10805
|
} else {
|
|
10060
10806
|
console.log(
|
|
10061
|
-
|
|
10807
|
+
chalk11.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + chalk11.gray(" \u2014 no API key (Local rules only)")
|
|
10062
10808
|
);
|
|
10063
10809
|
}
|
|
10064
10810
|
console.log("");
|
|
10065
10811
|
if (daemonRunning) {
|
|
10066
10812
|
console.log(
|
|
10067
|
-
|
|
10813
|
+
chalk11.green(" \u25CF Daemon running") + chalk11.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT}/`)
|
|
10068
10814
|
);
|
|
10069
10815
|
} else {
|
|
10070
|
-
console.log(
|
|
10816
|
+
console.log(chalk11.gray(" \u25CB Daemon stopped"));
|
|
10071
10817
|
}
|
|
10072
10818
|
if (settings.enableUndo) {
|
|
10073
10819
|
console.log(
|
|
10074
|
-
|
|
10820
|
+
chalk11.magenta(" \u25CF Undo Engine") + chalk11.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
|
|
10075
10821
|
);
|
|
10076
10822
|
}
|
|
10077
10823
|
console.log("");
|
|
10078
|
-
const modeLabel = settings.mode === "audit" ?
|
|
10824
|
+
const modeLabel = settings.mode === "audit" ? chalk11.blue("audit") : settings.mode === "strict" ? chalk11.red("strict") : chalk11.white("standard");
|
|
10079
10825
|
console.log(` Mode: ${modeLabel}`);
|
|
10080
|
-
const projectConfig =
|
|
10081
|
-
const globalConfig =
|
|
10826
|
+
const projectConfig = path27.join(process.cwd(), "node9.config.json");
|
|
10827
|
+
const globalConfig = path27.join(os21.homedir(), ".node9", "config.json");
|
|
10082
10828
|
console.log(
|
|
10083
|
-
` Local: ${
|
|
10829
|
+
` Local: ${fs25.existsSync(projectConfig) ? chalk11.green("Active (node9.config.json)") : chalk11.gray("Not present")}`
|
|
10084
10830
|
);
|
|
10085
10831
|
console.log(
|
|
10086
|
-
` Global: ${
|
|
10832
|
+
` Global: ${fs25.existsSync(globalConfig) ? chalk11.green("Active (~/.node9/config.json)") : chalk11.gray("Not present")}`
|
|
10087
10833
|
);
|
|
10088
10834
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
10089
10835
|
console.log(
|
|
10090
|
-
` Sandbox: ${
|
|
10836
|
+
` Sandbox: ${chalk11.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
10091
10837
|
);
|
|
10092
10838
|
}
|
|
10093
|
-
const homeDir2 =
|
|
10839
|
+
const homeDir2 = os21.homedir();
|
|
10094
10840
|
const claudeSettings = readJson2(
|
|
10095
|
-
|
|
10841
|
+
path27.join(homeDir2, ".claude", "settings.json")
|
|
10096
10842
|
);
|
|
10097
|
-
const claudeConfig = readJson2(
|
|
10843
|
+
const claudeConfig = readJson2(path27.join(homeDir2, ".claude.json"));
|
|
10098
10844
|
const geminiSettings = readJson2(
|
|
10099
|
-
|
|
10845
|
+
path27.join(homeDir2, ".gemini", "settings.json")
|
|
10100
10846
|
);
|
|
10101
|
-
const cursorConfig = readJson2(
|
|
10847
|
+
const cursorConfig = readJson2(path27.join(homeDir2, ".cursor", "mcp.json"));
|
|
10102
10848
|
const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
|
|
10103
10849
|
if (agentFound) {
|
|
10104
10850
|
console.log("");
|
|
10105
|
-
console.log(
|
|
10851
|
+
console.log(chalk11.bold(" Agent Wiring:"));
|
|
10106
10852
|
console.log("");
|
|
10107
10853
|
if (claudeSettings || claudeConfig) {
|
|
10108
10854
|
const preHook = claudeSettings?.hooks?.PreToolUse?.some(
|
|
@@ -10148,7 +10894,7 @@ function registerStatusCommand(program2) {
|
|
|
10148
10894
|
const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
|
|
10149
10895
|
console.log("");
|
|
10150
10896
|
console.log(
|
|
10151
|
-
|
|
10897
|
+
chalk11.yellow(` \u23F8 PAUSED until ${expiresAt}`) + chalk11.gray(" \u2014 all tool calls allowed")
|
|
10152
10898
|
);
|
|
10153
10899
|
}
|
|
10154
10900
|
console.log("");
|
|
@@ -10157,10 +10903,10 @@ function registerStatusCommand(program2) {
|
|
|
10157
10903
|
|
|
10158
10904
|
// src/cli/commands/init.ts
|
|
10159
10905
|
init_core();
|
|
10160
|
-
import
|
|
10161
|
-
import
|
|
10162
|
-
import
|
|
10163
|
-
import
|
|
10906
|
+
import chalk12 from "chalk";
|
|
10907
|
+
import fs26 from "fs";
|
|
10908
|
+
import path28 from "path";
|
|
10909
|
+
import os22 from "os";
|
|
10164
10910
|
import https2 from "https";
|
|
10165
10911
|
init_shields();
|
|
10166
10912
|
var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
|
|
@@ -10195,7 +10941,7 @@ function fireTelemetryPing(agents) {
|
|
|
10195
10941
|
}
|
|
10196
10942
|
function registerInitCommand(program2) {
|
|
10197
10943
|
program2.command("init").description("Set up Node9: create config and wire all detected AI agents").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").option("--skip-setup", "Only create config \u2014 do not wire AI agents").action(async (options) => {
|
|
10198
|
-
console.log(
|
|
10944
|
+
console.log(chalk12.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
|
|
10199
10945
|
let chosenMode = options.mode.toLowerCase();
|
|
10200
10946
|
if (!["standard", "strict", "audit"].includes(chosenMode)) {
|
|
10201
10947
|
chosenMode = DEFAULT_CONFIG.settings.mode;
|
|
@@ -10214,37 +10960,37 @@ function registerInitCommand(program2) {
|
|
|
10214
10960
|
const hasNewShields = DEFAULT_SHIELDS.some((s) => !current.includes(s));
|
|
10215
10961
|
if (hasNewShields) writeActiveShields(merged);
|
|
10216
10962
|
} catch (err2) {
|
|
10217
|
-
console.log(
|
|
10963
|
+
console.log(chalk12.yellow(` \u26A0\uFE0F Could not update shields: ${String(err2)}`));
|
|
10218
10964
|
}
|
|
10219
10965
|
}
|
|
10220
10966
|
console.log("");
|
|
10221
10967
|
}
|
|
10222
|
-
const configPath =
|
|
10223
|
-
if (
|
|
10968
|
+
const configPath = path28.join(os22.homedir(), ".node9", "config.json");
|
|
10969
|
+
if (fs26.existsSync(configPath) && !options.force) {
|
|
10224
10970
|
try {
|
|
10225
|
-
const existing = JSON.parse(
|
|
10971
|
+
const existing = JSON.parse(fs26.readFileSync(configPath, "utf-8"));
|
|
10226
10972
|
const settings = existing.settings ?? {};
|
|
10227
10973
|
if (settings.mode !== chosenMode) {
|
|
10228
10974
|
settings.mode = chosenMode;
|
|
10229
10975
|
existing.settings = settings;
|
|
10230
|
-
|
|
10231
|
-
console.log(
|
|
10976
|
+
fs26.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
|
|
10977
|
+
console.log(chalk12.green(`\u2705 Mode updated: ${chosenMode}`));
|
|
10232
10978
|
} else {
|
|
10233
|
-
console.log(
|
|
10979
|
+
console.log(chalk12.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
10234
10980
|
}
|
|
10235
10981
|
} catch {
|
|
10236
|
-
console.log(
|
|
10982
|
+
console.log(chalk12.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
10237
10983
|
}
|
|
10238
10984
|
} else {
|
|
10239
10985
|
const configToSave = {
|
|
10240
10986
|
...DEFAULT_CONFIG,
|
|
10241
10987
|
settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
|
|
10242
10988
|
};
|
|
10243
|
-
const dir =
|
|
10244
|
-
if (!
|
|
10245
|
-
|
|
10246
|
-
console.log(
|
|
10247
|
-
console.log(
|
|
10989
|
+
const dir = path28.dirname(configPath);
|
|
10990
|
+
if (!fs26.existsSync(dir)) fs26.mkdirSync(dir, { recursive: true });
|
|
10991
|
+
fs26.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
|
|
10992
|
+
console.log(chalk12.green(`\u2705 Config created: ${configPath}`));
|
|
10993
|
+
console.log(chalk12.gray(` Mode: ${chosenMode}`));
|
|
10248
10994
|
}
|
|
10249
10995
|
if (options.skipSetup) return;
|
|
10250
10996
|
console.log("");
|
|
@@ -10254,18 +11000,18 @@ function registerInitCommand(program2) {
|
|
|
10254
11000
|
);
|
|
10255
11001
|
if (found.length === 0) {
|
|
10256
11002
|
console.log(
|
|
10257
|
-
|
|
11003
|
+
chalk12.gray("No AI agents detected. Install Claude Code, Gemini CLI, Cursor, or Codex")
|
|
10258
11004
|
);
|
|
10259
|
-
console.log(
|
|
11005
|
+
console.log(chalk12.gray("then run: node9 addto <claude|gemini|cursor|codex>"));
|
|
10260
11006
|
return;
|
|
10261
11007
|
}
|
|
10262
|
-
console.log(
|
|
11008
|
+
console.log(chalk12.bold("Detected agents:"));
|
|
10263
11009
|
for (const agent of found) {
|
|
10264
|
-
console.log(
|
|
11010
|
+
console.log(chalk12.green(` \u2713 ${agent}`));
|
|
10265
11011
|
}
|
|
10266
11012
|
console.log("");
|
|
10267
11013
|
for (const agent of found) {
|
|
10268
|
-
console.log(
|
|
11014
|
+
console.log(chalk12.bold(`Wiring ${agent}...`));
|
|
10269
11015
|
if (agent === "claude") await setupClaude();
|
|
10270
11016
|
else if (agent === "gemini") await setupGemini();
|
|
10271
11017
|
else if (agent === "cursor") await setupCursor();
|
|
@@ -10282,26 +11028,26 @@ function registerInitCommand(program2) {
|
|
|
10282
11028
|
console.log("");
|
|
10283
11029
|
}
|
|
10284
11030
|
const agentList = found.join(", ");
|
|
10285
|
-
console.log(
|
|
11031
|
+
console.log(chalk12.green.bold(`\u{1F6E1}\uFE0F Node9 is protecting ${agentList}!`));
|
|
10286
11032
|
console.log("");
|
|
10287
|
-
console.log(
|
|
10288
|
-
console.log(
|
|
11033
|
+
console.log(chalk12.white(" Watch live: ") + chalk12.cyan("node9 tail"));
|
|
11034
|
+
console.log(chalk12.white(" Local UI: ") + chalk12.cyan("node9 daemon --openui"));
|
|
10289
11035
|
console.log("");
|
|
10290
|
-
console.log(
|
|
11036
|
+
console.log(chalk12.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10291
11037
|
console.log(
|
|
10292
|
-
|
|
11038
|
+
chalk12.white(" Team dashboard + full audit trail \u2192 ") + chalk12.cyan.bold("https://node9.ai")
|
|
10293
11039
|
);
|
|
10294
|
-
console.log(
|
|
11040
|
+
console.log(chalk12.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
10295
11041
|
});
|
|
10296
11042
|
}
|
|
10297
11043
|
|
|
10298
11044
|
// src/cli/commands/undo.ts
|
|
10299
|
-
import
|
|
10300
|
-
import
|
|
11045
|
+
import path29 from "path";
|
|
11046
|
+
import chalk14 from "chalk";
|
|
10301
11047
|
|
|
10302
11048
|
// src/tui/undo-navigator.ts
|
|
10303
11049
|
import readline2 from "readline";
|
|
10304
|
-
import
|
|
11050
|
+
import chalk13 from "chalk";
|
|
10305
11051
|
var RESET = "\x1B[0m";
|
|
10306
11052
|
var BOLD = "\x1B[1m";
|
|
10307
11053
|
var CLEAR_SCREEN = "\x1B[2J\x1B[H";
|
|
@@ -10319,15 +11065,15 @@ function renderDiff(raw) {
|
|
|
10319
11065
|
);
|
|
10320
11066
|
for (const line of lines) {
|
|
10321
11067
|
if (line.startsWith("+++") || line.startsWith("---")) {
|
|
10322
|
-
process.stdout.write(
|
|
11068
|
+
process.stdout.write(chalk13.bold(line) + "\n");
|
|
10323
11069
|
} else if (line.startsWith("+")) {
|
|
10324
|
-
process.stdout.write(
|
|
11070
|
+
process.stdout.write(chalk13.green(line) + "\n");
|
|
10325
11071
|
} else if (line.startsWith("-")) {
|
|
10326
|
-
process.stdout.write(
|
|
11072
|
+
process.stdout.write(chalk13.red(line) + "\n");
|
|
10327
11073
|
} else if (line.startsWith("@@")) {
|
|
10328
|
-
process.stdout.write(
|
|
11074
|
+
process.stdout.write(chalk13.cyan(line) + "\n");
|
|
10329
11075
|
} else {
|
|
10330
|
-
process.stdout.write(
|
|
11076
|
+
process.stdout.write(chalk13.gray(line) + "\n");
|
|
10331
11077
|
}
|
|
10332
11078
|
}
|
|
10333
11079
|
}
|
|
@@ -10346,23 +11092,23 @@ function render(entries, idx) {
|
|
|
10346
11092
|
const step = idx + 1;
|
|
10347
11093
|
process.stdout.write(CLEAR_SCREEN);
|
|
10348
11094
|
process.stdout.write(
|
|
10349
|
-
|
|
11095
|
+
chalk13.magenta.bold(`\u23EA Node9 Undo`) + chalk13.gray(` \u2500\u2500 step ${step} of ${total}`) + (entry.files?.length ? chalk13.gray(
|
|
10350
11096
|
` \u2500\u2500 ${entry.files.slice(0, 2).join(", ")}${entry.files.length > 2 ? ` +${entry.files.length - 2} more` : ""}`
|
|
10351
11097
|
) : "") + "\n\n"
|
|
10352
11098
|
);
|
|
10353
11099
|
process.stdout.write(
|
|
10354
|
-
` ${BOLD}Tool:${RESET} ${
|
|
11100
|
+
` ${BOLD}Tool:${RESET} ${chalk13.cyan(entry.tool)}` + (entry.argsSummary ? chalk13.gray(" \u2192 " + entry.argsSummary) : "") + "\n"
|
|
10355
11101
|
);
|
|
10356
|
-
process.stdout.write(` ${BOLD}When:${RESET} ${
|
|
11102
|
+
process.stdout.write(` ${BOLD}When:${RESET} ${chalk13.gray(formatAge(entry.timestamp))}
|
|
10357
11103
|
`);
|
|
10358
|
-
process.stdout.write(` ${BOLD}Dir: ${RESET} ${
|
|
11104
|
+
process.stdout.write(` ${BOLD}Dir: ${RESET} ${chalk13.gray(entry.cwd)}
|
|
10359
11105
|
`);
|
|
10360
11106
|
if (entry.files && entry.files.length > 0) {
|
|
10361
|
-
process.stdout.write(` ${BOLD}Files:${RESET} ${
|
|
11107
|
+
process.stdout.write(` ${BOLD}Files:${RESET} ${chalk13.gray(entry.files.join(", "))}
|
|
10362
11108
|
`);
|
|
10363
11109
|
}
|
|
10364
11110
|
if (idx < total - 1 && isSessionBoundary(entries, idx + 1)) {
|
|
10365
|
-
process.stdout.write(
|
|
11111
|
+
process.stdout.write(chalk13.gray("\n \u2500\u2500 session boundary above \u2500\u2500\n"));
|
|
10366
11112
|
}
|
|
10367
11113
|
process.stdout.write("\n");
|
|
10368
11114
|
const diff = entry.diff ?? computeUndoDiff(entry.hash, entry.cwd);
|
|
@@ -10370,12 +11116,12 @@ function render(entries, idx) {
|
|
|
10370
11116
|
renderDiff(diff);
|
|
10371
11117
|
} else {
|
|
10372
11118
|
process.stdout.write(
|
|
10373
|
-
|
|
11119
|
+
chalk13.gray(" (no diff \u2014 working tree may already match this snapshot)\n")
|
|
10374
11120
|
);
|
|
10375
11121
|
}
|
|
10376
11122
|
process.stdout.write("\n");
|
|
10377
11123
|
process.stdout.write(
|
|
10378
|
-
|
|
11124
|
+
chalk13.gray(" ") + (idx < total - 1 ? chalk13.white("[\u2190] older") : chalk13.gray("[\u2190] older")) + chalk13.gray(" ") + (idx > 0 ? chalk13.white("[\u2192] newer") : chalk13.gray("[\u2192] newer")) + chalk13.gray(" ") + chalk13.green("[\u21B5] restore here") + chalk13.gray(" ") + chalk13.yellow("[s] session start") + chalk13.gray(" ") + chalk13.gray("[q] quit") + "\n"
|
|
10379
11125
|
);
|
|
10380
11126
|
}
|
|
10381
11127
|
async function runUndoNavigator(entries) {
|
|
@@ -10429,19 +11175,19 @@ async function runUndoNavigator(entries) {
|
|
|
10429
11175
|
cleanup();
|
|
10430
11176
|
process.stdout.write(CLEAR_SCREEN);
|
|
10431
11177
|
const entry = display[idx];
|
|
10432
|
-
process.stdout.write(
|
|
11178
|
+
process.stdout.write(chalk13.magenta.bold("\n\u23EA Restoring snapshot...\n\n"));
|
|
10433
11179
|
if (applyUndo(entry.hash, entry.cwd)) {
|
|
10434
|
-
process.stdout.write(
|
|
11180
|
+
process.stdout.write(chalk13.green("\u2705 Reverted successfully.\n\n"));
|
|
10435
11181
|
resolve({ restored: true });
|
|
10436
11182
|
} else {
|
|
10437
|
-
process.stdout.write(
|
|
11183
|
+
process.stdout.write(chalk13.red("\u274C Undo failed.\n\n"));
|
|
10438
11184
|
resolve({ restored: false });
|
|
10439
11185
|
}
|
|
10440
11186
|
} else if (name === "q" || key?.ctrl && name === "c") {
|
|
10441
11187
|
done = true;
|
|
10442
11188
|
cleanup();
|
|
10443
11189
|
process.stdout.write(CLEAR_SCREEN);
|
|
10444
|
-
process.stdout.write(
|
|
11190
|
+
process.stdout.write(chalk13.gray("\nCancelled.\n\n"));
|
|
10445
11191
|
resolve({ restored: false });
|
|
10446
11192
|
}
|
|
10447
11193
|
};
|
|
@@ -10455,7 +11201,7 @@ function findMatchingCwd(startDir, history) {
|
|
|
10455
11201
|
let dir = startDir;
|
|
10456
11202
|
while (true) {
|
|
10457
11203
|
if (cwds.has(dir)) return dir;
|
|
10458
|
-
const parent =
|
|
11204
|
+
const parent = path29.dirname(dir);
|
|
10459
11205
|
if (parent === dir) return null;
|
|
10460
11206
|
dir = parent;
|
|
10461
11207
|
}
|
|
@@ -10477,39 +11223,39 @@ function registerUndoCommand(program2) {
|
|
|
10477
11223
|
if (history.length === 0) {
|
|
10478
11224
|
if (!options.all && allHistory.length > 0) {
|
|
10479
11225
|
console.log(
|
|
10480
|
-
|
|
11226
|
+
chalk14.yellow(
|
|
10481
11227
|
`
|
|
10482
11228
|
\u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
|
|
10483
|
-
Run ${
|
|
11229
|
+
Run ${chalk14.cyan("node9 undo --all")} to see snapshots from all projects.
|
|
10484
11230
|
`
|
|
10485
11231
|
)
|
|
10486
11232
|
);
|
|
10487
11233
|
} else {
|
|
10488
|
-
console.log(
|
|
11234
|
+
console.log(chalk14.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
|
|
10489
11235
|
}
|
|
10490
11236
|
return;
|
|
10491
11237
|
}
|
|
10492
11238
|
if (options.list) {
|
|
10493
|
-
console.log(
|
|
11239
|
+
console.log(chalk14.magenta.bold("\n\u23EA Snapshot History\n"));
|
|
10494
11240
|
console.log(
|
|
10495
|
-
|
|
11241
|
+
chalk14.gray(
|
|
10496
11242
|
` ${"#".padEnd(3)} ${"File / Command".padEnd(30)} ${"Tool".padEnd(8)} ${"When".padEnd(10)} Dir`
|
|
10497
11243
|
)
|
|
10498
11244
|
);
|
|
10499
|
-
console.log(
|
|
11245
|
+
console.log(chalk14.gray(" " + "\u2500".repeat(80)));
|
|
10500
11246
|
const display = [...history].reverse();
|
|
10501
11247
|
let prevTs = null;
|
|
10502
11248
|
for (let i = 0; i < display.length; i++) {
|
|
10503
11249
|
const e = display[i];
|
|
10504
11250
|
const isGap = prevTs !== null && prevTs - e.timestamp > 6e4;
|
|
10505
|
-
if (isGap) console.log(
|
|
11251
|
+
if (isGap) console.log(chalk14.gray(" \u2500\u2500 earlier \u2500\u2500"));
|
|
10506
11252
|
const label = (e.argsSummary || e.files?.[0] || "\u2014").slice(0, 30).padEnd(30);
|
|
10507
11253
|
const tool = e.tool.slice(0, 8).padEnd(8);
|
|
10508
11254
|
const when = formatAge2(e.timestamp).padEnd(10);
|
|
10509
11255
|
const dir = e.cwd.length > 30 ? "\u2026" + e.cwd.slice(-29) : e.cwd;
|
|
10510
11256
|
console.log(
|
|
10511
|
-
|
|
10512
|
-
` ${String(i + 1).padEnd(3)} ${label} ${
|
|
11257
|
+
chalk14.white(
|
|
11258
|
+
` ${String(i + 1).padEnd(3)} ${label} ${chalk14.cyan(tool)} ${chalk14.gray(when)} ${chalk14.gray(dir)}`
|
|
10513
11259
|
)
|
|
10514
11260
|
);
|
|
10515
11261
|
prevTs = e.timestamp;
|
|
@@ -10522,7 +11268,7 @@ function registerUndoCommand(program2) {
|
|
|
10522
11268
|
const idx = history.length - steps;
|
|
10523
11269
|
if (idx < 0) {
|
|
10524
11270
|
console.log(
|
|
10525
|
-
|
|
11271
|
+
chalk14.yellow(
|
|
10526
11272
|
`
|
|
10527
11273
|
\u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
|
|
10528
11274
|
`
|
|
@@ -10533,47 +11279,47 @@ function registerUndoCommand(program2) {
|
|
|
10533
11279
|
const snapshot = history[idx];
|
|
10534
11280
|
const ageStr = formatAge2(snapshot.timestamp);
|
|
10535
11281
|
console.log(
|
|
10536
|
-
|
|
11282
|
+
chalk14.magenta.bold(`
|
|
10537
11283
|
\u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`)
|
|
10538
11284
|
);
|
|
10539
11285
|
console.log(
|
|
10540
|
-
|
|
10541
|
-
` Tool: ${
|
|
11286
|
+
chalk14.white(
|
|
11287
|
+
` Tool: ${chalk14.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk14.gray(" \u2192 " + snapshot.argsSummary) : ""}`
|
|
10542
11288
|
)
|
|
10543
11289
|
);
|
|
10544
|
-
console.log(
|
|
10545
|
-
console.log(
|
|
11290
|
+
console.log(chalk14.white(` When: ${chalk14.gray(ageStr)}`));
|
|
11291
|
+
console.log(chalk14.white(` Dir: ${chalk14.gray(snapshot.cwd)}`));
|
|
10546
11292
|
if (steps > 1)
|
|
10547
11293
|
console.log(
|
|
10548
|
-
|
|
11294
|
+
chalk14.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
|
|
10549
11295
|
);
|
|
10550
11296
|
console.log("");
|
|
10551
11297
|
const diff = snapshot.diff ?? computeUndoDiff(snapshot.hash, snapshot.cwd);
|
|
10552
11298
|
if (diff) {
|
|
10553
11299
|
const lines = diff.split("\n").filter((l) => !l.startsWith("diff --git") && !l.startsWith("index "));
|
|
10554
11300
|
for (const line of lines) {
|
|
10555
|
-
if (line.startsWith("+++") || line.startsWith("---")) console.log(
|
|
10556
|
-
else if (line.startsWith("+")) console.log(
|
|
10557
|
-
else if (line.startsWith("-")) console.log(
|
|
10558
|
-
else if (line.startsWith("@@")) console.log(
|
|
10559
|
-
else console.log(
|
|
11301
|
+
if (line.startsWith("+++") || line.startsWith("---")) console.log(chalk14.bold(line));
|
|
11302
|
+
else if (line.startsWith("+")) console.log(chalk14.green(line));
|
|
11303
|
+
else if (line.startsWith("-")) console.log(chalk14.red(line));
|
|
11304
|
+
else if (line.startsWith("@@")) console.log(chalk14.cyan(line));
|
|
11305
|
+
else console.log(chalk14.gray(line));
|
|
10560
11306
|
}
|
|
10561
11307
|
console.log("");
|
|
10562
11308
|
} else {
|
|
10563
11309
|
console.log(
|
|
10564
|
-
|
|
11310
|
+
chalk14.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
|
|
10565
11311
|
);
|
|
10566
11312
|
}
|
|
10567
11313
|
const { confirm: confirm3 } = await import("@inquirer/prompts");
|
|
10568
11314
|
const proceed = await confirm3({ message: `Revert to this snapshot?`, default: false });
|
|
10569
11315
|
if (proceed) {
|
|
10570
11316
|
if (applyUndo(snapshot.hash, snapshot.cwd)) {
|
|
10571
|
-
console.log(
|
|
11317
|
+
console.log(chalk14.green("\n\u2705 Reverted successfully.\n"));
|
|
10572
11318
|
} else {
|
|
10573
|
-
console.error(
|
|
11319
|
+
console.error(chalk14.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
|
|
10574
11320
|
}
|
|
10575
11321
|
} else {
|
|
10576
|
-
console.log(
|
|
11322
|
+
console.log(chalk14.gray("\nCancelled.\n"));
|
|
10577
11323
|
}
|
|
10578
11324
|
return;
|
|
10579
11325
|
}
|
|
@@ -10583,7 +11329,7 @@ function registerUndoCommand(program2) {
|
|
|
10583
11329
|
|
|
10584
11330
|
// src/cli/commands/watch.ts
|
|
10585
11331
|
init_daemon();
|
|
10586
|
-
import
|
|
11332
|
+
import chalk15 from "chalk";
|
|
10587
11333
|
import { spawn as spawn8, spawnSync as spawnSync5 } from "child_process";
|
|
10588
11334
|
function registerWatchCommand(program2) {
|
|
10589
11335
|
program2.command("watch").description("Run a command under Node9 watch mode (daemon stays alive for the session)").argument("<command>", "Command to run").argument("[args...]", "Arguments for the command").action(async (cmd, args) => {
|
|
@@ -10599,7 +11345,7 @@ function registerWatchCommand(program2) {
|
|
|
10599
11345
|
throw new Error("not running");
|
|
10600
11346
|
}
|
|
10601
11347
|
} catch {
|
|
10602
|
-
console.error(
|
|
11348
|
+
console.error(chalk15.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
|
|
10603
11349
|
const child = spawn8(process.execPath, [process.argv[1], "daemon"], {
|
|
10604
11350
|
detached: true,
|
|
10605
11351
|
stdio: "ignore",
|
|
@@ -10621,12 +11367,12 @@ function registerWatchCommand(program2) {
|
|
|
10621
11367
|
}
|
|
10622
11368
|
}
|
|
10623
11369
|
if (!ready) {
|
|
10624
|
-
console.error(
|
|
11370
|
+
console.error(chalk15.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
10625
11371
|
process.exit(1);
|
|
10626
11372
|
}
|
|
10627
11373
|
}
|
|
10628
11374
|
console.error(
|
|
10629
|
-
|
|
11375
|
+
chalk15.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + chalk15.dim(` \u2192 localhost:${port}`) + chalk15.dim(
|
|
10630
11376
|
"\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
|
|
10631
11377
|
)
|
|
10632
11378
|
);
|
|
@@ -10635,7 +11381,7 @@ function registerWatchCommand(program2) {
|
|
|
10635
11381
|
env: { ...process.env, NODE9_WATCH_MODE: "1" }
|
|
10636
11382
|
});
|
|
10637
11383
|
if (result.error) {
|
|
10638
|
-
console.error(
|
|
11384
|
+
console.error(chalk15.red(`\u274C Failed to run command: ${result.error.message}`));
|
|
10639
11385
|
process.exit(1);
|
|
10640
11386
|
}
|
|
10641
11387
|
process.exit(result.status ?? 0);
|
|
@@ -10645,18 +11391,18 @@ function registerWatchCommand(program2) {
|
|
|
10645
11391
|
// src/mcp-gateway/index.ts
|
|
10646
11392
|
init_orchestrator();
|
|
10647
11393
|
import readline3 from "readline";
|
|
10648
|
-
import
|
|
11394
|
+
import chalk16 from "chalk";
|
|
10649
11395
|
import { spawn as spawn9 } from "child_process";
|
|
10650
11396
|
import { execa as execa2 } from "execa";
|
|
10651
11397
|
init_provenance();
|
|
10652
11398
|
|
|
10653
11399
|
// src/mcp-pin.ts
|
|
10654
|
-
import
|
|
10655
|
-
import
|
|
10656
|
-
import
|
|
10657
|
-
import
|
|
11400
|
+
import fs27 from "fs";
|
|
11401
|
+
import path30 from "path";
|
|
11402
|
+
import os23 from "os";
|
|
11403
|
+
import crypto4 from "crypto";
|
|
10658
11404
|
function getPinsFilePath() {
|
|
10659
|
-
return
|
|
11405
|
+
return path30.join(os23.homedir(), ".node9", "mcp-pins.json");
|
|
10660
11406
|
}
|
|
10661
11407
|
function hashToolDefinitions(tools) {
|
|
10662
11408
|
const sorted = [...tools].sort((a, b) => {
|
|
@@ -10665,15 +11411,15 @@ function hashToolDefinitions(tools) {
|
|
|
10665
11411
|
return nameA.localeCompare(nameB);
|
|
10666
11412
|
});
|
|
10667
11413
|
const canonical = JSON.stringify(sorted);
|
|
10668
|
-
return
|
|
11414
|
+
return crypto4.createHash("sha256").update(canonical).digest("hex");
|
|
10669
11415
|
}
|
|
10670
11416
|
function getServerKey(upstreamCommand) {
|
|
10671
|
-
return
|
|
11417
|
+
return crypto4.createHash("sha256").update(upstreamCommand).digest("hex").slice(0, 16);
|
|
10672
11418
|
}
|
|
10673
11419
|
function readMcpPinsSafe() {
|
|
10674
11420
|
const filePath = getPinsFilePath();
|
|
10675
11421
|
try {
|
|
10676
|
-
const raw =
|
|
11422
|
+
const raw = fs27.readFileSync(filePath, "utf-8");
|
|
10677
11423
|
if (!raw.trim()) {
|
|
10678
11424
|
return { ok: false, reason: "corrupt", detail: "empty file" };
|
|
10679
11425
|
}
|
|
@@ -10697,10 +11443,10 @@ function readMcpPins() {
|
|
|
10697
11443
|
}
|
|
10698
11444
|
function writeMcpPins(data) {
|
|
10699
11445
|
const filePath = getPinsFilePath();
|
|
10700
|
-
|
|
10701
|
-
const tmp = `${filePath}.${
|
|
10702
|
-
|
|
10703
|
-
|
|
11446
|
+
fs27.mkdirSync(path30.dirname(filePath), { recursive: true });
|
|
11447
|
+
const tmp = `${filePath}.${crypto4.randomBytes(6).toString("hex")}.tmp`;
|
|
11448
|
+
fs27.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
|
|
11449
|
+
fs27.renameSync(tmp, filePath);
|
|
10704
11450
|
}
|
|
10705
11451
|
function checkPin(serverKey, currentHash) {
|
|
10706
11452
|
const result = readMcpPinsSafe();
|
|
@@ -10792,13 +11538,13 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
10792
11538
|
const prov = checkProvenance(executable);
|
|
10793
11539
|
if (prov.trustLevel === "suspect") {
|
|
10794
11540
|
console.error(
|
|
10795
|
-
|
|
11541
|
+
chalk16.red(
|
|
10796
11542
|
`\u26A0\uFE0F Node9: Upstream MCP server binary is suspect \u2014 ${prov.reason} (${prov.resolvedPath})`
|
|
10797
11543
|
)
|
|
10798
11544
|
);
|
|
10799
|
-
console.error(
|
|
11545
|
+
console.error(chalk16.red(" Verify this binary is trusted before proceeding."));
|
|
10800
11546
|
}
|
|
10801
|
-
console.error(
|
|
11547
|
+
console.error(chalk16.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
|
|
10802
11548
|
const UPSTREAM_INJECTOR_VARS = /* @__PURE__ */ new Set([
|
|
10803
11549
|
"NODE_OPTIONS",
|
|
10804
11550
|
"NODE_PATH",
|
|
@@ -10901,10 +11647,10 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
10901
11647
|
mcpServer
|
|
10902
11648
|
});
|
|
10903
11649
|
if (!result.approved) {
|
|
10904
|
-
console.error(
|
|
11650
|
+
console.error(chalk16.red(`
|
|
10905
11651
|
\u{1F6D1} Node9 MCP Gateway: Action Blocked`));
|
|
10906
|
-
console.error(
|
|
10907
|
-
console.error(
|
|
11652
|
+
console.error(chalk16.gray(` Tool: ${toolName}`));
|
|
11653
|
+
console.error(chalk16.gray(` Reason: ${result.reason ?? "Security Policy"}
|
|
10908
11654
|
`));
|
|
10909
11655
|
const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
|
|
10910
11656
|
const isHumanDecision = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
|
|
@@ -10983,7 +11729,7 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
10983
11729
|
updatePin(serverKey, upstreamCommand, currentHash, toolNames);
|
|
10984
11730
|
pinState = "validated";
|
|
10985
11731
|
console.error(
|
|
10986
|
-
|
|
11732
|
+
chalk16.green(
|
|
10987
11733
|
`\u{1F512} Node9: Pinned ${toolNames.length} tool definition(s) for this MCP server`
|
|
10988
11734
|
)
|
|
10989
11735
|
);
|
|
@@ -10996,11 +11742,11 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
10996
11742
|
} else if (pinStatus === "corrupt") {
|
|
10997
11743
|
pinState = "quarantined";
|
|
10998
11744
|
console.error(
|
|
10999
|
-
|
|
11745
|
+
chalk16.red("\n\u{1F6A8} Node9: MCP pin file is corrupt or unreadable \u2014 session quarantined!")
|
|
11000
11746
|
);
|
|
11001
|
-
console.error(
|
|
11747
|
+
console.error(chalk16.red(" Tool calls are blocked until the pin file is repaired."));
|
|
11002
11748
|
console.error(
|
|
11003
|
-
|
|
11749
|
+
chalk16.yellow(` Run: node9 mcp pin reset (to clear and re-pin on next connect)
|
|
11004
11750
|
`)
|
|
11005
11751
|
);
|
|
11006
11752
|
const errorResponse = {
|
|
@@ -11017,13 +11763,13 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
11017
11763
|
} else {
|
|
11018
11764
|
pinState = "quarantined";
|
|
11019
11765
|
console.error(
|
|
11020
|
-
|
|
11766
|
+
chalk16.red("\n\u{1F6A8} Node9: MCP tool definitions have changed since last verified!")
|
|
11021
11767
|
);
|
|
11022
11768
|
console.error(
|
|
11023
|
-
|
|
11769
|
+
chalk16.red(" This could indicate a supply chain attack (tool poisoning / rug pull).")
|
|
11024
11770
|
);
|
|
11025
|
-
console.error(
|
|
11026
|
-
console.error(
|
|
11771
|
+
console.error(chalk16.red(" Session quarantined \u2014 all tool calls blocked."));
|
|
11772
|
+
console.error(chalk16.yellow(` Run: node9 mcp pin update ${serverKey}
|
|
11027
11773
|
`));
|
|
11028
11774
|
const errorResponse = {
|
|
11029
11775
|
jsonrpc: "2.0",
|
|
@@ -11072,9 +11818,9 @@ function registerMcpGatewayCommand(program2) {
|
|
|
11072
11818
|
|
|
11073
11819
|
// src/mcp-server/index.ts
|
|
11074
11820
|
import readline4 from "readline";
|
|
11075
|
-
import
|
|
11076
|
-
import
|
|
11077
|
-
import
|
|
11821
|
+
import fs28 from "fs";
|
|
11822
|
+
import os24 from "os";
|
|
11823
|
+
import path31 from "path";
|
|
11078
11824
|
init_core();
|
|
11079
11825
|
init_daemon();
|
|
11080
11826
|
init_shields();
|
|
@@ -11249,13 +11995,13 @@ function handleStatus() {
|
|
|
11249
11995
|
lines.push(`Active shields: ${activeShields.length > 0 ? activeShields.join(", ") : "none"}`);
|
|
11250
11996
|
lines.push(`Smart rules: ${config.policy.smartRules.length} loaded`);
|
|
11251
11997
|
lines.push(`DLP: ${config.policy.dlp?.enabled !== false ? "enabled" : "disabled"}`);
|
|
11252
|
-
const projectConfig =
|
|
11253
|
-
const globalConfig =
|
|
11998
|
+
const projectConfig = path31.join(process.cwd(), "node9.config.json");
|
|
11999
|
+
const globalConfig = path31.join(os24.homedir(), ".node9", "config.json");
|
|
11254
12000
|
lines.push(
|
|
11255
|
-
`Project config (node9.config.json): ${
|
|
12001
|
+
`Project config (node9.config.json): ${fs28.existsSync(projectConfig) ? "present" : "not found"}`
|
|
11256
12002
|
);
|
|
11257
12003
|
lines.push(
|
|
11258
|
-
`Global config (~/.node9/config.json): ${
|
|
12004
|
+
`Global config (~/.node9/config.json): ${fs28.existsSync(globalConfig) ? "present" : "not found"}`
|
|
11259
12005
|
);
|
|
11260
12006
|
return lines.join("\n");
|
|
11261
12007
|
}
|
|
@@ -11329,21 +12075,21 @@ function handleShieldDisable(args) {
|
|
|
11329
12075
|
writeActiveShields(active.filter((s) => s !== name));
|
|
11330
12076
|
return `Shield "${name}" disabled.`;
|
|
11331
12077
|
}
|
|
11332
|
-
var GLOBAL_CONFIG_PATH2 =
|
|
12078
|
+
var GLOBAL_CONFIG_PATH2 = path31.join(os24.homedir(), ".node9", "config.json");
|
|
11333
12079
|
var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
|
|
11334
12080
|
function readGlobalConfigRaw() {
|
|
11335
12081
|
try {
|
|
11336
|
-
if (
|
|
11337
|
-
return JSON.parse(
|
|
12082
|
+
if (fs28.existsSync(GLOBAL_CONFIG_PATH2)) {
|
|
12083
|
+
return JSON.parse(fs28.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
|
|
11338
12084
|
}
|
|
11339
12085
|
} catch {
|
|
11340
12086
|
}
|
|
11341
12087
|
return {};
|
|
11342
12088
|
}
|
|
11343
12089
|
function writeGlobalConfigRaw(data) {
|
|
11344
|
-
const dir =
|
|
11345
|
-
if (!
|
|
11346
|
-
|
|
12090
|
+
const dir = path31.dirname(GLOBAL_CONFIG_PATH2);
|
|
12091
|
+
if (!fs28.existsSync(dir)) fs28.mkdirSync(dir, { recursive: true });
|
|
12092
|
+
fs28.writeFileSync(GLOBAL_CONFIG_PATH2, JSON.stringify(data, null, 2) + "\n");
|
|
11347
12093
|
}
|
|
11348
12094
|
function handleApproverList() {
|
|
11349
12095
|
const config = getConfig();
|
|
@@ -11386,9 +12132,9 @@ function handleApproverSet(args) {
|
|
|
11386
12132
|
}
|
|
11387
12133
|
function handleAuditGet(args) {
|
|
11388
12134
|
const limit = Math.min(typeof args.limit === "number" ? args.limit : 20, 100);
|
|
11389
|
-
const auditPath =
|
|
11390
|
-
if (!
|
|
11391
|
-
const lines =
|
|
12135
|
+
const auditPath = path31.join(os24.homedir(), ".node9", "audit.log");
|
|
12136
|
+
if (!fs28.existsSync(auditPath)) return "No audit log found.";
|
|
12137
|
+
const lines = fs28.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
|
|
11392
12138
|
const recent = lines.slice(-limit);
|
|
11393
12139
|
const entries = recent.map((line) => {
|
|
11394
12140
|
try {
|
|
@@ -11576,7 +12322,7 @@ function registerMcpServerCommand(program2) {
|
|
|
11576
12322
|
|
|
11577
12323
|
// src/cli/commands/trust.ts
|
|
11578
12324
|
init_trusted_hosts();
|
|
11579
|
-
import
|
|
12325
|
+
import chalk17 from "chalk";
|
|
11580
12326
|
function isValidHost(host) {
|
|
11581
12327
|
return /^(\*\.)?[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/.test(host);
|
|
11582
12328
|
}
|
|
@@ -11586,51 +12332,51 @@ function registerTrustCommand(program2) {
|
|
|
11586
12332
|
const normalized = normalizeHost(host.trim());
|
|
11587
12333
|
if (!isValidHost(normalized)) {
|
|
11588
12334
|
console.error(
|
|
11589
|
-
|
|
12335
|
+
chalk17.red(`
|
|
11590
12336
|
\u274C Invalid host: "${host}"
|
|
11591
|
-
`) +
|
|
12337
|
+
`) + chalk17.gray(" Use an FQDN like api.mycompany.com or *.mycompany.com\n")
|
|
11592
12338
|
);
|
|
11593
12339
|
process.exit(1);
|
|
11594
12340
|
}
|
|
11595
12341
|
addTrustedHost(normalized);
|
|
11596
|
-
console.log(
|
|
12342
|
+
console.log(chalk17.green(`
|
|
11597
12343
|
\u2705 ${normalized} added to trusted hosts.`));
|
|
11598
12344
|
console.log(
|
|
11599
|
-
|
|
12345
|
+
chalk17.gray(" Pipe-chain blocks to this host: critical \u2192 review, high \u2192 allow\n")
|
|
11600
12346
|
);
|
|
11601
12347
|
});
|
|
11602
12348
|
trustCmd.command("remove <host>").description("Remove a trusted host").action((host) => {
|
|
11603
12349
|
const normalized = normalizeHost(host.trim());
|
|
11604
12350
|
const removed = removeTrustedHost(normalized);
|
|
11605
12351
|
if (!removed) {
|
|
11606
|
-
console.error(
|
|
12352
|
+
console.error(chalk17.yellow(`
|
|
11607
12353
|
\u26A0\uFE0F "${normalized}" is not in the trusted hosts list.
|
|
11608
12354
|
`));
|
|
11609
12355
|
process.exit(1);
|
|
11610
12356
|
}
|
|
11611
|
-
console.log(
|
|
12357
|
+
console.log(chalk17.green(`
|
|
11612
12358
|
\u2705 ${normalized} removed from trusted hosts.
|
|
11613
12359
|
`));
|
|
11614
12360
|
});
|
|
11615
12361
|
trustCmd.command("list").description("Show all trusted hosts").action(() => {
|
|
11616
12362
|
const hosts = readTrustedHosts();
|
|
11617
12363
|
if (hosts.length === 0) {
|
|
11618
|
-
console.log(
|
|
11619
|
-
console.log(` Add one: ${
|
|
12364
|
+
console.log(chalk17.gray("\n No trusted hosts configured.\n"));
|
|
12365
|
+
console.log(` Add one: ${chalk17.cyan("node9 trust add api.mycompany.com")}
|
|
11620
12366
|
`);
|
|
11621
12367
|
return;
|
|
11622
12368
|
}
|
|
11623
|
-
console.log(
|
|
12369
|
+
console.log(chalk17.bold("\n\u{1F513} Trusted Hosts\n"));
|
|
11624
12370
|
for (const entry of hosts) {
|
|
11625
12371
|
const date = new Date(entry.addedAt).toLocaleDateString();
|
|
11626
|
-
console.log(` ${
|
|
12372
|
+
console.log(` ${chalk17.cyan(entry.host.padEnd(40))} ${chalk17.gray(`added ${date}`)}`);
|
|
11627
12373
|
}
|
|
11628
12374
|
console.log("");
|
|
11629
12375
|
});
|
|
11630
12376
|
}
|
|
11631
12377
|
|
|
11632
12378
|
// src/cli/commands/mcp-pin.ts
|
|
11633
|
-
import
|
|
12379
|
+
import chalk18 from "chalk";
|
|
11634
12380
|
function registerMcpPinCommand(program2) {
|
|
11635
12381
|
const pinCmd = program2.command("mcp").description("Manage MCP server tool definition pinning (rug pull defense)");
|
|
11636
12382
|
const pinSubCmd = pinCmd.command("pin").description("Manage pinned MCP server tool definitions");
|
|
@@ -11638,31 +12384,31 @@ function registerMcpPinCommand(program2) {
|
|
|
11638
12384
|
const result = readMcpPinsSafe();
|
|
11639
12385
|
if (!result.ok) {
|
|
11640
12386
|
if (result.reason === "missing") {
|
|
11641
|
-
console.log(
|
|
12387
|
+
console.log(chalk18.gray("\nNo MCP servers are pinned yet."));
|
|
11642
12388
|
console.log(
|
|
11643
|
-
|
|
12389
|
+
chalk18.gray("Pins are created automatically when the MCP gateway first connects.\n")
|
|
11644
12390
|
);
|
|
11645
12391
|
return;
|
|
11646
12392
|
}
|
|
11647
|
-
console.error(
|
|
12393
|
+
console.error(chalk18.red(`
|
|
11648
12394
|
\u274C Pin file is corrupt: ${result.detail}`));
|
|
11649
|
-
console.error(
|
|
12395
|
+
console.error(chalk18.yellow(" Run: node9 mcp pin reset\n"));
|
|
11650
12396
|
process.exit(1);
|
|
11651
12397
|
}
|
|
11652
12398
|
const entries = Object.entries(result.pins.servers);
|
|
11653
12399
|
if (entries.length === 0) {
|
|
11654
|
-
console.log(
|
|
12400
|
+
console.log(chalk18.gray("\nNo MCP servers are pinned yet."));
|
|
11655
12401
|
console.log(
|
|
11656
|
-
|
|
12402
|
+
chalk18.gray("Pins are created automatically when the MCP gateway first connects.\n")
|
|
11657
12403
|
);
|
|
11658
12404
|
return;
|
|
11659
12405
|
}
|
|
11660
|
-
console.log(
|
|
12406
|
+
console.log(chalk18.bold("\n\u{1F512} Pinned MCP Servers\n"));
|
|
11661
12407
|
for (const [key, entry] of entries) {
|
|
11662
|
-
console.log(` ${
|
|
11663
|
-
console.log(` Tools (${entry.toolCount}): ${
|
|
11664
|
-
console.log(` Hash: ${
|
|
11665
|
-
console.log(` Pinned: ${
|
|
12408
|
+
console.log(` ${chalk18.cyan(key)} ${chalk18.gray(entry.label)}`);
|
|
12409
|
+
console.log(` Tools (${entry.toolCount}): ${chalk18.white(entry.toolNames.join(", "))}`);
|
|
12410
|
+
console.log(` Hash: ${chalk18.gray(entry.toolsHash.slice(0, 16))}...`);
|
|
12411
|
+
console.log(` Pinned: ${chalk18.gray(entry.pinnedAt)}`);
|
|
11666
12412
|
console.log("");
|
|
11667
12413
|
}
|
|
11668
12414
|
});
|
|
@@ -11673,55 +12419,55 @@ function registerMcpPinCommand(program2) {
|
|
|
11673
12419
|
try {
|
|
11674
12420
|
pins = readMcpPins();
|
|
11675
12421
|
} catch {
|
|
11676
|
-
console.error(
|
|
11677
|
-
console.error(
|
|
12422
|
+
console.error(chalk18.red("\n\u274C Pin file is corrupt."));
|
|
12423
|
+
console.error(chalk18.yellow(" Run: node9 mcp pin reset\n"));
|
|
11678
12424
|
process.exit(1);
|
|
11679
12425
|
}
|
|
11680
12426
|
if (!pins.servers[serverKey]) {
|
|
11681
|
-
console.error(
|
|
12427
|
+
console.error(chalk18.red(`
|
|
11682
12428
|
\u274C No pin found for server key "${serverKey}"
|
|
11683
12429
|
`));
|
|
11684
|
-
console.error(`Run ${
|
|
12430
|
+
console.error(`Run ${chalk18.cyan("node9 mcp pin list")} to see pinned servers.
|
|
11685
12431
|
`);
|
|
11686
12432
|
process.exit(1);
|
|
11687
12433
|
}
|
|
11688
12434
|
const label = pins.servers[serverKey].label;
|
|
11689
12435
|
removePin(serverKey);
|
|
11690
|
-
console.log(
|
|
11691
|
-
\u{1F513} Pin removed for ${
|
|
11692
|
-
console.log(
|
|
11693
|
-
console.log(
|
|
12436
|
+
console.log(chalk18.green(`
|
|
12437
|
+
\u{1F513} Pin removed for ${chalk18.cyan(serverKey)}`));
|
|
12438
|
+
console.log(chalk18.gray(` Server: ${label}`));
|
|
12439
|
+
console.log(chalk18.gray(" Next connection will re-pin with current tool definitions.\n"));
|
|
11694
12440
|
});
|
|
11695
12441
|
pinSubCmd.command("reset").description("Clear all MCP pins (next connection to each server will re-pin)").action(() => {
|
|
11696
12442
|
const result = readMcpPinsSafe();
|
|
11697
12443
|
if (!result.ok && result.reason === "missing") {
|
|
11698
|
-
console.log(
|
|
12444
|
+
console.log(chalk18.gray("\nNo pins to clear.\n"));
|
|
11699
12445
|
return;
|
|
11700
12446
|
}
|
|
11701
12447
|
const count = result.ok ? Object.keys(result.pins.servers).length : "?";
|
|
11702
12448
|
clearAllPins();
|
|
11703
|
-
console.log(
|
|
12449
|
+
console.log(chalk18.green(`
|
|
11704
12450
|
\u{1F513} Cleared ${count} MCP pin(s).`));
|
|
11705
|
-
console.log(
|
|
12451
|
+
console.log(chalk18.gray(" Next connection to each server will re-pin.\n"));
|
|
11706
12452
|
});
|
|
11707
12453
|
}
|
|
11708
12454
|
|
|
11709
12455
|
// src/cli.ts
|
|
11710
12456
|
var { version } = JSON.parse(
|
|
11711
|
-
|
|
12457
|
+
fs31.readFileSync(path34.join(__dirname, "../package.json"), "utf-8")
|
|
11712
12458
|
);
|
|
11713
12459
|
var program = new Command();
|
|
11714
12460
|
program.name("node9").description("The Sudo Command for AI Agents").version(version);
|
|
11715
12461
|
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) => {
|
|
11716
12462
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
11717
|
-
const credPath =
|
|
11718
|
-
if (!
|
|
11719
|
-
|
|
12463
|
+
const credPath = path34.join(os27.homedir(), ".node9", "credentials.json");
|
|
12464
|
+
if (!fs31.existsSync(path34.dirname(credPath)))
|
|
12465
|
+
fs31.mkdirSync(path34.dirname(credPath), { recursive: true });
|
|
11720
12466
|
const profileName = options.profile || "default";
|
|
11721
12467
|
let existingCreds = {};
|
|
11722
12468
|
try {
|
|
11723
|
-
if (
|
|
11724
|
-
const raw = JSON.parse(
|
|
12469
|
+
if (fs31.existsSync(credPath)) {
|
|
12470
|
+
const raw = JSON.parse(fs31.readFileSync(credPath, "utf-8"));
|
|
11725
12471
|
if (raw.apiKey) {
|
|
11726
12472
|
existingCreds = {
|
|
11727
12473
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
|
|
@@ -11733,13 +12479,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
11733
12479
|
} catch {
|
|
11734
12480
|
}
|
|
11735
12481
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
11736
|
-
|
|
12482
|
+
fs31.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
11737
12483
|
if (profileName === "default") {
|
|
11738
|
-
const configPath =
|
|
12484
|
+
const configPath = path34.join(os27.homedir(), ".node9", "config.json");
|
|
11739
12485
|
let config = {};
|
|
11740
12486
|
try {
|
|
11741
|
-
if (
|
|
11742
|
-
config = JSON.parse(
|
|
12487
|
+
if (fs31.existsSync(configPath))
|
|
12488
|
+
config = JSON.parse(fs31.readFileSync(configPath, "utf-8"));
|
|
11743
12489
|
} catch {
|
|
11744
12490
|
}
|
|
11745
12491
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -11754,19 +12500,19 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
11754
12500
|
approvers.cloud = false;
|
|
11755
12501
|
}
|
|
11756
12502
|
s.approvers = approvers;
|
|
11757
|
-
if (!
|
|
11758
|
-
|
|
11759
|
-
|
|
12503
|
+
if (!fs31.existsSync(path34.dirname(configPath)))
|
|
12504
|
+
fs31.mkdirSync(path34.dirname(configPath), { recursive: true });
|
|
12505
|
+
fs31.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
11760
12506
|
}
|
|
11761
12507
|
if (options.profile && profileName !== "default") {
|
|
11762
|
-
console.log(
|
|
11763
|
-
console.log(
|
|
12508
|
+
console.log(chalk20.green(`\u2705 Profile "${profileName}" saved`));
|
|
12509
|
+
console.log(chalk20.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
11764
12510
|
} else if (options.local) {
|
|
11765
|
-
console.log(
|
|
11766
|
-
console.log(
|
|
12511
|
+
console.log(chalk20.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
12512
|
+
console.log(chalk20.gray(` All decisions stay on this machine.`));
|
|
11767
12513
|
} else {
|
|
11768
|
-
console.log(
|
|
11769
|
-
console.log(
|
|
12514
|
+
console.log(chalk20.green(`\u2705 Logged in \u2014 agent mode`));
|
|
12515
|
+
console.log(chalk20.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
11770
12516
|
}
|
|
11771
12517
|
});
|
|
11772
12518
|
program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor hud").argument("<target>", "The agent to protect: claude | gemini | cursor | hud").action(async (target) => {
|
|
@@ -11774,19 +12520,19 @@ program.command("addto").description("Integrate Node9 with an AI agent").addHelp
|
|
|
11774
12520
|
if (target === "claude") return await setupClaude();
|
|
11775
12521
|
if (target === "cursor") return await setupCursor();
|
|
11776
12522
|
if (target === "hud") return setupHud();
|
|
11777
|
-
console.error(
|
|
12523
|
+
console.error(chalk20.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
|
|
11778
12524
|
process.exit(1);
|
|
11779
12525
|
});
|
|
11780
12526
|
program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor hud").argument("[target]", "The agent to protect: claude | gemini | cursor | hud").action(async (target) => {
|
|
11781
12527
|
if (!target) {
|
|
11782
|
-
console.log(
|
|
11783
|
-
console.log(" Usage: " +
|
|
12528
|
+
console.log(chalk20.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
12529
|
+
console.log(" Usage: " + chalk20.white("node9 setup <target>") + "\n");
|
|
11784
12530
|
console.log(" Targets:");
|
|
11785
|
-
console.log(" " +
|
|
11786
|
-
console.log(" " +
|
|
11787
|
-
console.log(" " +
|
|
12531
|
+
console.log(" " + chalk20.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
12532
|
+
console.log(" " + chalk20.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
12533
|
+
console.log(" " + chalk20.green("cursor") + " \u2014 Cursor (hook mode)");
|
|
11788
12534
|
process.stdout.write(
|
|
11789
|
-
" " +
|
|
12535
|
+
" " + chalk20.green("hud") + " \u2014 Claude Code security statusline\n"
|
|
11790
12536
|
);
|
|
11791
12537
|
console.log("");
|
|
11792
12538
|
return;
|
|
@@ -11796,7 +12542,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
11796
12542
|
if (t === "claude") return await setupClaude();
|
|
11797
12543
|
if (t === "cursor") return await setupCursor();
|
|
11798
12544
|
if (t === "hud") return setupHud();
|
|
11799
|
-
console.error(
|
|
12545
|
+
console.error(chalk20.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
|
|
11800
12546
|
process.exit(1);
|
|
11801
12547
|
});
|
|
11802
12548
|
program.command("removefrom").description("Remove Node9 hooks from an AI agent configuration").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to remove from: claude | gemini | cursor").action((target) => {
|
|
@@ -11807,31 +12553,31 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
|
|
|
11807
12553
|
else if (target === "hud") fn = teardownHud;
|
|
11808
12554
|
else {
|
|
11809
12555
|
console.error(
|
|
11810
|
-
|
|
12556
|
+
chalk20.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`)
|
|
11811
12557
|
);
|
|
11812
12558
|
process.exit(1);
|
|
11813
12559
|
}
|
|
11814
|
-
console.log(
|
|
12560
|
+
console.log(chalk20.cyan(`
|
|
11815
12561
|
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
11816
12562
|
`));
|
|
11817
12563
|
try {
|
|
11818
12564
|
fn();
|
|
11819
12565
|
} catch (err2) {
|
|
11820
|
-
console.error(
|
|
12566
|
+
console.error(chalk20.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
11821
12567
|
process.exit(1);
|
|
11822
12568
|
}
|
|
11823
|
-
console.log(
|
|
12569
|
+
console.log(chalk20.gray("\n Restart the agent for changes to take effect."));
|
|
11824
12570
|
});
|
|
11825
12571
|
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) => {
|
|
11826
|
-
console.log(
|
|
11827
|
-
console.log(
|
|
12572
|
+
console.log(chalk20.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
12573
|
+
console.log(chalk20.bold("Stopping daemon..."));
|
|
11828
12574
|
try {
|
|
11829
12575
|
stopDaemon();
|
|
11830
|
-
console.log(
|
|
12576
|
+
console.log(chalk20.green(" \u2705 Daemon stopped"));
|
|
11831
12577
|
} catch {
|
|
11832
|
-
console.log(
|
|
12578
|
+
console.log(chalk20.blue(" \u2139\uFE0F Daemon was not running"));
|
|
11833
12579
|
}
|
|
11834
|
-
console.log(
|
|
12580
|
+
console.log(chalk20.bold("\nRemoving hooks..."));
|
|
11835
12581
|
let teardownFailed = false;
|
|
11836
12582
|
for (const [label, fn] of [
|
|
11837
12583
|
["Claude", teardownClaude],
|
|
@@ -11843,45 +12589,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
11843
12589
|
} catch (err2) {
|
|
11844
12590
|
teardownFailed = true;
|
|
11845
12591
|
console.error(
|
|
11846
|
-
|
|
12592
|
+
chalk20.red(
|
|
11847
12593
|
` \u26A0\uFE0F Failed to remove ${label} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
11848
12594
|
)
|
|
11849
12595
|
);
|
|
11850
12596
|
}
|
|
11851
12597
|
}
|
|
11852
12598
|
if (options.purge) {
|
|
11853
|
-
const node9Dir =
|
|
11854
|
-
if (
|
|
12599
|
+
const node9Dir = path34.join(os27.homedir(), ".node9");
|
|
12600
|
+
if (fs31.existsSync(node9Dir)) {
|
|
11855
12601
|
const confirmed = await confirm2({
|
|
11856
12602
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
11857
12603
|
default: false
|
|
11858
12604
|
});
|
|
11859
12605
|
if (confirmed) {
|
|
11860
|
-
|
|
11861
|
-
if (
|
|
12606
|
+
fs31.rmSync(node9Dir, { recursive: true });
|
|
12607
|
+
if (fs31.existsSync(node9Dir)) {
|
|
11862
12608
|
console.error(
|
|
11863
|
-
|
|
12609
|
+
chalk20.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
11864
12610
|
);
|
|
11865
12611
|
} else {
|
|
11866
|
-
console.log(
|
|
12612
|
+
console.log(chalk20.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
11867
12613
|
}
|
|
11868
12614
|
} else {
|
|
11869
|
-
console.log(
|
|
12615
|
+
console.log(chalk20.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
11870
12616
|
}
|
|
11871
12617
|
} else {
|
|
11872
|
-
console.log(
|
|
12618
|
+
console.log(chalk20.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
11873
12619
|
}
|
|
11874
12620
|
} else {
|
|
11875
12621
|
console.log(
|
|
11876
|
-
|
|
12622
|
+
chalk20.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
11877
12623
|
);
|
|
11878
12624
|
}
|
|
11879
12625
|
if (teardownFailed) {
|
|
11880
|
-
console.error(
|
|
12626
|
+
console.error(chalk20.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
11881
12627
|
process.exit(1);
|
|
11882
12628
|
}
|
|
11883
|
-
console.log(
|
|
11884
|
-
console.log(
|
|
12629
|
+
console.log(chalk20.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
|
|
12630
|
+
console.log(chalk20.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
|
|
11885
12631
|
});
|
|
11886
12632
|
registerDoctorCommand(program, version);
|
|
11887
12633
|
program.command("explain").description(
|
|
@@ -11894,7 +12640,7 @@ program.command("explain").description(
|
|
|
11894
12640
|
try {
|
|
11895
12641
|
args = JSON.parse(trimmed);
|
|
11896
12642
|
} catch {
|
|
11897
|
-
console.error(
|
|
12643
|
+
console.error(chalk20.red(`
|
|
11898
12644
|
\u274C Invalid JSON: ${trimmed}
|
|
11899
12645
|
`));
|
|
11900
12646
|
process.exit(1);
|
|
@@ -11905,60 +12651,61 @@ program.command("explain").description(
|
|
|
11905
12651
|
}
|
|
11906
12652
|
const result = await explainPolicy(tool, args);
|
|
11907
12653
|
console.log("");
|
|
11908
|
-
console.log(
|
|
12654
|
+
console.log(chalk20.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
11909
12655
|
console.log("");
|
|
11910
|
-
console.log(` ${
|
|
12656
|
+
console.log(` ${chalk20.bold("Tool:")} ${chalk20.white(result.tool)}`);
|
|
11911
12657
|
if (argsRaw) {
|
|
11912
12658
|
const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
11913
|
-
console.log(` ${
|
|
12659
|
+
console.log(` ${chalk20.bold("Input:")} ${chalk20.gray(preview)}`);
|
|
11914
12660
|
}
|
|
11915
12661
|
console.log("");
|
|
11916
|
-
console.log(
|
|
12662
|
+
console.log(chalk20.bold("Config Sources (Waterfall):"));
|
|
11917
12663
|
for (const tier of result.waterfall) {
|
|
11918
|
-
const
|
|
12664
|
+
const num2 = chalk20.gray(` ${tier.tier}.`);
|
|
11919
12665
|
const label = tier.label.padEnd(16);
|
|
11920
12666
|
let statusStr;
|
|
11921
12667
|
if (tier.tier === 1) {
|
|
11922
|
-
statusStr =
|
|
12668
|
+
statusStr = chalk20.gray(tier.note ?? "");
|
|
11923
12669
|
} else if (tier.status === "active") {
|
|
11924
|
-
const loc = tier.path ?
|
|
11925
|
-
const note = tier.note ?
|
|
11926
|
-
statusStr =
|
|
12670
|
+
const loc = tier.path ? chalk20.gray(tier.path) : "";
|
|
12671
|
+
const note = tier.note ? chalk20.gray(`(${tier.note})`) : "";
|
|
12672
|
+
statusStr = chalk20.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
|
|
11927
12673
|
} else {
|
|
11928
|
-
statusStr =
|
|
12674
|
+
statusStr = chalk20.gray("\u25CB " + (tier.note ?? "not found"));
|
|
11929
12675
|
}
|
|
11930
|
-
console.log(`${
|
|
12676
|
+
console.log(`${num2} ${chalk20.white(label)} ${statusStr}`);
|
|
11931
12677
|
}
|
|
11932
12678
|
console.log("");
|
|
11933
|
-
console.log(
|
|
12679
|
+
console.log(chalk20.bold("Policy Evaluation:"));
|
|
11934
12680
|
for (const step of result.steps) {
|
|
11935
12681
|
const isFinal = step.isFinal;
|
|
11936
12682
|
let icon;
|
|
11937
|
-
if (step.outcome === "allow") icon =
|
|
11938
|
-
else if (step.outcome === "review") icon =
|
|
11939
|
-
else if (step.outcome === "skip") icon =
|
|
11940
|
-
else icon =
|
|
12683
|
+
if (step.outcome === "allow") icon = chalk20.green(" \u2705");
|
|
12684
|
+
else if (step.outcome === "review") icon = chalk20.red(" \u{1F534}");
|
|
12685
|
+
else if (step.outcome === "skip") icon = chalk20.gray(" \u2500 ");
|
|
12686
|
+
else icon = chalk20.gray(" \u25CB ");
|
|
11941
12687
|
const name = step.name.padEnd(18);
|
|
11942
|
-
const nameStr = isFinal ?
|
|
11943
|
-
const detail = isFinal ?
|
|
11944
|
-
const arrow = isFinal ?
|
|
12688
|
+
const nameStr = isFinal ? chalk20.white.bold(name) : chalk20.white(name);
|
|
12689
|
+
const detail = isFinal ? chalk20.white(step.detail) : chalk20.gray(step.detail);
|
|
12690
|
+
const arrow = isFinal ? chalk20.yellow(" \u2190 STOP") : "";
|
|
11945
12691
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
11946
12692
|
}
|
|
11947
12693
|
console.log("");
|
|
11948
12694
|
if (result.decision === "allow") {
|
|
11949
|
-
console.log(
|
|
12695
|
+
console.log(chalk20.green.bold(" Decision: \u2705 ALLOW") + chalk20.gray(" \u2014 no approval needed"));
|
|
11950
12696
|
} else {
|
|
11951
12697
|
console.log(
|
|
11952
|
-
|
|
12698
|
+
chalk20.red.bold(" Decision: \u{1F534} REVIEW") + chalk20.gray(" \u2014 human approval required")
|
|
11953
12699
|
);
|
|
11954
12700
|
if (result.blockedByLabel) {
|
|
11955
|
-
console.log(
|
|
12701
|
+
console.log(chalk20.gray(` Reason: ${result.blockedByLabel}`));
|
|
11956
12702
|
}
|
|
11957
12703
|
}
|
|
11958
12704
|
console.log("");
|
|
11959
12705
|
});
|
|
11960
12706
|
registerInitCommand(program);
|
|
11961
12707
|
registerAuditCommand(program);
|
|
12708
|
+
registerReportCommand(program);
|
|
11962
12709
|
registerStatusCommand(program);
|
|
11963
12710
|
registerDaemonCommand(program);
|
|
11964
12711
|
program.command("tail").description("Stream live agent activity to the terminal").option("--history", "Replay recent history then continue live", false).option("--clear", "Clear the history buffer and exit (does not stream)", false).action(async (options) => {
|
|
@@ -11966,7 +12713,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
|
|
|
11966
12713
|
try {
|
|
11967
12714
|
await startTail2(options);
|
|
11968
12715
|
} catch (err2) {
|
|
11969
|
-
console.error(
|
|
12716
|
+
console.error(chalk20.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
11970
12717
|
process.exit(1);
|
|
11971
12718
|
}
|
|
11972
12719
|
});
|
|
@@ -11998,14 +12745,14 @@ Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
|
|
|
11998
12745
|
Run "node9 addto claude" to register it as the statusLine.`
|
|
11999
12746
|
).argument("[subcommand]", 'Optional: "debug on" / "debug off" to toggle stdin logging').argument("[state]", 'on|off \u2014 used with "debug" subcommand').action(async (subcommand, state) => {
|
|
12000
12747
|
if (subcommand === "debug") {
|
|
12001
|
-
const flagFile =
|
|
12748
|
+
const flagFile = path34.join(os27.homedir(), ".node9", "hud-debug");
|
|
12002
12749
|
if (state === "on") {
|
|
12003
|
-
|
|
12004
|
-
|
|
12750
|
+
fs31.mkdirSync(path34.dirname(flagFile), { recursive: true });
|
|
12751
|
+
fs31.writeFileSync(flagFile, "");
|
|
12005
12752
|
console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
|
|
12006
12753
|
console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
|
|
12007
12754
|
} else if (state === "off") {
|
|
12008
|
-
if (
|
|
12755
|
+
if (fs31.existsSync(flagFile)) fs31.unlinkSync(flagFile);
|
|
12009
12756
|
console.log("HUD debug logging disabled.");
|
|
12010
12757
|
} else {
|
|
12011
12758
|
console.error("Usage: node9 hud debug on|off");
|
|
@@ -12020,7 +12767,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
12020
12767
|
const ms = parseDuration(options.duration);
|
|
12021
12768
|
if (ms === null) {
|
|
12022
12769
|
console.error(
|
|
12023
|
-
|
|
12770
|
+
chalk20.red(`
|
|
12024
12771
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
12025
12772
|
`)
|
|
12026
12773
|
);
|
|
@@ -12028,20 +12775,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
12028
12775
|
}
|
|
12029
12776
|
pauseNode9(ms, options.duration);
|
|
12030
12777
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
12031
|
-
console.log(
|
|
12778
|
+
console.log(chalk20.yellow(`
|
|
12032
12779
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
12033
|
-
console.log(
|
|
12034
|
-
console.log(
|
|
12780
|
+
console.log(chalk20.gray(` All tool calls will be allowed without review.`));
|
|
12781
|
+
console.log(chalk20.gray(` Run "node9 resume" to re-enable early.
|
|
12035
12782
|
`));
|
|
12036
12783
|
});
|
|
12037
12784
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
12038
12785
|
const { paused } = checkPause();
|
|
12039
12786
|
if (!paused) {
|
|
12040
|
-
console.log(
|
|
12787
|
+
console.log(chalk20.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
12041
12788
|
return;
|
|
12042
12789
|
}
|
|
12043
12790
|
resumeNode9();
|
|
12044
|
-
console.log(
|
|
12791
|
+
console.log(chalk20.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
12045
12792
|
});
|
|
12046
12793
|
var HOOK_BASED_AGENTS = {
|
|
12047
12794
|
claude: "claude",
|
|
@@ -12054,15 +12801,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
12054
12801
|
if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
|
|
12055
12802
|
const target = HOOK_BASED_AGENTS[firstArg2];
|
|
12056
12803
|
console.error(
|
|
12057
|
-
|
|
12804
|
+
chalk20.yellow(`
|
|
12058
12805
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
12059
12806
|
);
|
|
12060
|
-
console.error(
|
|
12807
|
+
console.error(chalk20.white(`
|
|
12061
12808
|
"${target}" uses its own hook system. Use:`));
|
|
12062
12809
|
console.error(
|
|
12063
|
-
|
|
12810
|
+
chalk20.green(` node9 addto ${target} `) + chalk20.gray("# one-time setup")
|
|
12064
12811
|
);
|
|
12065
|
-
console.error(
|
|
12812
|
+
console.error(chalk20.green(` ${target} `) + chalk20.gray("# run normally"));
|
|
12066
12813
|
process.exit(1);
|
|
12067
12814
|
}
|
|
12068
12815
|
const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
|
|
@@ -12079,7 +12826,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
12079
12826
|
}
|
|
12080
12827
|
);
|
|
12081
12828
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
12082
|
-
console.error(
|
|
12829
|
+
console.error(chalk20.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
12083
12830
|
const daemonReady = await autoStartDaemonAndWait();
|
|
12084
12831
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
12085
12832
|
}
|
|
@@ -12092,12 +12839,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
12092
12839
|
}
|
|
12093
12840
|
if (!result.approved) {
|
|
12094
12841
|
console.error(
|
|
12095
|
-
|
|
12842
|
+
chalk20.red(`
|
|
12096
12843
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
12097
12844
|
);
|
|
12098
12845
|
process.exit(1);
|
|
12099
12846
|
}
|
|
12100
|
-
console.error(
|
|
12847
|
+
console.error(chalk20.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
12101
12848
|
await runProxy(fullCommand);
|
|
12102
12849
|
} else {
|
|
12103
12850
|
program.help();
|
|
@@ -12112,9 +12859,9 @@ if (process.argv[2] !== "daemon") {
|
|
|
12112
12859
|
const isCheckHook = process.argv[2] === "check";
|
|
12113
12860
|
if (isCheckHook) {
|
|
12114
12861
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
12115
|
-
const logPath =
|
|
12862
|
+
const logPath = path34.join(os27.homedir(), ".node9", "hook-debug.log");
|
|
12116
12863
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
12117
|
-
|
|
12864
|
+
fs31.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
12118
12865
|
`);
|
|
12119
12866
|
}
|
|
12120
12867
|
process.exit(0);
|