@node9/proxy 1.9.3 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -3
- package/dist/cli.js +1123 -610
- package/dist/cli.mjs +1105 -592
- package/dist/index.js +97 -10
- package/dist/index.mjs +95 -8
- 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 path34 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
151
|
+
return ` \u2022 ${path34}: ${issue.message}`;
|
|
144
152
|
});
|
|
145
153
|
return {
|
|
146
154
|
sanitized,
|
|
@@ -239,6 +247,11 @@ var init_config_schema = __esm({
|
|
|
239
247
|
dlp: z.object({
|
|
240
248
|
enabled: z.boolean().optional(),
|
|
241
249
|
scanIgnoredTools: z.boolean().optional()
|
|
250
|
+
}).optional(),
|
|
251
|
+
loopDetection: z.object({
|
|
252
|
+
enabled: z.boolean().optional(),
|
|
253
|
+
threshold: z.number().min(2).optional(),
|
|
254
|
+
windowSeconds: z.number().min(10).optional()
|
|
242
255
|
}).optional()
|
|
243
256
|
}).optional(),
|
|
244
257
|
environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
@@ -536,7 +549,8 @@ function getConfig(cwd) {
|
|
|
536
549
|
onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
|
|
537
550
|
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
538
551
|
},
|
|
539
|
-
dlp: { ...DEFAULT_CONFIG.policy.dlp }
|
|
552
|
+
dlp: { ...DEFAULT_CONFIG.policy.dlp },
|
|
553
|
+
loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection }
|
|
540
554
|
};
|
|
541
555
|
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
542
556
|
const applyLayer = (source) => {
|
|
@@ -575,6 +589,13 @@ function getConfig(cwd) {
|
|
|
575
589
|
if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
|
|
576
590
|
if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
|
|
577
591
|
}
|
|
592
|
+
if (p.loopDetection) {
|
|
593
|
+
const ld = p.loopDetection;
|
|
594
|
+
if (ld.enabled !== void 0) mergedPolicy.loopDetection.enabled = ld.enabled;
|
|
595
|
+
if (ld.threshold !== void 0) mergedPolicy.loopDetection.threshold = ld.threshold;
|
|
596
|
+
if (ld.windowSeconds !== void 0)
|
|
597
|
+
mergedPolicy.loopDetection.windowSeconds = ld.windowSeconds;
|
|
598
|
+
}
|
|
578
599
|
const envs = source.environments || {};
|
|
579
600
|
for (const [envName, envConfig] of Object.entries(envs)) {
|
|
580
601
|
if (envConfig && typeof envConfig === "object") {
|
|
@@ -878,7 +899,8 @@ var init_config = __esm({
|
|
|
878
899
|
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
900
|
}
|
|
880
901
|
],
|
|
881
|
-
dlp: { enabled: true, scanIgnoredTools: true }
|
|
902
|
+
dlp: { enabled: true, scanIgnoredTools: true },
|
|
903
|
+
loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 }
|
|
882
904
|
},
|
|
883
905
|
environments: {}
|
|
884
906
|
};
|
|
@@ -1687,9 +1709,9 @@ function matchesPattern(text, patterns) {
|
|
|
1687
1709
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1688
1710
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1689
1711
|
}
|
|
1690
|
-
function getNestedValue(obj,
|
|
1712
|
+
function getNestedValue(obj, path34) {
|
|
1691
1713
|
if (!obj || typeof obj !== "object") return null;
|
|
1692
|
-
return
|
|
1714
|
+
return path34.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1693
1715
|
}
|
|
1694
1716
|
function shouldSnapshot(toolName, args, config) {
|
|
1695
1717
|
if (!config.settings.enableUndo) return false;
|
|
@@ -3056,6 +3078,58 @@ var init_cloud = __esm({
|
|
|
3056
3078
|
}
|
|
3057
3079
|
});
|
|
3058
3080
|
|
|
3081
|
+
// src/loop-detector.ts
|
|
3082
|
+
import fs11 from "fs";
|
|
3083
|
+
import path14 from "path";
|
|
3084
|
+
import os10 from "os";
|
|
3085
|
+
import crypto2 from "crypto";
|
|
3086
|
+
function loopStateFile() {
|
|
3087
|
+
return path14.join(os10.homedir(), ".node9", "loop-state.json");
|
|
3088
|
+
}
|
|
3089
|
+
function computeArgsHash(args) {
|
|
3090
|
+
const str = JSON.stringify(args ?? "");
|
|
3091
|
+
return crypto2.createHash("sha256").update(str).digest("hex").slice(0, 16);
|
|
3092
|
+
}
|
|
3093
|
+
function readState() {
|
|
3094
|
+
try {
|
|
3095
|
+
if (!fs11.existsSync(loopStateFile())) return [];
|
|
3096
|
+
const raw = fs11.readFileSync(loopStateFile(), "utf-8");
|
|
3097
|
+
const parsed = JSON.parse(raw);
|
|
3098
|
+
if (!Array.isArray(parsed)) return [];
|
|
3099
|
+
return parsed;
|
|
3100
|
+
} catch {
|
|
3101
|
+
return [];
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
function writeState(records) {
|
|
3105
|
+
const dir = path14.dirname(loopStateFile());
|
|
3106
|
+
if (!fs11.existsSync(dir)) fs11.mkdirSync(dir, { recursive: true });
|
|
3107
|
+
const tmpPath = `${loopStateFile()}.${os10.hostname()}.${process.pid}.tmp`;
|
|
3108
|
+
fs11.writeFileSync(tmpPath, JSON.stringify(records));
|
|
3109
|
+
fs11.renameSync(tmpPath, loopStateFile());
|
|
3110
|
+
}
|
|
3111
|
+
function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
3112
|
+
try {
|
|
3113
|
+
const hash = computeArgsHash(args);
|
|
3114
|
+
const now = Date.now();
|
|
3115
|
+
const cutoff = now - windowMs;
|
|
3116
|
+
const records = readState().filter((r) => r.ts >= cutoff);
|
|
3117
|
+
records.push({ t: tool, h: hash, ts: now });
|
|
3118
|
+
const count = records.filter((r) => r.t === tool && r.h === hash).length;
|
|
3119
|
+
writeState(records.slice(-MAX_RECORDS));
|
|
3120
|
+
return { looping: count >= threshold, count };
|
|
3121
|
+
} catch {
|
|
3122
|
+
return { looping: false, count: 0 };
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
var MAX_RECORDS;
|
|
3126
|
+
var init_loop_detector = __esm({
|
|
3127
|
+
"src/loop-detector.ts"() {
|
|
3128
|
+
"use strict";
|
|
3129
|
+
MAX_RECORDS = 500;
|
|
3130
|
+
}
|
|
3131
|
+
});
|
|
3132
|
+
|
|
3059
3133
|
// src/auth/orchestrator.ts
|
|
3060
3134
|
import { randomUUID } from "crypto";
|
|
3061
3135
|
function isWriteTool(toolName) {
|
|
@@ -3144,6 +3218,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3144
3218
|
let explainableLabel = "Local Config";
|
|
3145
3219
|
let policyMatchedField;
|
|
3146
3220
|
let policyMatchedWord;
|
|
3221
|
+
let policyRuleDescription;
|
|
3147
3222
|
let riskMetadata;
|
|
3148
3223
|
let statefulRecoveryCommand;
|
|
3149
3224
|
let localSmartRuleMatched = false;
|
|
@@ -3237,6 +3312,21 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3237
3312
|
return { approved: true, checkedBy: "audit" };
|
|
3238
3313
|
}
|
|
3239
3314
|
if (!taintWarning && !isIgnoredTool(toolName)) {
|
|
3315
|
+
const ld = config.policy.loopDetection;
|
|
3316
|
+
if (ld.enabled) {
|
|
3317
|
+
const loopResult = recordAndCheck(toolName, args, ld.threshold, ld.windowSeconds * 1e3);
|
|
3318
|
+
if (loopResult.looping) {
|
|
3319
|
+
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?`;
|
|
3320
|
+
if (!isManual)
|
|
3321
|
+
appendLocalAudit(toolName, args, "deny", "loop-detected", meta, hashAuditArgs);
|
|
3322
|
+
return {
|
|
3323
|
+
approved: false,
|
|
3324
|
+
reason,
|
|
3325
|
+
blockedBy: "loop-detection",
|
|
3326
|
+
blockedByLabel: "\u{1F504} Loop Detected"
|
|
3327
|
+
};
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3240
3330
|
if (getActiveTrustSession(toolName)) {
|
|
3241
3331
|
if (approvers.cloud && creds?.apiKey)
|
|
3242
3332
|
await auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
@@ -3292,6 +3382,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3292
3382
|
policyMatchedField = policyResult.matchedField;
|
|
3293
3383
|
policyMatchedWord = policyResult.matchedWord;
|
|
3294
3384
|
if (policyResult.ruleName) localSmartRuleMatched = true;
|
|
3385
|
+
if (policyResult.ruleDescription) policyRuleDescription = policyResult.ruleDescription;
|
|
3386
|
+
else if (policyResult.reason) policyRuleDescription = policyResult.reason;
|
|
3295
3387
|
riskMetadata = computeRiskMetadata(
|
|
3296
3388
|
args,
|
|
3297
3389
|
policyResult.tier ?? 6,
|
|
@@ -3555,7 +3647,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
3555
3647
|
hashAuditArgs
|
|
3556
3648
|
);
|
|
3557
3649
|
}
|
|
3558
|
-
|
|
3650
|
+
const enrichedResult = !finalResult.approved && policyRuleDescription && !finalResult.ruleDescription ? { ...finalResult, ruleDescription: policyRuleDescription } : finalResult;
|
|
3651
|
+
return enrichedResult;
|
|
3559
3652
|
}
|
|
3560
3653
|
var WRITE_TOOLS;
|
|
3561
3654
|
var init_orchestrator = __esm({
|
|
@@ -3570,6 +3663,7 @@ var init_orchestrator = __esm({
|
|
|
3570
3663
|
init_state();
|
|
3571
3664
|
init_daemon();
|
|
3572
3665
|
init_cloud();
|
|
3666
|
+
init_loop_detector();
|
|
3573
3667
|
WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
3574
3668
|
"write",
|
|
3575
3669
|
"write_file",
|
|
@@ -5307,8 +5401,8 @@ var init_suggestion_tracker = __esm({
|
|
|
5307
5401
|
});
|
|
5308
5402
|
|
|
5309
5403
|
// src/daemon/taint-store.ts
|
|
5310
|
-
import
|
|
5311
|
-
import
|
|
5404
|
+
import fs13 from "fs";
|
|
5405
|
+
import path16 from "path";
|
|
5312
5406
|
var DEFAULT_TTL_MS, TaintStore;
|
|
5313
5407
|
var init_taint_store = __esm({
|
|
5314
5408
|
"src/daemon/taint-store.ts"() {
|
|
@@ -5377,9 +5471,9 @@ var init_taint_store = __esm({
|
|
|
5377
5471
|
/** Resolve to absolute path, falling back to path.resolve if file doesn't exist yet. */
|
|
5378
5472
|
_resolve(filePath) {
|
|
5379
5473
|
try {
|
|
5380
|
-
return
|
|
5474
|
+
return fs13.realpathSync.native(path16.resolve(filePath));
|
|
5381
5475
|
} catch {
|
|
5382
|
-
return
|
|
5476
|
+
return path16.resolve(filePath);
|
|
5383
5477
|
}
|
|
5384
5478
|
}
|
|
5385
5479
|
};
|
|
@@ -5496,15 +5590,15 @@ var init_session_history = __esm({
|
|
|
5496
5590
|
|
|
5497
5591
|
// src/daemon/state.ts
|
|
5498
5592
|
import net2 from "net";
|
|
5499
|
-
import
|
|
5500
|
-
import
|
|
5501
|
-
import
|
|
5593
|
+
import fs14 from "fs";
|
|
5594
|
+
import path17 from "path";
|
|
5595
|
+
import os12 from "os";
|
|
5502
5596
|
import { spawn as spawn2 } from "child_process";
|
|
5503
5597
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
5504
5598
|
function loadInsightCounts() {
|
|
5505
5599
|
try {
|
|
5506
|
-
if (!
|
|
5507
|
-
const data = JSON.parse(
|
|
5600
|
+
if (!fs14.existsSync(INSIGHT_COUNTS_FILE)) return;
|
|
5601
|
+
const data = JSON.parse(fs14.readFileSync(INSIGHT_COUNTS_FILE, "utf-8"));
|
|
5508
5602
|
for (const [tool, count] of Object.entries(data)) {
|
|
5509
5603
|
if (typeof count === "number" && count > 0) insightCounts.set(tool, count);
|
|
5510
5604
|
}
|
|
@@ -5543,23 +5637,23 @@ function markRejectionHandlerRegistered() {
|
|
|
5543
5637
|
daemonRejectionHandlerRegistered = true;
|
|
5544
5638
|
}
|
|
5545
5639
|
function atomicWriteSync2(filePath, data, options) {
|
|
5546
|
-
const dir =
|
|
5547
|
-
if (!
|
|
5640
|
+
const dir = path17.dirname(filePath);
|
|
5641
|
+
if (!fs14.existsSync(dir)) fs14.mkdirSync(dir, { recursive: true });
|
|
5548
5642
|
const tmpPath = `${filePath}.${randomUUID3()}.tmp`;
|
|
5549
5643
|
try {
|
|
5550
|
-
|
|
5644
|
+
fs14.writeFileSync(tmpPath, data, options);
|
|
5551
5645
|
} catch (err2) {
|
|
5552
5646
|
try {
|
|
5553
|
-
|
|
5647
|
+
fs14.unlinkSync(tmpPath);
|
|
5554
5648
|
} catch {
|
|
5555
5649
|
}
|
|
5556
5650
|
throw err2;
|
|
5557
5651
|
}
|
|
5558
5652
|
try {
|
|
5559
|
-
|
|
5653
|
+
fs14.renameSync(tmpPath, filePath);
|
|
5560
5654
|
} catch (err2) {
|
|
5561
5655
|
try {
|
|
5562
|
-
|
|
5656
|
+
fs14.unlinkSync(tmpPath);
|
|
5563
5657
|
} catch {
|
|
5564
5658
|
}
|
|
5565
5659
|
throw err2;
|
|
@@ -5583,16 +5677,16 @@ function appendAuditLog(data) {
|
|
|
5583
5677
|
decision: data.decision,
|
|
5584
5678
|
source: "daemon"
|
|
5585
5679
|
};
|
|
5586
|
-
const dir =
|
|
5587
|
-
if (!
|
|
5588
|
-
|
|
5680
|
+
const dir = path17.dirname(AUDIT_LOG_FILE);
|
|
5681
|
+
if (!fs14.existsSync(dir)) fs14.mkdirSync(dir, { recursive: true });
|
|
5682
|
+
fs14.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
|
|
5589
5683
|
} catch {
|
|
5590
5684
|
}
|
|
5591
5685
|
}
|
|
5592
5686
|
function getAuditHistory(limit = 20) {
|
|
5593
5687
|
try {
|
|
5594
|
-
if (!
|
|
5595
|
-
const lines =
|
|
5688
|
+
if (!fs14.existsSync(AUDIT_LOG_FILE)) return [];
|
|
5689
|
+
const lines = fs14.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
|
|
5596
5690
|
if (lines.length === 1 && lines[0] === "") return [];
|
|
5597
5691
|
return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
|
|
5598
5692
|
} catch {
|
|
@@ -5601,19 +5695,19 @@ function getAuditHistory(limit = 20) {
|
|
|
5601
5695
|
}
|
|
5602
5696
|
function getOrgName() {
|
|
5603
5697
|
try {
|
|
5604
|
-
if (
|
|
5698
|
+
if (fs14.existsSync(CREDENTIALS_FILE)) return "Node9 Cloud";
|
|
5605
5699
|
} catch {
|
|
5606
5700
|
}
|
|
5607
5701
|
return null;
|
|
5608
5702
|
}
|
|
5609
5703
|
function hasStoredSlackKey() {
|
|
5610
|
-
return
|
|
5704
|
+
return fs14.existsSync(CREDENTIALS_FILE);
|
|
5611
5705
|
}
|
|
5612
5706
|
function writeGlobalSetting(key, value) {
|
|
5613
5707
|
let config = {};
|
|
5614
5708
|
try {
|
|
5615
|
-
if (
|
|
5616
|
-
config = JSON.parse(
|
|
5709
|
+
if (fs14.existsSync(GLOBAL_CONFIG_FILE)) {
|
|
5710
|
+
config = JSON.parse(fs14.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
|
|
5617
5711
|
}
|
|
5618
5712
|
} catch {
|
|
5619
5713
|
}
|
|
@@ -5625,8 +5719,8 @@ function writeTrustEntry(toolName, durationMs) {
|
|
|
5625
5719
|
try {
|
|
5626
5720
|
let trust = { entries: [] };
|
|
5627
5721
|
try {
|
|
5628
|
-
if (
|
|
5629
|
-
trust = JSON.parse(
|
|
5722
|
+
if (fs14.existsSync(TRUST_FILE2))
|
|
5723
|
+
trust = JSON.parse(fs14.readFileSync(TRUST_FILE2, "utf-8"));
|
|
5630
5724
|
} catch {
|
|
5631
5725
|
}
|
|
5632
5726
|
trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
|
|
@@ -5637,8 +5731,8 @@ function writeTrustEntry(toolName, durationMs) {
|
|
|
5637
5731
|
}
|
|
5638
5732
|
function readPersistentDecisions() {
|
|
5639
5733
|
try {
|
|
5640
|
-
if (
|
|
5641
|
-
return JSON.parse(
|
|
5734
|
+
if (fs14.existsSync(DECISIONS_FILE)) {
|
|
5735
|
+
return JSON.parse(fs14.readFileSync(DECISIONS_FILE, "utf-8"));
|
|
5642
5736
|
}
|
|
5643
5737
|
} catch {
|
|
5644
5738
|
}
|
|
@@ -5675,7 +5769,7 @@ function estimateToolCost(tool, args) {
|
|
|
5675
5769
|
const filePath = a.file_path ?? a.path;
|
|
5676
5770
|
if (filePath) {
|
|
5677
5771
|
try {
|
|
5678
|
-
const bytes =
|
|
5772
|
+
const bytes = fs14.statSync(filePath).size;
|
|
5679
5773
|
return bytes / BYTES_PER_TOKEN / 1e6 * INPUT_PRICE_PER_1M;
|
|
5680
5774
|
} catch {
|
|
5681
5775
|
}
|
|
@@ -5733,7 +5827,7 @@ function abandonPending() {
|
|
|
5733
5827
|
});
|
|
5734
5828
|
if (autoStarted) {
|
|
5735
5829
|
try {
|
|
5736
|
-
|
|
5830
|
+
fs14.unlinkSync(DAEMON_PID_FILE);
|
|
5737
5831
|
} catch {
|
|
5738
5832
|
}
|
|
5739
5833
|
setTimeout(() => {
|
|
@@ -5744,7 +5838,7 @@ function abandonPending() {
|
|
|
5744
5838
|
}
|
|
5745
5839
|
function startActivitySocket() {
|
|
5746
5840
|
try {
|
|
5747
|
-
|
|
5841
|
+
fs14.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
5748
5842
|
} catch {
|
|
5749
5843
|
}
|
|
5750
5844
|
const ACTIVITY_MAX_BYTES = 1024 * 1024;
|
|
@@ -5825,7 +5919,7 @@ function startActivitySocket() {
|
|
|
5825
5919
|
unixServer.listen(ACTIVITY_SOCKET_PATH2);
|
|
5826
5920
|
process.on("exit", () => {
|
|
5827
5921
|
try {
|
|
5828
|
-
|
|
5922
|
+
fs14.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
5829
5923
|
} catch {
|
|
5830
5924
|
}
|
|
5831
5925
|
});
|
|
@@ -5839,14 +5933,14 @@ var init_state2 = __esm({
|
|
|
5839
5933
|
init_taint_store();
|
|
5840
5934
|
init_session_counters();
|
|
5841
5935
|
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 =
|
|
5936
|
+
homeDir = os12.homedir();
|
|
5937
|
+
DAEMON_PID_FILE = path17.join(homeDir, ".node9", "daemon.pid");
|
|
5938
|
+
DECISIONS_FILE = path17.join(homeDir, ".node9", "decisions.json");
|
|
5939
|
+
AUDIT_LOG_FILE = path17.join(homeDir, ".node9", "audit.log");
|
|
5940
|
+
TRUST_FILE2 = path17.join(homeDir, ".node9", "trust.json");
|
|
5941
|
+
GLOBAL_CONFIG_FILE = path17.join(homeDir, ".node9", "config.json");
|
|
5942
|
+
CREDENTIALS_FILE = path17.join(homeDir, ".node9", "credentials.json");
|
|
5943
|
+
INSIGHT_COUNTS_FILE = path17.join(homeDir, ".node9", "insight-counts.json");
|
|
5850
5944
|
pending = /* @__PURE__ */ new Map();
|
|
5851
5945
|
sseClients = /* @__PURE__ */ new Set();
|
|
5852
5946
|
suggestionTracker = new SuggestionTracker(3);
|
|
@@ -5864,7 +5958,7 @@ var init_state2 = __esm({
|
|
|
5864
5958
|
"2h": 2 * 60 * 6e4
|
|
5865
5959
|
};
|
|
5866
5960
|
autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
5867
|
-
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
5961
|
+
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path17.join(os12.tmpdir(), "node9-activity.sock");
|
|
5868
5962
|
ACTIVITY_RING_SIZE = 100;
|
|
5869
5963
|
activityRing = [];
|
|
5870
5964
|
SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
@@ -5886,14 +5980,14 @@ var init_state2 = __esm({
|
|
|
5886
5980
|
});
|
|
5887
5981
|
|
|
5888
5982
|
// src/config/patch.ts
|
|
5889
|
-
import
|
|
5890
|
-
import
|
|
5891
|
-
import
|
|
5983
|
+
import fs15 from "fs";
|
|
5984
|
+
import path18 from "path";
|
|
5985
|
+
import os13 from "os";
|
|
5892
5986
|
function patchConfig(configPath, patch) {
|
|
5893
5987
|
let config = {};
|
|
5894
5988
|
try {
|
|
5895
|
-
if (
|
|
5896
|
-
config = JSON.parse(
|
|
5989
|
+
if (fs15.existsSync(configPath)) {
|
|
5990
|
+
config = JSON.parse(fs15.readFileSync(configPath, "utf8"));
|
|
5897
5991
|
}
|
|
5898
5992
|
} catch {
|
|
5899
5993
|
throw new Error(`Cannot read config at ${configPath} \u2014 file may be corrupted`);
|
|
@@ -5912,23 +6006,23 @@ function patchConfig(configPath, patch) {
|
|
|
5912
6006
|
ignored.push(patch.toolName);
|
|
5913
6007
|
}
|
|
5914
6008
|
}
|
|
5915
|
-
const dir =
|
|
5916
|
-
|
|
6009
|
+
const dir = path18.dirname(configPath);
|
|
6010
|
+
fs15.mkdirSync(dir, { recursive: true });
|
|
5917
6011
|
const tmp = configPath + ".node9-tmp";
|
|
5918
6012
|
try {
|
|
5919
|
-
|
|
6013
|
+
fs15.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
|
|
5920
6014
|
} catch (err2) {
|
|
5921
6015
|
try {
|
|
5922
|
-
|
|
6016
|
+
fs15.unlinkSync(tmp);
|
|
5923
6017
|
} catch {
|
|
5924
6018
|
}
|
|
5925
6019
|
throw err2;
|
|
5926
6020
|
}
|
|
5927
6021
|
try {
|
|
5928
|
-
|
|
6022
|
+
fs15.renameSync(tmp, configPath);
|
|
5929
6023
|
} catch (err2) {
|
|
5930
6024
|
try {
|
|
5931
|
-
|
|
6025
|
+
fs15.unlinkSync(tmp);
|
|
5932
6026
|
} catch {
|
|
5933
6027
|
}
|
|
5934
6028
|
throw err2;
|
|
@@ -5938,14 +6032,14 @@ var GLOBAL_CONFIG_PATH;
|
|
|
5938
6032
|
var init_patch = __esm({
|
|
5939
6033
|
"src/config/patch.ts"() {
|
|
5940
6034
|
"use strict";
|
|
5941
|
-
GLOBAL_CONFIG_PATH =
|
|
6035
|
+
GLOBAL_CONFIG_PATH = path18.join(os13.homedir(), ".node9", "config.json");
|
|
5942
6036
|
}
|
|
5943
6037
|
});
|
|
5944
6038
|
|
|
5945
6039
|
// src/daemon/server.ts
|
|
5946
6040
|
import http from "http";
|
|
5947
|
-
import
|
|
5948
|
-
import
|
|
6041
|
+
import fs16 from "fs";
|
|
6042
|
+
import path19 from "path";
|
|
5949
6043
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
5950
6044
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
5951
6045
|
import chalk2 from "chalk";
|
|
@@ -5965,7 +6059,7 @@ function startDaemon() {
|
|
|
5965
6059
|
idleTimer = setTimeout(() => {
|
|
5966
6060
|
if (autoStarted) {
|
|
5967
6061
|
try {
|
|
5968
|
-
|
|
6062
|
+
fs16.unlinkSync(DAEMON_PID_FILE);
|
|
5969
6063
|
} catch {
|
|
5970
6064
|
}
|
|
5971
6065
|
}
|
|
@@ -6128,7 +6222,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
6128
6222
|
status: "pending"
|
|
6129
6223
|
});
|
|
6130
6224
|
}
|
|
6131
|
-
const projectCwd = typeof cwd === "string" &&
|
|
6225
|
+
const projectCwd = typeof cwd === "string" && path19.isAbsolute(cwd) ? cwd : void 0;
|
|
6132
6226
|
const projectConfig = getConfig(projectCwd);
|
|
6133
6227
|
const browserEnabled = projectConfig.settings.approvers?.browser !== false;
|
|
6134
6228
|
const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
|
|
@@ -6518,8 +6612,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
6518
6612
|
const body = await readBody(req);
|
|
6519
6613
|
const data = body ? JSON.parse(body) : {};
|
|
6520
6614
|
const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
|
|
6521
|
-
const node9Dir =
|
|
6522
|
-
if (!
|
|
6615
|
+
const node9Dir = path19.dirname(GLOBAL_CONFIG_PATH);
|
|
6616
|
+
if (!path19.resolve(configPath).startsWith(node9Dir + path19.sep)) {
|
|
6523
6617
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6524
6618
|
return res.end(
|
|
6525
6619
|
JSON.stringify({ error: "configPath must be within the node9 config directory" })
|
|
@@ -6630,14 +6724,14 @@ data: ${JSON.stringify(item.data)}
|
|
|
6630
6724
|
server.on("error", (e) => {
|
|
6631
6725
|
if (e.code === "EADDRINUSE") {
|
|
6632
6726
|
try {
|
|
6633
|
-
if (
|
|
6634
|
-
const { pid } = JSON.parse(
|
|
6727
|
+
if (fs16.existsSync(DAEMON_PID_FILE)) {
|
|
6728
|
+
const { pid } = JSON.parse(fs16.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
6635
6729
|
process.kill(pid, 0);
|
|
6636
6730
|
return process.exit(0);
|
|
6637
6731
|
}
|
|
6638
6732
|
} catch {
|
|
6639
6733
|
try {
|
|
6640
|
-
|
|
6734
|
+
fs16.unlinkSync(DAEMON_PID_FILE);
|
|
6641
6735
|
} catch {
|
|
6642
6736
|
}
|
|
6643
6737
|
server.listen(DAEMON_PORT, DAEMON_HOST);
|
|
@@ -6709,28 +6803,28 @@ var init_server = __esm({
|
|
|
6709
6803
|
});
|
|
6710
6804
|
|
|
6711
6805
|
// src/daemon/index.ts
|
|
6712
|
-
import
|
|
6806
|
+
import fs17 from "fs";
|
|
6713
6807
|
import chalk3 from "chalk";
|
|
6714
6808
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
6715
6809
|
function stopDaemon() {
|
|
6716
|
-
if (!
|
|
6810
|
+
if (!fs17.existsSync(DAEMON_PID_FILE)) return console.log(chalk3.yellow("Not running."));
|
|
6717
6811
|
try {
|
|
6718
|
-
const { pid } = JSON.parse(
|
|
6812
|
+
const { pid } = JSON.parse(fs17.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
6719
6813
|
process.kill(pid, "SIGTERM");
|
|
6720
6814
|
console.log(chalk3.green("\u2705 Stopped."));
|
|
6721
6815
|
} catch {
|
|
6722
6816
|
console.log(chalk3.gray("Cleaned up stale PID file."));
|
|
6723
6817
|
} finally {
|
|
6724
6818
|
try {
|
|
6725
|
-
|
|
6819
|
+
fs17.unlinkSync(DAEMON_PID_FILE);
|
|
6726
6820
|
} catch {
|
|
6727
6821
|
}
|
|
6728
6822
|
}
|
|
6729
6823
|
}
|
|
6730
6824
|
function daemonStatus() {
|
|
6731
|
-
if (
|
|
6825
|
+
if (fs17.existsSync(DAEMON_PID_FILE)) {
|
|
6732
6826
|
try {
|
|
6733
|
-
const { pid } = JSON.parse(
|
|
6827
|
+
const { pid } = JSON.parse(fs17.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
6734
6828
|
process.kill(pid, 0);
|
|
6735
6829
|
console.log(chalk3.green("Node9 daemon: running"));
|
|
6736
6830
|
return;
|
|
@@ -6764,10 +6858,10 @@ __export(tail_exports, {
|
|
|
6764
6858
|
startTail: () => startTail
|
|
6765
6859
|
});
|
|
6766
6860
|
import http2 from "http";
|
|
6767
|
-
import
|
|
6768
|
-
import
|
|
6769
|
-
import
|
|
6770
|
-
import
|
|
6861
|
+
import chalk19 from "chalk";
|
|
6862
|
+
import fs28 from "fs";
|
|
6863
|
+
import os24 from "os";
|
|
6864
|
+
import path31 from "path";
|
|
6771
6865
|
import readline5 from "readline";
|
|
6772
6866
|
import { spawn as spawn10, execSync as execSync3 } from "child_process";
|
|
6773
6867
|
function getIcon(tool) {
|
|
@@ -6777,44 +6871,64 @@ function getIcon(tool) {
|
|
|
6777
6871
|
}
|
|
6778
6872
|
return "\u{1F6E0}\uFE0F";
|
|
6779
6873
|
}
|
|
6874
|
+
function visibleLength(s) {
|
|
6875
|
+
return s.replace(/\x1B\[[0-9;]*m/g, "").length;
|
|
6876
|
+
}
|
|
6877
|
+
function wrappedLineCount(text) {
|
|
6878
|
+
const cols = process.stdout.columns;
|
|
6879
|
+
if (!cols) return 1;
|
|
6880
|
+
const len = visibleLength(text);
|
|
6881
|
+
return Math.max(1, Math.ceil(len / cols));
|
|
6882
|
+
}
|
|
6780
6883
|
function formatBase(activity) {
|
|
6781
6884
|
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
6782
6885
|
const icon = getIcon(activity.tool);
|
|
6783
6886
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
6784
|
-
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(
|
|
6887
|
+
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os24.homedir(), "~");
|
|
6785
6888
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
6786
|
-
return `${
|
|
6889
|
+
return `${chalk19.gray(time)} ${icon} ${chalk19.white.bold(toolName)} ${chalk19.dim(argsPreview)}`;
|
|
6787
6890
|
}
|
|
6788
6891
|
function renderResult(activity, result) {
|
|
6789
6892
|
const base = formatBase(activity);
|
|
6790
6893
|
let status;
|
|
6791
6894
|
if (result.status === "allow") {
|
|
6792
|
-
status =
|
|
6895
|
+
status = chalk19.green("\u2713 ALLOW");
|
|
6793
6896
|
} else if (result.status === "dlp") {
|
|
6794
|
-
status =
|
|
6897
|
+
status = chalk19.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
6795
6898
|
} else {
|
|
6796
|
-
status =
|
|
6899
|
+
status = chalk19.red("\u2717 BLOCK");
|
|
6797
6900
|
}
|
|
6798
6901
|
const cost = result.costEstimate ?? activity.costEstimate;
|
|
6799
|
-
const costSuffix = cost == null ? "" :
|
|
6902
|
+
const costSuffix = cost == null ? "" : chalk19.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
|
|
6800
6903
|
if (process.stdout.isTTY) {
|
|
6801
|
-
|
|
6802
|
-
|
|
6904
|
+
if (pendingShownForId === activity.id && pendingWrappedLines > 1) {
|
|
6905
|
+
readline5.moveCursor(process.stdout, 0, -(pendingWrappedLines - 1));
|
|
6906
|
+
readline5.cursorTo(process.stdout, 0);
|
|
6907
|
+
process.stdout.write(ERASE_DOWN);
|
|
6908
|
+
} else {
|
|
6909
|
+
readline5.clearLine(process.stdout, 0);
|
|
6910
|
+
readline5.cursorTo(process.stdout, 0);
|
|
6911
|
+
}
|
|
6912
|
+
pendingShownForId = null;
|
|
6913
|
+
pendingWrappedLines = 0;
|
|
6803
6914
|
}
|
|
6804
6915
|
console.log(`${base} ${status}${costSuffix}`);
|
|
6805
6916
|
}
|
|
6806
6917
|
function renderPending(activity) {
|
|
6807
6918
|
if (!process.stdout.isTTY) return;
|
|
6808
|
-
|
|
6919
|
+
const line = `${formatBase(activity)} ${chalk19.yellow("\u25CF \u2026")}`;
|
|
6920
|
+
pendingShownForId = activity.id;
|
|
6921
|
+
pendingWrappedLines = wrappedLineCount(line);
|
|
6922
|
+
process.stdout.write(`${line}\r`);
|
|
6809
6923
|
}
|
|
6810
6924
|
async function ensureDaemon() {
|
|
6811
6925
|
let pidPort = null;
|
|
6812
|
-
if (
|
|
6926
|
+
if (fs28.existsSync(PID_FILE)) {
|
|
6813
6927
|
try {
|
|
6814
|
-
const { port } = JSON.parse(
|
|
6928
|
+
const { port } = JSON.parse(fs28.readFileSync(PID_FILE, "utf-8"));
|
|
6815
6929
|
pidPort = port;
|
|
6816
6930
|
} catch {
|
|
6817
|
-
console.error(
|
|
6931
|
+
console.error(chalk19.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
6818
6932
|
}
|
|
6819
6933
|
}
|
|
6820
6934
|
const checkPort = pidPort ?? DAEMON_PORT;
|
|
@@ -6825,7 +6939,7 @@ async function ensureDaemon() {
|
|
|
6825
6939
|
if (res.ok) return checkPort;
|
|
6826
6940
|
} catch {
|
|
6827
6941
|
}
|
|
6828
|
-
console.log(
|
|
6942
|
+
console.log(chalk19.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
6829
6943
|
const child = spawn10(process.execPath, [process.argv[1], "daemon"], {
|
|
6830
6944
|
detached: true,
|
|
6831
6945
|
stdio: "ignore",
|
|
@@ -6842,7 +6956,7 @@ async function ensureDaemon() {
|
|
|
6842
6956
|
} catch {
|
|
6843
6957
|
}
|
|
6844
6958
|
}
|
|
6845
|
-
console.error(
|
|
6959
|
+
console.error(chalk19.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
6846
6960
|
process.exit(1);
|
|
6847
6961
|
}
|
|
6848
6962
|
function postDecisionHttp(id, decision, csrfToken, port, opts) {
|
|
@@ -6931,9 +7045,9 @@ function buildRecoveryCardLines(req) {
|
|
|
6931
7045
|
];
|
|
6932
7046
|
}
|
|
6933
7047
|
function readApproversFromDisk() {
|
|
6934
|
-
const configPath =
|
|
7048
|
+
const configPath = path31.join(os24.homedir(), ".node9", "config.json");
|
|
6935
7049
|
try {
|
|
6936
|
-
const raw = JSON.parse(
|
|
7050
|
+
const raw = JSON.parse(fs28.readFileSync(configPath, "utf-8"));
|
|
6937
7051
|
const settings = raw.settings ?? {};
|
|
6938
7052
|
return settings.approvers ?? {};
|
|
6939
7053
|
} catch {
|
|
@@ -6944,20 +7058,20 @@ function approverStatusLine() {
|
|
|
6944
7058
|
const a = readApproversFromDisk();
|
|
6945
7059
|
const fmt = (label, key) => {
|
|
6946
7060
|
const on = a[key] !== false;
|
|
6947
|
-
return `[${key[0]}]${label.slice(1)} ${on ?
|
|
7061
|
+
return `[${key[0]}]${label.slice(1)} ${on ? chalk19.green("\u2713") : chalk19.dim("\u2717")}`;
|
|
6948
7062
|
};
|
|
6949
7063
|
return `${fmt("native", "native")} ${fmt("browser", "browser")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
|
|
6950
7064
|
}
|
|
6951
7065
|
function toggleApprover(channel) {
|
|
6952
|
-
const configPath =
|
|
7066
|
+
const configPath = path31.join(os24.homedir(), ".node9", "config.json");
|
|
6953
7067
|
try {
|
|
6954
|
-
const raw = JSON.parse(
|
|
7068
|
+
const raw = JSON.parse(fs28.readFileSync(configPath, "utf-8"));
|
|
6955
7069
|
const settings = raw.settings ?? {};
|
|
6956
7070
|
const approvers = settings.approvers ?? {};
|
|
6957
7071
|
approvers[channel] = approvers[channel] === false;
|
|
6958
7072
|
settings.approvers = approvers;
|
|
6959
7073
|
raw.settings = settings;
|
|
6960
|
-
|
|
7074
|
+
fs28.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
|
|
6961
7075
|
} catch (err2) {
|
|
6962
7076
|
process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
|
|
6963
7077
|
`);
|
|
@@ -6989,7 +7103,7 @@ async function startTail(options = {}) {
|
|
|
6989
7103
|
req2.end();
|
|
6990
7104
|
});
|
|
6991
7105
|
if (result.ok) {
|
|
6992
|
-
console.log(
|
|
7106
|
+
console.log(chalk19.green("\u2713 Flight Recorder buffer cleared."));
|
|
6993
7107
|
} else if (result.code === "ECONNREFUSED") {
|
|
6994
7108
|
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
6995
7109
|
} else if (result.code === "ETIMEDOUT") {
|
|
@@ -7033,7 +7147,7 @@ async function startTail(options = {}) {
|
|
|
7033
7147
|
const channel = name === "n" ? "native" : name === "b" ? "browser" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
|
|
7034
7148
|
if (channel) {
|
|
7035
7149
|
toggleApprover(channel);
|
|
7036
|
-
console.log(
|
|
7150
|
+
console.log(chalk19.dim(` Approvers: ${approverStatusLine()}`));
|
|
7037
7151
|
}
|
|
7038
7152
|
};
|
|
7039
7153
|
process.stdin.on("keypress", idleKeypressHandler);
|
|
@@ -7099,7 +7213,7 @@ async function startTail(options = {}) {
|
|
|
7099
7213
|
localAllowCounts.get(req2.toolName) ?? 0
|
|
7100
7214
|
)
|
|
7101
7215
|
);
|
|
7102
|
-
const decisionStamp = action === "always-allow" ?
|
|
7216
|
+
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
7217
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
|
|
7104
7218
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
7105
7219
|
process.stdout.write(SHOW_CURSOR);
|
|
@@ -7127,8 +7241,8 @@ async function startTail(options = {}) {
|
|
|
7127
7241
|
}
|
|
7128
7242
|
postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err2) => {
|
|
7129
7243
|
try {
|
|
7130
|
-
|
|
7131
|
-
|
|
7244
|
+
fs28.appendFileSync(
|
|
7245
|
+
path31.join(os24.homedir(), ".node9", "hook-debug.log"),
|
|
7132
7246
|
`[tail] POST /decision failed: ${String(err2)}
|
|
7133
7247
|
`
|
|
7134
7248
|
);
|
|
@@ -7150,7 +7264,7 @@ async function startTail(options = {}) {
|
|
|
7150
7264
|
);
|
|
7151
7265
|
const stampedLines = buildCardLines(req2, priorCount);
|
|
7152
7266
|
if (externalDecision) {
|
|
7153
|
-
const source = externalDecision === "allow" ?
|
|
7267
|
+
const source = externalDecision === "allow" ? chalk19.green("\u2713 ALLOWED") : chalk19.red("\u2717 DENIED");
|
|
7154
7268
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
|
|
7155
7269
|
}
|
|
7156
7270
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
@@ -7209,16 +7323,16 @@ async function startTail(options = {}) {
|
|
|
7209
7323
|
}
|
|
7210
7324
|
} catch {
|
|
7211
7325
|
}
|
|
7212
|
-
console.log(
|
|
7213
|
-
\u{1F6F0}\uFE0F Node9 tail `) +
|
|
7326
|
+
console.log(chalk19.cyan.bold(`
|
|
7327
|
+
\u{1F6F0}\uFE0F Node9 tail `) + chalk19.dim(`\u2192 ${dashboardUrl}`));
|
|
7214
7328
|
if (canApprove) {
|
|
7215
|
-
console.log(
|
|
7216
|
-
console.log(
|
|
7329
|
+
console.log(chalk19.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
|
|
7330
|
+
console.log(chalk19.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
|
|
7217
7331
|
}
|
|
7218
7332
|
if (options.history) {
|
|
7219
|
-
console.log(
|
|
7333
|
+
console.log(chalk19.dim("Showing history + live events.\n"));
|
|
7220
7334
|
} else {
|
|
7221
|
-
console.log(
|
|
7335
|
+
console.log(chalk19.dim("Showing live events only. Use --history to include past.\n"));
|
|
7222
7336
|
}
|
|
7223
7337
|
process.on("SIGINT", () => {
|
|
7224
7338
|
exitIdleMode();
|
|
@@ -7228,13 +7342,13 @@ async function startTail(options = {}) {
|
|
|
7228
7342
|
readline5.clearLine(process.stdout, 0);
|
|
7229
7343
|
readline5.cursorTo(process.stdout, 0);
|
|
7230
7344
|
}
|
|
7231
|
-
console.log(
|
|
7345
|
+
console.log(chalk19.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
7232
7346
|
process.exit(0);
|
|
7233
7347
|
});
|
|
7234
7348
|
const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
|
|
7235
7349
|
const req = http2.get(sseUrl, (res) => {
|
|
7236
7350
|
if (res.statusCode !== 200) {
|
|
7237
|
-
console.error(
|
|
7351
|
+
console.error(chalk19.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
7238
7352
|
process.exit(1);
|
|
7239
7353
|
}
|
|
7240
7354
|
if (canApprove) enterIdleMode();
|
|
@@ -7265,7 +7379,7 @@ async function startTail(options = {}) {
|
|
|
7265
7379
|
readline5.clearLine(process.stdout, 0);
|
|
7266
7380
|
readline5.cursorTo(process.stdout, 0);
|
|
7267
7381
|
}
|
|
7268
|
-
console.log(
|
|
7382
|
+
console.log(chalk19.red("\n\u274C Daemon disconnected."));
|
|
7269
7383
|
process.exit(1);
|
|
7270
7384
|
});
|
|
7271
7385
|
});
|
|
@@ -7357,9 +7471,9 @@ async function startTail(options = {}) {
|
|
|
7357
7471
|
const hash = data.hash ?? "";
|
|
7358
7472
|
const summary = data.argsSummary ?? data.tool;
|
|
7359
7473
|
const fileCount = data.fileCount ?? 0;
|
|
7360
|
-
const files = fileCount > 0 ?
|
|
7474
|
+
const files = fileCount > 0 ? chalk19.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
|
|
7361
7475
|
process.stdout.write(
|
|
7362
|
-
`${
|
|
7476
|
+
`${chalk19.dim(time)} ${chalk19.cyan("\u{1F4F8} snapshot")} ${chalk19.dim(hash)} ${summary}${files}
|
|
7363
7477
|
`
|
|
7364
7478
|
);
|
|
7365
7479
|
return;
|
|
@@ -7376,19 +7490,19 @@ async function startTail(options = {}) {
|
|
|
7376
7490
|
}
|
|
7377
7491
|
req.on("error", (err2) => {
|
|
7378
7492
|
const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
|
|
7379
|
-
console.error(
|
|
7493
|
+
console.error(chalk19.red(`
|
|
7380
7494
|
\u274C ${msg}`));
|
|
7381
7495
|
process.exit(1);
|
|
7382
7496
|
});
|
|
7383
7497
|
}
|
|
7384
|
-
var PID_FILE, ICONS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, DIVIDER;
|
|
7498
|
+
var PID_FILE, ICONS, RESET2, BOLD2, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, pendingShownForId, pendingWrappedLines, DIVIDER;
|
|
7385
7499
|
var init_tail = __esm({
|
|
7386
7500
|
"src/tui/tail.ts"() {
|
|
7387
7501
|
"use strict";
|
|
7388
7502
|
init_daemon2();
|
|
7389
7503
|
init_daemon();
|
|
7390
7504
|
init_core();
|
|
7391
|
-
PID_FILE =
|
|
7505
|
+
PID_FILE = path31.join(os24.homedir(), ".node9", "daemon.pid");
|
|
7392
7506
|
ICONS = {
|
|
7393
7507
|
bash: "\u{1F4BB}",
|
|
7394
7508
|
shell: "\u{1F4BB}",
|
|
@@ -7416,6 +7530,8 @@ var init_tail = __esm({
|
|
|
7416
7530
|
HIDE_CURSOR = "\x1B[?25l";
|
|
7417
7531
|
SHOW_CURSOR = "\x1B[?25h";
|
|
7418
7532
|
ERASE_DOWN = "\x1B[J";
|
|
7533
|
+
pendingShownForId = null;
|
|
7534
|
+
pendingWrappedLines = 0;
|
|
7419
7535
|
DIVIDER = "\u2500".repeat(60);
|
|
7420
7536
|
}
|
|
7421
7537
|
});
|
|
@@ -7427,9 +7543,9 @@ __export(hud_exports, {
|
|
|
7427
7543
|
main: () => main,
|
|
7428
7544
|
renderEnvironmentLine: () => renderEnvironmentLine
|
|
7429
7545
|
});
|
|
7430
|
-
import
|
|
7431
|
-
import
|
|
7432
|
-
import
|
|
7546
|
+
import fs29 from "fs";
|
|
7547
|
+
import path32 from "path";
|
|
7548
|
+
import os25 from "os";
|
|
7433
7549
|
import http3 from "http";
|
|
7434
7550
|
async function readStdin() {
|
|
7435
7551
|
const chunks = [];
|
|
@@ -7488,10 +7604,10 @@ function bold(s) {
|
|
|
7488
7604
|
function color(c, s) {
|
|
7489
7605
|
return `${c}${s}${RESET3}`;
|
|
7490
7606
|
}
|
|
7491
|
-
function progressBar(
|
|
7492
|
-
const filled = Math.round(Math.min(
|
|
7607
|
+
function progressBar(pct2, warnAt = 70, critAt = 85) {
|
|
7608
|
+
const filled = Math.round(Math.min(pct2, 100) / 100 * BAR_WIDTH);
|
|
7493
7609
|
const bar = BAR_FILLED.repeat(filled) + BAR_EMPTY.repeat(BAR_WIDTH - filled);
|
|
7494
|
-
const c =
|
|
7610
|
+
const c = pct2 >= critAt ? RED2 : pct2 >= warnAt ? YELLOW2 : GREEN2;
|
|
7495
7611
|
return `${c}${bar}${RESET3}`;
|
|
7496
7612
|
}
|
|
7497
7613
|
function formatTimeLeft(resetsAt) {
|
|
@@ -7505,9 +7621,9 @@ function formatTimeLeft(resetsAt) {
|
|
|
7505
7621
|
return ` (${m}m left)`;
|
|
7506
7622
|
}
|
|
7507
7623
|
function safeReadJson(filePath) {
|
|
7508
|
-
if (!
|
|
7624
|
+
if (!fs29.existsSync(filePath)) return null;
|
|
7509
7625
|
try {
|
|
7510
|
-
return JSON.parse(
|
|
7626
|
+
return JSON.parse(fs29.readFileSync(filePath, "utf-8"));
|
|
7511
7627
|
} catch {
|
|
7512
7628
|
return null;
|
|
7513
7629
|
}
|
|
@@ -7528,12 +7644,12 @@ function countHooksInFile(filePath) {
|
|
|
7528
7644
|
return Object.keys(cfg.hooks).length;
|
|
7529
7645
|
}
|
|
7530
7646
|
function countRulesInDir(rulesDir) {
|
|
7531
|
-
if (!
|
|
7647
|
+
if (!fs29.existsSync(rulesDir)) return 0;
|
|
7532
7648
|
let count = 0;
|
|
7533
7649
|
try {
|
|
7534
|
-
for (const entry of
|
|
7650
|
+
for (const entry of fs29.readdirSync(rulesDir, { withFileTypes: true })) {
|
|
7535
7651
|
if (entry.isDirectory()) {
|
|
7536
|
-
count += countRulesInDir(
|
|
7652
|
+
count += countRulesInDir(path32.join(rulesDir, entry.name));
|
|
7537
7653
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
7538
7654
|
count++;
|
|
7539
7655
|
}
|
|
@@ -7544,46 +7660,46 @@ function countRulesInDir(rulesDir) {
|
|
|
7544
7660
|
}
|
|
7545
7661
|
function isSamePath(a, b) {
|
|
7546
7662
|
try {
|
|
7547
|
-
return
|
|
7663
|
+
return path32.resolve(a) === path32.resolve(b);
|
|
7548
7664
|
} catch {
|
|
7549
7665
|
return false;
|
|
7550
7666
|
}
|
|
7551
7667
|
}
|
|
7552
7668
|
function countConfigs(cwd) {
|
|
7553
|
-
const homeDir2 =
|
|
7554
|
-
const claudeDir =
|
|
7669
|
+
const homeDir2 = os25.homedir();
|
|
7670
|
+
const claudeDir = path32.join(homeDir2, ".claude");
|
|
7555
7671
|
let claudeMdCount = 0;
|
|
7556
7672
|
let rulesCount = 0;
|
|
7557
7673
|
let hooksCount = 0;
|
|
7558
7674
|
const userMcpServers = /* @__PURE__ */ new Set();
|
|
7559
7675
|
const projectMcpServers = /* @__PURE__ */ new Set();
|
|
7560
|
-
if (
|
|
7561
|
-
rulesCount += countRulesInDir(
|
|
7562
|
-
const userSettings =
|
|
7676
|
+
if (fs29.existsSync(path32.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
7677
|
+
rulesCount += countRulesInDir(path32.join(claudeDir, "rules"));
|
|
7678
|
+
const userSettings = path32.join(claudeDir, "settings.json");
|
|
7563
7679
|
for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
|
|
7564
7680
|
hooksCount += countHooksInFile(userSettings);
|
|
7565
|
-
const userClaudeJson =
|
|
7681
|
+
const userClaudeJson = path32.join(homeDir2, ".claude.json");
|
|
7566
7682
|
for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
|
|
7567
7683
|
for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
|
|
7568
7684
|
userMcpServers.delete(name);
|
|
7569
7685
|
}
|
|
7570
7686
|
if (cwd) {
|
|
7571
|
-
if (
|
|
7572
|
-
if (
|
|
7573
|
-
const projectClaudeDir =
|
|
7687
|
+
if (fs29.existsSync(path32.join(cwd, "CLAUDE.md"))) claudeMdCount++;
|
|
7688
|
+
if (fs29.existsSync(path32.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
|
|
7689
|
+
const projectClaudeDir = path32.join(cwd, ".claude");
|
|
7574
7690
|
const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
|
|
7575
7691
|
if (!overlapsUserScope) {
|
|
7576
|
-
if (
|
|
7577
|
-
rulesCount += countRulesInDir(
|
|
7578
|
-
const projSettings =
|
|
7692
|
+
if (fs29.existsSync(path32.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
7693
|
+
rulesCount += countRulesInDir(path32.join(projectClaudeDir, "rules"));
|
|
7694
|
+
const projSettings = path32.join(projectClaudeDir, "settings.json");
|
|
7579
7695
|
for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
|
|
7580
7696
|
hooksCount += countHooksInFile(projSettings);
|
|
7581
7697
|
}
|
|
7582
|
-
if (
|
|
7583
|
-
const localSettings =
|
|
7698
|
+
if (fs29.existsSync(path32.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
|
|
7699
|
+
const localSettings = path32.join(projectClaudeDir, "settings.local.json");
|
|
7584
7700
|
for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
|
|
7585
7701
|
hooksCount += countHooksInFile(localSettings);
|
|
7586
|
-
const mcpJsonServers = getMcpServerNames(
|
|
7702
|
+
const mcpJsonServers = getMcpServerNames(path32.join(cwd, ".mcp.json"));
|
|
7587
7703
|
const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
|
|
7588
7704
|
for (const name of disabledMcpJson) mcpJsonServers.delete(name);
|
|
7589
7705
|
for (const name of mcpJsonServers) projectMcpServers.add(name);
|
|
@@ -7616,12 +7732,12 @@ function readActiveShieldsHud() {
|
|
|
7616
7732
|
return shieldsCache.value;
|
|
7617
7733
|
}
|
|
7618
7734
|
try {
|
|
7619
|
-
const shieldsPath =
|
|
7620
|
-
if (!
|
|
7735
|
+
const shieldsPath = path32.join(os25.homedir(), ".node9", "shields.json");
|
|
7736
|
+
if (!fs29.existsSync(shieldsPath)) {
|
|
7621
7737
|
shieldsCache = { value: [], ts: now };
|
|
7622
7738
|
return [];
|
|
7623
7739
|
}
|
|
7624
|
-
const parsed = JSON.parse(
|
|
7740
|
+
const parsed = JSON.parse(fs29.readFileSync(shieldsPath, "utf-8"));
|
|
7625
7741
|
if (!Array.isArray(parsed.active)) {
|
|
7626
7742
|
shieldsCache = { value: [], ts: now };
|
|
7627
7743
|
return [];
|
|
@@ -7707,15 +7823,15 @@ function renderContextLine(stdin) {
|
|
|
7707
7823
|
}
|
|
7708
7824
|
const rl = stdin.rate_limits;
|
|
7709
7825
|
if (rl?.five_hour?.used_percentage !== void 0) {
|
|
7710
|
-
const
|
|
7711
|
-
const bar = progressBar(
|
|
7826
|
+
const pct2 = Math.round(rl.five_hour.used_percentage);
|
|
7827
|
+
const bar = progressBar(pct2, 60, 80);
|
|
7712
7828
|
const left = formatTimeLeft(rl.five_hour.resets_at);
|
|
7713
|
-
parts.push(`${dim("\u2502")} 5h ${bar} ${
|
|
7829
|
+
parts.push(`${dim("\u2502")} 5h ${bar} ${pct2}%${left}`);
|
|
7714
7830
|
}
|
|
7715
7831
|
if (rl?.seven_day?.used_percentage !== void 0) {
|
|
7716
|
-
const
|
|
7717
|
-
const bar = progressBar(
|
|
7718
|
-
parts.push(`${dim("\u2502")} 7d ${bar} ${
|
|
7832
|
+
const pct2 = Math.round(rl.seven_day.used_percentage);
|
|
7833
|
+
const bar = progressBar(pct2, 60, 80);
|
|
7834
|
+
parts.push(`${dim("\u2502")} 7d ${bar} ${pct2}%`);
|
|
7719
7835
|
}
|
|
7720
7836
|
if (parts.length === 0) return null;
|
|
7721
7837
|
return parts.join(" ");
|
|
@@ -7723,17 +7839,17 @@ function renderContextLine(stdin) {
|
|
|
7723
7839
|
async function main() {
|
|
7724
7840
|
try {
|
|
7725
7841
|
const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
|
|
7726
|
-
if (
|
|
7842
|
+
if (fs29.existsSync(path32.join(os25.homedir(), ".node9", "hud-debug"))) {
|
|
7727
7843
|
try {
|
|
7728
|
-
const logPath =
|
|
7844
|
+
const logPath = path32.join(os25.homedir(), ".node9", "hud-debug.log");
|
|
7729
7845
|
const MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
7730
7846
|
let size = 0;
|
|
7731
7847
|
try {
|
|
7732
|
-
size =
|
|
7848
|
+
size = fs29.statSync(logPath).size;
|
|
7733
7849
|
} catch {
|
|
7734
7850
|
}
|
|
7735
7851
|
if (size < MAX_LOG_SIZE) {
|
|
7736
|
-
|
|
7852
|
+
fs29.appendFileSync(
|
|
7737
7853
|
logPath,
|
|
7738
7854
|
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
|
|
7739
7855
|
);
|
|
@@ -7754,11 +7870,11 @@ async function main() {
|
|
|
7754
7870
|
try {
|
|
7755
7871
|
const cwd = stdin.cwd ?? process.cwd();
|
|
7756
7872
|
for (const configPath of [
|
|
7757
|
-
|
|
7758
|
-
|
|
7873
|
+
path32.join(cwd, "node9.config.json"),
|
|
7874
|
+
path32.join(os25.homedir(), ".node9", "config.json")
|
|
7759
7875
|
]) {
|
|
7760
|
-
if (!
|
|
7761
|
-
const cfg = JSON.parse(
|
|
7876
|
+
if (!fs29.existsSync(configPath)) continue;
|
|
7877
|
+
const cfg = JSON.parse(fs29.readFileSync(configPath, "utf-8"));
|
|
7762
7878
|
const hud = cfg.settings?.hud;
|
|
7763
7879
|
if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
|
|
7764
7880
|
}
|
|
@@ -7804,9 +7920,9 @@ init_core();
|
|
|
7804
7920
|
import { Command } from "commander";
|
|
7805
7921
|
|
|
7806
7922
|
// src/setup.ts
|
|
7807
|
-
import
|
|
7808
|
-
import
|
|
7809
|
-
import
|
|
7923
|
+
import fs12 from "fs";
|
|
7924
|
+
import path15 from "path";
|
|
7925
|
+
import os11 from "os";
|
|
7810
7926
|
import chalk from "chalk";
|
|
7811
7927
|
import { confirm } from "@inquirer/prompts";
|
|
7812
7928
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
@@ -7834,26 +7950,26 @@ function fullPathCommand(subcommand) {
|
|
|
7834
7950
|
}
|
|
7835
7951
|
function readJson(filePath) {
|
|
7836
7952
|
try {
|
|
7837
|
-
if (
|
|
7838
|
-
return JSON.parse(
|
|
7953
|
+
if (fs12.existsSync(filePath)) {
|
|
7954
|
+
return JSON.parse(fs12.readFileSync(filePath, "utf-8"));
|
|
7839
7955
|
}
|
|
7840
7956
|
} catch {
|
|
7841
7957
|
}
|
|
7842
7958
|
return null;
|
|
7843
7959
|
}
|
|
7844
7960
|
function writeJson(filePath, data) {
|
|
7845
|
-
const dir =
|
|
7846
|
-
if (!
|
|
7847
|
-
|
|
7961
|
+
const dir = path15.dirname(filePath);
|
|
7962
|
+
if (!fs12.existsSync(dir)) fs12.mkdirSync(dir, { recursive: true });
|
|
7963
|
+
fs12.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
7848
7964
|
}
|
|
7849
7965
|
function isNode9Hook(cmd) {
|
|
7850
7966
|
if (!cmd) return false;
|
|
7851
7967
|
return /(?:^|[\s/\\])node9 (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js (?:check|log)/.test(cmd);
|
|
7852
7968
|
}
|
|
7853
7969
|
function teardownClaude() {
|
|
7854
|
-
const homeDir2 =
|
|
7855
|
-
const hooksPath =
|
|
7856
|
-
const mcpPath =
|
|
7970
|
+
const homeDir2 = os11.homedir();
|
|
7971
|
+
const hooksPath = path15.join(homeDir2, ".claude", "settings.json");
|
|
7972
|
+
const mcpPath = path15.join(homeDir2, ".claude", ".mcp.json");
|
|
7857
7973
|
let changed = false;
|
|
7858
7974
|
const settings = readJson(hooksPath);
|
|
7859
7975
|
if (settings?.hooks) {
|
|
@@ -7901,8 +8017,8 @@ function teardownClaude() {
|
|
|
7901
8017
|
}
|
|
7902
8018
|
}
|
|
7903
8019
|
function teardownGemini() {
|
|
7904
|
-
const homeDir2 =
|
|
7905
|
-
const settingsPath =
|
|
8020
|
+
const homeDir2 = os11.homedir();
|
|
8021
|
+
const settingsPath = path15.join(homeDir2, ".gemini", "settings.json");
|
|
7906
8022
|
const settings = readJson(settingsPath);
|
|
7907
8023
|
if (!settings) {
|
|
7908
8024
|
console.log(chalk.blue(" \u2139\uFE0F ~/.gemini/settings.json not found \u2014 nothing to remove"));
|
|
@@ -7945,8 +8061,8 @@ function teardownGemini() {
|
|
|
7945
8061
|
}
|
|
7946
8062
|
}
|
|
7947
8063
|
function teardownCursor() {
|
|
7948
|
-
const homeDir2 =
|
|
7949
|
-
const mcpPath =
|
|
8064
|
+
const homeDir2 = os11.homedir();
|
|
8065
|
+
const mcpPath = path15.join(homeDir2, ".cursor", "mcp.json");
|
|
7950
8066
|
const mcpConfig = readJson(mcpPath);
|
|
7951
8067
|
if (!mcpConfig?.mcpServers) {
|
|
7952
8068
|
console.log(chalk.blue(" \u2139\uFE0F ~/.cursor/mcp.json not found \u2014 nothing to remove"));
|
|
@@ -7977,9 +8093,9 @@ function teardownCursor() {
|
|
|
7977
8093
|
}
|
|
7978
8094
|
}
|
|
7979
8095
|
async function setupClaude() {
|
|
7980
|
-
const homeDir2 =
|
|
7981
|
-
const mcpPath =
|
|
7982
|
-
const hooksPath =
|
|
8096
|
+
const homeDir2 = os11.homedir();
|
|
8097
|
+
const mcpPath = path15.join(homeDir2, ".claude", ".mcp.json");
|
|
8098
|
+
const hooksPath = path15.join(homeDir2, ".claude", "settings.json");
|
|
7983
8099
|
const claudeConfig = readJson(mcpPath) ?? {};
|
|
7984
8100
|
const settings = readJson(hooksPath) ?? {};
|
|
7985
8101
|
const servers = claudeConfig.mcpServers ?? {};
|
|
@@ -8076,8 +8192,8 @@ async function setupClaude() {
|
|
|
8076
8192
|
}
|
|
8077
8193
|
}
|
|
8078
8194
|
async function setupGemini() {
|
|
8079
|
-
const homeDir2 =
|
|
8080
|
-
const settingsPath =
|
|
8195
|
+
const homeDir2 = os11.homedir();
|
|
8196
|
+
const settingsPath = path15.join(homeDir2, ".gemini", "settings.json");
|
|
8081
8197
|
const settings = readJson(settingsPath) ?? {};
|
|
8082
8198
|
const servers = settings.mcpServers ?? {};
|
|
8083
8199
|
let hooksChanged = false;
|
|
@@ -8172,10 +8288,10 @@ async function setupGemini() {
|
|
|
8172
8288
|
printDaemonTip();
|
|
8173
8289
|
}
|
|
8174
8290
|
}
|
|
8175
|
-
function detectAgents(homeDir2 =
|
|
8291
|
+
function detectAgents(homeDir2 = os11.homedir()) {
|
|
8176
8292
|
const exists = (p) => {
|
|
8177
8293
|
try {
|
|
8178
|
-
return
|
|
8294
|
+
return fs12.existsSync(p);
|
|
8179
8295
|
} catch (err2) {
|
|
8180
8296
|
const code = err2.code;
|
|
8181
8297
|
if (code !== "ENOENT") {
|
|
@@ -8186,15 +8302,15 @@ function detectAgents(homeDir2 = os10.homedir()) {
|
|
|
8186
8302
|
}
|
|
8187
8303
|
};
|
|
8188
8304
|
return {
|
|
8189
|
-
claude: exists(
|
|
8190
|
-
gemini: exists(
|
|
8191
|
-
cursor: exists(
|
|
8192
|
-
codex: exists(
|
|
8305
|
+
claude: exists(path15.join(homeDir2, ".claude")) || exists(path15.join(homeDir2, ".claude.json")),
|
|
8306
|
+
gemini: exists(path15.join(homeDir2, ".gemini")),
|
|
8307
|
+
cursor: exists(path15.join(homeDir2, ".cursor")),
|
|
8308
|
+
codex: exists(path15.join(homeDir2, ".codex"))
|
|
8193
8309
|
};
|
|
8194
8310
|
}
|
|
8195
8311
|
async function setupCursor() {
|
|
8196
|
-
const homeDir2 =
|
|
8197
|
-
const mcpPath =
|
|
8312
|
+
const homeDir2 = os11.homedir();
|
|
8313
|
+
const mcpPath = path15.join(homeDir2, ".cursor", "mcp.json");
|
|
8198
8314
|
const mcpConfig = readJson(mcpPath) ?? {};
|
|
8199
8315
|
const servers = mcpConfig.mcpServers ?? {};
|
|
8200
8316
|
let anythingChanged = false;
|
|
@@ -8260,21 +8376,21 @@ async function setupCursor() {
|
|
|
8260
8376
|
}
|
|
8261
8377
|
function readToml(filePath) {
|
|
8262
8378
|
try {
|
|
8263
|
-
if (
|
|
8264
|
-
return parseToml(
|
|
8379
|
+
if (fs12.existsSync(filePath)) {
|
|
8380
|
+
return parseToml(fs12.readFileSync(filePath, "utf-8"));
|
|
8265
8381
|
}
|
|
8266
8382
|
} catch {
|
|
8267
8383
|
}
|
|
8268
8384
|
return null;
|
|
8269
8385
|
}
|
|
8270
8386
|
function writeToml(filePath, data) {
|
|
8271
|
-
const dir =
|
|
8272
|
-
if (!
|
|
8273
|
-
|
|
8387
|
+
const dir = path15.dirname(filePath);
|
|
8388
|
+
if (!fs12.existsSync(dir)) fs12.mkdirSync(dir, { recursive: true });
|
|
8389
|
+
fs12.writeFileSync(filePath, stringifyToml(data));
|
|
8274
8390
|
}
|
|
8275
8391
|
async function setupCodex() {
|
|
8276
|
-
const homeDir2 =
|
|
8277
|
-
const configPath =
|
|
8392
|
+
const homeDir2 = os11.homedir();
|
|
8393
|
+
const configPath = path15.join(homeDir2, ".codex", "config.toml");
|
|
8278
8394
|
const config = readToml(configPath) ?? {};
|
|
8279
8395
|
const servers = config.mcp_servers ?? {};
|
|
8280
8396
|
let anythingChanged = false;
|
|
@@ -8339,8 +8455,8 @@ async function setupCodex() {
|
|
|
8339
8455
|
}
|
|
8340
8456
|
}
|
|
8341
8457
|
function setupHud() {
|
|
8342
|
-
const homeDir2 =
|
|
8343
|
-
const hooksPath =
|
|
8458
|
+
const homeDir2 = os11.homedir();
|
|
8459
|
+
const hooksPath = path15.join(homeDir2, ".claude", "settings.json");
|
|
8344
8460
|
const settings = readJson(hooksPath) ?? {};
|
|
8345
8461
|
const hudCommand = fullPathCommand("hud");
|
|
8346
8462
|
const statusLineObj = { type: "command", command: hudCommand };
|
|
@@ -8366,8 +8482,8 @@ function setupHud() {
|
|
|
8366
8482
|
console.log(chalk.gray(" Restart Claude Code to activate."));
|
|
8367
8483
|
}
|
|
8368
8484
|
function teardownHud() {
|
|
8369
|
-
const homeDir2 =
|
|
8370
|
-
const hooksPath =
|
|
8485
|
+
const homeDir2 = os11.homedir();
|
|
8486
|
+
const hooksPath = path15.join(homeDir2, ".claude", "settings.json");
|
|
8371
8487
|
const settings = readJson(hooksPath);
|
|
8372
8488
|
if (!settings) {
|
|
8373
8489
|
console.log(chalk.blue(" \u2139\uFE0F ~/.claude/settings.json not found \u2014 nothing to remove"));
|
|
@@ -8387,10 +8503,10 @@ function teardownHud() {
|
|
|
8387
8503
|
|
|
8388
8504
|
// src/cli.ts
|
|
8389
8505
|
init_daemon2();
|
|
8390
|
-
import
|
|
8391
|
-
import
|
|
8392
|
-
import
|
|
8393
|
-
import
|
|
8506
|
+
import chalk20 from "chalk";
|
|
8507
|
+
import fs30 from "fs";
|
|
8508
|
+
import path33 from "path";
|
|
8509
|
+
import os26 from "os";
|
|
8394
8510
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
8395
8511
|
|
|
8396
8512
|
// src/utils/duration.ts
|
|
@@ -8619,19 +8735,19 @@ init_daemon();
|
|
|
8619
8735
|
init_config();
|
|
8620
8736
|
init_policy();
|
|
8621
8737
|
import chalk5 from "chalk";
|
|
8622
|
-
import
|
|
8738
|
+
import fs19 from "fs";
|
|
8623
8739
|
import { spawn as spawn6 } from "child_process";
|
|
8624
|
-
import
|
|
8625
|
-
import
|
|
8740
|
+
import path21 from "path";
|
|
8741
|
+
import os15 from "os";
|
|
8626
8742
|
|
|
8627
8743
|
// src/undo.ts
|
|
8628
8744
|
import { spawnSync as spawnSync4, spawn as spawn5 } from "child_process";
|
|
8629
|
-
import
|
|
8630
|
-
import
|
|
8745
|
+
import crypto3 from "crypto";
|
|
8746
|
+
import fs18 from "fs";
|
|
8631
8747
|
import net3 from "net";
|
|
8632
|
-
import
|
|
8633
|
-
import
|
|
8634
|
-
var ACTIVITY_SOCKET_PATH3 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
8748
|
+
import path20 from "path";
|
|
8749
|
+
import os14 from "os";
|
|
8750
|
+
var ACTIVITY_SOCKET_PATH3 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path20.join(os14.tmpdir(), "node9-activity.sock");
|
|
8635
8751
|
function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
|
|
8636
8752
|
try {
|
|
8637
8753
|
const payload = JSON.stringify({
|
|
@@ -8651,22 +8767,22 @@ function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
|
|
|
8651
8767
|
} catch {
|
|
8652
8768
|
}
|
|
8653
8769
|
}
|
|
8654
|
-
var SNAPSHOT_STACK_PATH =
|
|
8655
|
-
var UNDO_LATEST_PATH =
|
|
8770
|
+
var SNAPSHOT_STACK_PATH = path20.join(os14.homedir(), ".node9", "snapshots.json");
|
|
8771
|
+
var UNDO_LATEST_PATH = path20.join(os14.homedir(), ".node9", "undo_latest.txt");
|
|
8656
8772
|
var MAX_SNAPSHOTS = 10;
|
|
8657
8773
|
var GIT_TIMEOUT = 15e3;
|
|
8658
8774
|
function readStack() {
|
|
8659
8775
|
try {
|
|
8660
|
-
if (
|
|
8661
|
-
return JSON.parse(
|
|
8776
|
+
if (fs18.existsSync(SNAPSHOT_STACK_PATH))
|
|
8777
|
+
return JSON.parse(fs18.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
|
|
8662
8778
|
} catch {
|
|
8663
8779
|
}
|
|
8664
8780
|
return [];
|
|
8665
8781
|
}
|
|
8666
8782
|
function writeStack(stack) {
|
|
8667
|
-
const dir =
|
|
8668
|
-
if (!
|
|
8669
|
-
|
|
8783
|
+
const dir = path20.dirname(SNAPSHOT_STACK_PATH);
|
|
8784
|
+
if (!fs18.existsSync(dir)) fs18.mkdirSync(dir, { recursive: true });
|
|
8785
|
+
fs18.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
8670
8786
|
}
|
|
8671
8787
|
function extractFilePath(args) {
|
|
8672
8788
|
if (!args || typeof args !== "object") return null;
|
|
@@ -8686,12 +8802,12 @@ function buildArgsSummary(tool, args) {
|
|
|
8686
8802
|
return "";
|
|
8687
8803
|
}
|
|
8688
8804
|
function findProjectRoot(filePath) {
|
|
8689
|
-
let dir =
|
|
8805
|
+
let dir = path20.dirname(filePath);
|
|
8690
8806
|
while (true) {
|
|
8691
|
-
if (
|
|
8807
|
+
if (fs18.existsSync(path20.join(dir, ".git")) || fs18.existsSync(path20.join(dir, "package.json"))) {
|
|
8692
8808
|
return dir;
|
|
8693
8809
|
}
|
|
8694
|
-
const parent =
|
|
8810
|
+
const parent = path20.dirname(dir);
|
|
8695
8811
|
if (parent === dir) return process.cwd();
|
|
8696
8812
|
dir = parent;
|
|
8697
8813
|
}
|
|
@@ -8699,7 +8815,7 @@ function findProjectRoot(filePath) {
|
|
|
8699
8815
|
function normalizeCwdForHash(cwd) {
|
|
8700
8816
|
let normalized;
|
|
8701
8817
|
try {
|
|
8702
|
-
normalized =
|
|
8818
|
+
normalized = fs18.realpathSync(cwd);
|
|
8703
8819
|
} catch {
|
|
8704
8820
|
normalized = cwd;
|
|
8705
8821
|
}
|
|
@@ -8708,17 +8824,17 @@ function normalizeCwdForHash(cwd) {
|
|
|
8708
8824
|
return normalized;
|
|
8709
8825
|
}
|
|
8710
8826
|
function getShadowRepoDir(cwd) {
|
|
8711
|
-
const hash =
|
|
8712
|
-
return
|
|
8827
|
+
const hash = crypto3.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
|
|
8828
|
+
return path20.join(os14.homedir(), ".node9", "snapshots", hash);
|
|
8713
8829
|
}
|
|
8714
8830
|
function cleanOrphanedIndexFiles(shadowDir) {
|
|
8715
8831
|
try {
|
|
8716
8832
|
const cutoff = Date.now() - 6e4;
|
|
8717
|
-
for (const f of
|
|
8833
|
+
for (const f of fs18.readdirSync(shadowDir)) {
|
|
8718
8834
|
if (f.startsWith("index_")) {
|
|
8719
|
-
const fp =
|
|
8835
|
+
const fp = path20.join(shadowDir, f);
|
|
8720
8836
|
try {
|
|
8721
|
-
if (
|
|
8837
|
+
if (fs18.statSync(fp).mtimeMs < cutoff) fs18.unlinkSync(fp);
|
|
8722
8838
|
} catch {
|
|
8723
8839
|
}
|
|
8724
8840
|
}
|
|
@@ -8730,7 +8846,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
|
|
|
8730
8846
|
const hardcoded = [".git", ".node9"];
|
|
8731
8847
|
const lines = [...hardcoded, ...ignorePaths].join("\n");
|
|
8732
8848
|
try {
|
|
8733
|
-
|
|
8849
|
+
fs18.writeFileSync(path20.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
|
|
8734
8850
|
} catch {
|
|
8735
8851
|
}
|
|
8736
8852
|
}
|
|
@@ -8743,25 +8859,25 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
8743
8859
|
timeout: 3e3
|
|
8744
8860
|
});
|
|
8745
8861
|
if (check.status === 0) {
|
|
8746
|
-
const ptPath =
|
|
8862
|
+
const ptPath = path20.join(shadowDir, "project-path.txt");
|
|
8747
8863
|
try {
|
|
8748
|
-
const stored =
|
|
8864
|
+
const stored = fs18.readFileSync(ptPath, "utf8").trim();
|
|
8749
8865
|
if (stored === normalizedCwd) return true;
|
|
8750
8866
|
if (process.env.NODE9_DEBUG === "1")
|
|
8751
8867
|
console.error(
|
|
8752
8868
|
`[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
|
|
8753
8869
|
);
|
|
8754
|
-
|
|
8870
|
+
fs18.rmSync(shadowDir, { recursive: true, force: true });
|
|
8755
8871
|
} catch {
|
|
8756
8872
|
try {
|
|
8757
|
-
|
|
8873
|
+
fs18.writeFileSync(ptPath, normalizedCwd, "utf8");
|
|
8758
8874
|
} catch {
|
|
8759
8875
|
}
|
|
8760
8876
|
return true;
|
|
8761
8877
|
}
|
|
8762
8878
|
}
|
|
8763
8879
|
try {
|
|
8764
|
-
|
|
8880
|
+
fs18.mkdirSync(shadowDir, { recursive: true });
|
|
8765
8881
|
} catch {
|
|
8766
8882
|
}
|
|
8767
8883
|
const init = spawnSync4("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
|
|
@@ -8770,7 +8886,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
8770
8886
|
if (process.env.NODE9_DEBUG === "1") console.error("[Node9] git init --bare failed:", reason);
|
|
8771
8887
|
return false;
|
|
8772
8888
|
}
|
|
8773
|
-
const configFile =
|
|
8889
|
+
const configFile = path20.join(shadowDir, "config");
|
|
8774
8890
|
spawnSync4("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
|
|
8775
8891
|
timeout: 3e3
|
|
8776
8892
|
});
|
|
@@ -8778,7 +8894,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
8778
8894
|
timeout: 3e3
|
|
8779
8895
|
});
|
|
8780
8896
|
try {
|
|
8781
|
-
|
|
8897
|
+
fs18.writeFileSync(path20.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
|
|
8782
8898
|
} catch {
|
|
8783
8899
|
}
|
|
8784
8900
|
return true;
|
|
@@ -8798,12 +8914,12 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
8798
8914
|
let indexFile = null;
|
|
8799
8915
|
try {
|
|
8800
8916
|
const rawFilePath = extractFilePath(args);
|
|
8801
|
-
const absFilePath = rawFilePath &&
|
|
8917
|
+
const absFilePath = rawFilePath && path20.isAbsolute(rawFilePath) ? rawFilePath : null;
|
|
8802
8918
|
const cwd = absFilePath ? findProjectRoot(absFilePath) : process.cwd();
|
|
8803
8919
|
const shadowDir = getShadowRepoDir(cwd);
|
|
8804
8920
|
if (!ensureShadowRepo(shadowDir, cwd)) return null;
|
|
8805
8921
|
writeShadowExcludes(shadowDir, ignorePaths);
|
|
8806
|
-
indexFile =
|
|
8922
|
+
indexFile = path20.join(shadowDir, `index_${process.pid}_${Date.now()}`);
|
|
8807
8923
|
const shadowEnv = {
|
|
8808
8924
|
...process.env,
|
|
8809
8925
|
GIT_DIR: shadowDir,
|
|
@@ -8875,7 +8991,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
8875
8991
|
writeStack(stack);
|
|
8876
8992
|
const entry = stack[stack.length - 1];
|
|
8877
8993
|
notifySnapshotTaken(commitHash.slice(0, 7), tool, entry.argsSummary, capturedFiles.length);
|
|
8878
|
-
|
|
8994
|
+
fs18.writeFileSync(UNDO_LATEST_PATH, commitHash);
|
|
8879
8995
|
if (shouldGc) {
|
|
8880
8996
|
spawn5("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
|
|
8881
8997
|
}
|
|
@@ -8886,7 +9002,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
8886
9002
|
} finally {
|
|
8887
9003
|
if (indexFile) {
|
|
8888
9004
|
try {
|
|
8889
|
-
|
|
9005
|
+
fs18.unlinkSync(indexFile);
|
|
8890
9006
|
} catch {
|
|
8891
9007
|
}
|
|
8892
9008
|
}
|
|
@@ -8962,9 +9078,9 @@ function applyUndo(hash, cwd) {
|
|
|
8962
9078
|
timeout: GIT_TIMEOUT
|
|
8963
9079
|
}).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
8964
9080
|
for (const file of [...tracked, ...untracked]) {
|
|
8965
|
-
const fullPath =
|
|
8966
|
-
if (!snapshotFiles.has(file) &&
|
|
8967
|
-
|
|
9081
|
+
const fullPath = path20.join(dir, file);
|
|
9082
|
+
if (!snapshotFiles.has(file) && fs18.existsSync(fullPath)) {
|
|
9083
|
+
fs18.unlinkSync(fullPath);
|
|
8968
9084
|
}
|
|
8969
9085
|
}
|
|
8970
9086
|
return true;
|
|
@@ -8988,9 +9104,9 @@ function registerCheckCommand(program2) {
|
|
|
8988
9104
|
} catch (err2) {
|
|
8989
9105
|
const tempConfig = getConfig();
|
|
8990
9106
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
8991
|
-
const logPath =
|
|
9107
|
+
const logPath = path21.join(os15.homedir(), ".node9", "hook-debug.log");
|
|
8992
9108
|
const errMsg = err2 instanceof Error ? err2.message : String(err2);
|
|
8993
|
-
|
|
9109
|
+
fs19.appendFileSync(
|
|
8994
9110
|
logPath,
|
|
8995
9111
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
8996
9112
|
RAW: ${raw}
|
|
@@ -9003,10 +9119,10 @@ RAW: ${raw}
|
|
|
9003
9119
|
if (config.settings.autoStartDaemon && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON) {
|
|
9004
9120
|
try {
|
|
9005
9121
|
const scriptPath = process.argv[1];
|
|
9006
|
-
if (typeof scriptPath !== "string" || !
|
|
9122
|
+
if (typeof scriptPath !== "string" || !path21.isAbsolute(scriptPath))
|
|
9007
9123
|
throw new Error("node9: argv[1] is not an absolute path");
|
|
9008
|
-
const resolvedScript =
|
|
9009
|
-
const expectedCli =
|
|
9124
|
+
const resolvedScript = fs19.realpathSync(scriptPath);
|
|
9125
|
+
const expectedCli = fs19.realpathSync(path21.resolve(__dirname, "../../cli.js"));
|
|
9010
9126
|
if (resolvedScript !== expectedCli)
|
|
9011
9127
|
throw new Error(
|
|
9012
9128
|
"node9: daemon spawn aborted \u2014 argv[1] does not resolve to the node9 CLI"
|
|
@@ -9032,10 +9148,10 @@ RAW: ${raw}
|
|
|
9032
9148
|
}
|
|
9033
9149
|
}
|
|
9034
9150
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
9035
|
-
const logPath =
|
|
9036
|
-
if (!
|
|
9037
|
-
|
|
9038
|
-
|
|
9151
|
+
const logPath = path21.join(os15.homedir(), ".node9", "hook-debug.log");
|
|
9152
|
+
if (!fs19.existsSync(path21.dirname(logPath)))
|
|
9153
|
+
fs19.mkdirSync(path21.dirname(logPath), { recursive: true });
|
|
9154
|
+
fs19.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
9039
9155
|
`);
|
|
9040
9156
|
}
|
|
9041
9157
|
const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
|
|
@@ -9048,8 +9164,8 @@ RAW: ${raw}
|
|
|
9048
9164
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
9049
9165
|
let ttyFd = null;
|
|
9050
9166
|
try {
|
|
9051
|
-
ttyFd =
|
|
9052
|
-
const writeTty = (line) =>
|
|
9167
|
+
ttyFd = fs19.openSync("/dev/tty", "w");
|
|
9168
|
+
const writeTty = (line) => fs19.writeSync(ttyFd, line + "\n");
|
|
9053
9169
|
if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
|
|
9054
9170
|
writeTty(chalk5.bgRed.white.bold(`
|
|
9055
9171
|
\u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
@@ -9068,7 +9184,7 @@ RAW: ${raw}
|
|
|
9068
9184
|
} finally {
|
|
9069
9185
|
if (ttyFd !== null)
|
|
9070
9186
|
try {
|
|
9071
|
-
|
|
9187
|
+
fs19.closeSync(ttyFd);
|
|
9072
9188
|
} catch {
|
|
9073
9189
|
}
|
|
9074
9190
|
}
|
|
@@ -9100,7 +9216,7 @@ RAW: ${raw}
|
|
|
9100
9216
|
if (shouldSnapshot(toolName, toolInput, config)) {
|
|
9101
9217
|
await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
|
|
9102
9218
|
}
|
|
9103
|
-
const safeCwdForAuth = typeof payload.cwd === "string" &&
|
|
9219
|
+
const safeCwdForAuth = typeof payload.cwd === "string" && path21.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
9104
9220
|
const result = await authorizeHeadless(toolName, toolInput, meta, {
|
|
9105
9221
|
cwd: safeCwdForAuth
|
|
9106
9222
|
});
|
|
@@ -9112,12 +9228,12 @@ RAW: ${raw}
|
|
|
9112
9228
|
}
|
|
9113
9229
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
9114
9230
|
try {
|
|
9115
|
-
const tty =
|
|
9116
|
-
|
|
9231
|
+
const tty = fs19.openSync("/dev/tty", "w");
|
|
9232
|
+
fs19.writeSync(
|
|
9117
9233
|
tty,
|
|
9118
9234
|
chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
|
|
9119
9235
|
);
|
|
9120
|
-
|
|
9236
|
+
fs19.closeSync(tty);
|
|
9121
9237
|
} catch {
|
|
9122
9238
|
}
|
|
9123
9239
|
const daemonReady = await autoStartDaemonAndWait();
|
|
@@ -9144,9 +9260,9 @@ RAW: ${raw}
|
|
|
9144
9260
|
});
|
|
9145
9261
|
} catch (err2) {
|
|
9146
9262
|
if (process.env.NODE9_DEBUG === "1") {
|
|
9147
|
-
const logPath =
|
|
9263
|
+
const logPath = path21.join(os15.homedir(), ".node9", "hook-debug.log");
|
|
9148
9264
|
const errMsg = err2 instanceof Error ? err2.message : String(err2);
|
|
9149
|
-
|
|
9265
|
+
fs19.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
9150
9266
|
`);
|
|
9151
9267
|
}
|
|
9152
9268
|
process.exit(0);
|
|
@@ -9183,9 +9299,9 @@ RAW: ${raw}
|
|
|
9183
9299
|
init_audit();
|
|
9184
9300
|
init_config();
|
|
9185
9301
|
init_policy();
|
|
9186
|
-
import
|
|
9187
|
-
import
|
|
9188
|
-
import
|
|
9302
|
+
import fs20 from "fs";
|
|
9303
|
+
import path22 from "path";
|
|
9304
|
+
import os16 from "os";
|
|
9189
9305
|
init_daemon();
|
|
9190
9306
|
|
|
9191
9307
|
// src/utils/cp-mv-parser.ts
|
|
@@ -9226,9 +9342,9 @@ function containsShellMetachar(token) {
|
|
|
9226
9342
|
}
|
|
9227
9343
|
|
|
9228
9344
|
// src/cli/commands/log.ts
|
|
9229
|
-
var
|
|
9345
|
+
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
9346
|
function detectTestResult(command, output) {
|
|
9231
|
-
if (!
|
|
9347
|
+
if (!TEST_COMMAND_RE2.test(command)) return null;
|
|
9232
9348
|
const out = output.toLowerCase();
|
|
9233
9349
|
if (/\b(tests?\s+passed|all\s+tests?\s+passed|passing|test\s+suites?.*passed|ok\b|\d+\s+passed)/i.test(
|
|
9234
9350
|
out
|
|
@@ -9258,10 +9374,10 @@ function registerLogCommand(program2) {
|
|
|
9258
9374
|
decision: "allowed",
|
|
9259
9375
|
source: "post-hook"
|
|
9260
9376
|
};
|
|
9261
|
-
const logPath =
|
|
9262
|
-
if (!
|
|
9263
|
-
|
|
9264
|
-
|
|
9377
|
+
const logPath = path22.join(os16.homedir(), ".node9", "audit.log");
|
|
9378
|
+
if (!fs20.existsSync(path22.dirname(logPath)))
|
|
9379
|
+
fs20.mkdirSync(path22.dirname(logPath), { recursive: true });
|
|
9380
|
+
fs20.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
9265
9381
|
if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
|
|
9266
9382
|
const command = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
|
|
9267
9383
|
if (command) {
|
|
@@ -9277,16 +9393,24 @@ function registerLogCommand(program2) {
|
|
|
9277
9393
|
if (bashCommand && output) {
|
|
9278
9394
|
const testResult = detectTestResult(bashCommand, output);
|
|
9279
9395
|
if (testResult) {
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
ts: Date.now(),
|
|
9396
|
+
appendToLog(LOCAL_AUDIT_LOG, {
|
|
9397
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9283
9398
|
tool,
|
|
9284
|
-
|
|
9399
|
+
testResult,
|
|
9400
|
+
source: "test-result"
|
|
9285
9401
|
});
|
|
9402
|
+
if (isDaemonRunning()) {
|
|
9403
|
+
await notifyActivitySocket({
|
|
9404
|
+
id: "test-result",
|
|
9405
|
+
ts: Date.now(),
|
|
9406
|
+
tool,
|
|
9407
|
+
status: testResult === "pass" ? "test_pass" : "test_fail"
|
|
9408
|
+
});
|
|
9409
|
+
}
|
|
9286
9410
|
}
|
|
9287
9411
|
}
|
|
9288
9412
|
}
|
|
9289
|
-
const safeCwd = typeof payload.cwd === "string" &&
|
|
9413
|
+
const safeCwd = typeof payload.cwd === "string" && path22.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
9290
9414
|
const config = getConfig(safeCwd);
|
|
9291
9415
|
if (shouldSnapshot(tool, {}, config)) {
|
|
9292
9416
|
await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
|
|
@@ -9295,9 +9419,9 @@ function registerLogCommand(program2) {
|
|
|
9295
9419
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
9296
9420
|
process.stderr.write(`[Node9] audit log error: ${msg}
|
|
9297
9421
|
`);
|
|
9298
|
-
const debugPath =
|
|
9422
|
+
const debugPath = path22.join(os16.homedir(), ".node9", "hook-debug.log");
|
|
9299
9423
|
try {
|
|
9300
|
-
|
|
9424
|
+
fs20.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
|
|
9301
9425
|
`);
|
|
9302
9426
|
} catch {
|
|
9303
9427
|
}
|
|
@@ -9698,13 +9822,13 @@ function registerConfigShowCommand(program2) {
|
|
|
9698
9822
|
// src/cli/commands/doctor.ts
|
|
9699
9823
|
init_daemon();
|
|
9700
9824
|
import chalk7 from "chalk";
|
|
9701
|
-
import
|
|
9702
|
-
import
|
|
9703
|
-
import
|
|
9825
|
+
import fs21 from "fs";
|
|
9826
|
+
import path23 from "path";
|
|
9827
|
+
import os17 from "os";
|
|
9704
9828
|
import { execSync as execSync2 } from "child_process";
|
|
9705
9829
|
function registerDoctorCommand(program2, version2) {
|
|
9706
9830
|
program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
9707
|
-
const homeDir2 =
|
|
9831
|
+
const homeDir2 = os17.homedir();
|
|
9708
9832
|
let failures = 0;
|
|
9709
9833
|
function pass(msg) {
|
|
9710
9834
|
console.log(chalk7.green(" \u2705 ") + msg);
|
|
@@ -9753,10 +9877,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9753
9877
|
);
|
|
9754
9878
|
}
|
|
9755
9879
|
section("Configuration");
|
|
9756
|
-
const globalConfigPath =
|
|
9757
|
-
if (
|
|
9880
|
+
const globalConfigPath = path23.join(homeDir2, ".node9", "config.json");
|
|
9881
|
+
if (fs21.existsSync(globalConfigPath)) {
|
|
9758
9882
|
try {
|
|
9759
|
-
JSON.parse(
|
|
9883
|
+
JSON.parse(fs21.readFileSync(globalConfigPath, "utf-8"));
|
|
9760
9884
|
pass("~/.node9/config.json found and valid");
|
|
9761
9885
|
} catch {
|
|
9762
9886
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -9764,10 +9888,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9764
9888
|
} else {
|
|
9765
9889
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
9766
9890
|
}
|
|
9767
|
-
const projectConfigPath =
|
|
9768
|
-
if (
|
|
9891
|
+
const projectConfigPath = path23.join(process.cwd(), "node9.config.json");
|
|
9892
|
+
if (fs21.existsSync(projectConfigPath)) {
|
|
9769
9893
|
try {
|
|
9770
|
-
JSON.parse(
|
|
9894
|
+
JSON.parse(fs21.readFileSync(projectConfigPath, "utf-8"));
|
|
9771
9895
|
pass("node9.config.json found and valid (project)");
|
|
9772
9896
|
} catch {
|
|
9773
9897
|
fail(
|
|
@@ -9776,8 +9900,8 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9776
9900
|
);
|
|
9777
9901
|
}
|
|
9778
9902
|
}
|
|
9779
|
-
const credsPath =
|
|
9780
|
-
if (
|
|
9903
|
+
const credsPath = path23.join(homeDir2, ".node9", "credentials.json");
|
|
9904
|
+
if (fs21.existsSync(credsPath)) {
|
|
9781
9905
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
9782
9906
|
} else {
|
|
9783
9907
|
warn(
|
|
@@ -9786,10 +9910,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9786
9910
|
);
|
|
9787
9911
|
}
|
|
9788
9912
|
section("Agent Hooks");
|
|
9789
|
-
const claudeSettingsPath =
|
|
9790
|
-
if (
|
|
9913
|
+
const claudeSettingsPath = path23.join(homeDir2, ".claude", "settings.json");
|
|
9914
|
+
if (fs21.existsSync(claudeSettingsPath)) {
|
|
9791
9915
|
try {
|
|
9792
|
-
const cs = JSON.parse(
|
|
9916
|
+
const cs = JSON.parse(fs21.readFileSync(claudeSettingsPath, "utf-8"));
|
|
9793
9917
|
const hasHook = cs.hooks?.PreToolUse?.some(
|
|
9794
9918
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
9795
9919
|
);
|
|
@@ -9805,10 +9929,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9805
9929
|
} else {
|
|
9806
9930
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
9807
9931
|
}
|
|
9808
|
-
const geminiSettingsPath =
|
|
9809
|
-
if (
|
|
9932
|
+
const geminiSettingsPath = path23.join(homeDir2, ".gemini", "settings.json");
|
|
9933
|
+
if (fs21.existsSync(geminiSettingsPath)) {
|
|
9810
9934
|
try {
|
|
9811
|
-
const gs = JSON.parse(
|
|
9935
|
+
const gs = JSON.parse(fs21.readFileSync(geminiSettingsPath, "utf-8"));
|
|
9812
9936
|
const hasHook = gs.hooks?.BeforeTool?.some(
|
|
9813
9937
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
9814
9938
|
);
|
|
@@ -9824,10 +9948,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9824
9948
|
} else {
|
|
9825
9949
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
9826
9950
|
}
|
|
9827
|
-
const cursorHooksPath =
|
|
9828
|
-
if (
|
|
9951
|
+
const cursorHooksPath = path23.join(homeDir2, ".cursor", "hooks.json");
|
|
9952
|
+
if (fs21.existsSync(cursorHooksPath)) {
|
|
9829
9953
|
try {
|
|
9830
|
-
const cur = JSON.parse(
|
|
9954
|
+
const cur = JSON.parse(fs21.readFileSync(cursorHooksPath, "utf-8"));
|
|
9831
9955
|
const hasHook = cur.hooks?.preToolUse?.some(
|
|
9832
9956
|
(h) => h.command?.includes("node9") || h.command?.includes("cli.js")
|
|
9833
9957
|
);
|
|
@@ -9865,9 +9989,9 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9865
9989
|
|
|
9866
9990
|
// src/cli/commands/audit.ts
|
|
9867
9991
|
import chalk8 from "chalk";
|
|
9868
|
-
import
|
|
9869
|
-
import
|
|
9870
|
-
import
|
|
9992
|
+
import fs22 from "fs";
|
|
9993
|
+
import path24 from "path";
|
|
9994
|
+
import os18 from "os";
|
|
9871
9995
|
function formatRelativeTime(timestamp) {
|
|
9872
9996
|
const diff = Date.now() - new Date(timestamp).getTime();
|
|
9873
9997
|
const sec = Math.floor(diff / 1e3);
|
|
@@ -9880,14 +10004,14 @@ function formatRelativeTime(timestamp) {
|
|
|
9880
10004
|
}
|
|
9881
10005
|
function registerAuditCommand(program2) {
|
|
9882
10006
|
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 (!
|
|
10007
|
+
const logPath = path24.join(os18.homedir(), ".node9", "audit.log");
|
|
10008
|
+
if (!fs22.existsSync(logPath)) {
|
|
9885
10009
|
console.log(
|
|
9886
10010
|
chalk8.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
9887
10011
|
);
|
|
9888
10012
|
return;
|
|
9889
10013
|
}
|
|
9890
|
-
const raw =
|
|
10014
|
+
const raw = fs22.readFileSync(logPath, "utf-8");
|
|
9891
10015
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
9892
10016
|
let entries = lines.flatMap((line) => {
|
|
9893
10017
|
try {
|
|
@@ -9939,10 +10063,398 @@ function registerAuditCommand(program2) {
|
|
|
9939
10063
|
});
|
|
9940
10064
|
}
|
|
9941
10065
|
|
|
10066
|
+
// src/cli/commands/report.ts
|
|
10067
|
+
import chalk9 from "chalk";
|
|
10068
|
+
import fs23 from "fs";
|
|
10069
|
+
import path25 from "path";
|
|
10070
|
+
import os19 from "os";
|
|
10071
|
+
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;
|
|
10072
|
+
function buildTestTimestamps(allEntries) {
|
|
10073
|
+
const testTs = /* @__PURE__ */ new Set();
|
|
10074
|
+
for (const e of allEntries) {
|
|
10075
|
+
if (e.source !== "post-hook") continue;
|
|
10076
|
+
if (e.tool !== "Bash" && e.tool !== "bash") continue;
|
|
10077
|
+
const cmd = e.args?.command;
|
|
10078
|
+
if (typeof cmd === "string" && TEST_COMMAND_RE3.test(cmd)) {
|
|
10079
|
+
testTs.add(new Date(e.ts).getTime());
|
|
10080
|
+
}
|
|
10081
|
+
}
|
|
10082
|
+
return testTs;
|
|
10083
|
+
}
|
|
10084
|
+
function isTestEntry(entry, testTs) {
|
|
10085
|
+
if (entry.tool !== "Bash" && entry.tool !== "bash") return false;
|
|
10086
|
+
if (entry.testRun === true) return true;
|
|
10087
|
+
const cmd = entry.args?.command;
|
|
10088
|
+
if (typeof cmd === "string") return TEST_COMMAND_RE3.test(cmd);
|
|
10089
|
+
const t = new Date(entry.ts).getTime();
|
|
10090
|
+
for (const ts of testTs) {
|
|
10091
|
+
if (Math.abs(ts - t) <= 3e3) return true;
|
|
10092
|
+
}
|
|
10093
|
+
return false;
|
|
10094
|
+
}
|
|
10095
|
+
function getDateRange(period) {
|
|
10096
|
+
const now = /* @__PURE__ */ new Date();
|
|
10097
|
+
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
10098
|
+
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
|
|
10099
|
+
switch (period) {
|
|
10100
|
+
case "today":
|
|
10101
|
+
return { start: todayStart, end };
|
|
10102
|
+
case "7d": {
|
|
10103
|
+
const s = new Date(todayStart);
|
|
10104
|
+
s.setDate(s.getDate() - 6);
|
|
10105
|
+
return { start: s, end };
|
|
10106
|
+
}
|
|
10107
|
+
case "30d": {
|
|
10108
|
+
const s = new Date(todayStart);
|
|
10109
|
+
s.setDate(s.getDate() - 29);
|
|
10110
|
+
return { start: s, end };
|
|
10111
|
+
}
|
|
10112
|
+
case "month":
|
|
10113
|
+
return { start: new Date(now.getFullYear(), now.getMonth(), 1), end };
|
|
10114
|
+
}
|
|
10115
|
+
}
|
|
10116
|
+
function parseAuditLog(logPath) {
|
|
10117
|
+
if (!fs23.existsSync(logPath)) return [];
|
|
10118
|
+
const raw = fs23.readFileSync(logPath, "utf-8");
|
|
10119
|
+
return raw.split("\n").flatMap((line) => {
|
|
10120
|
+
if (!line.trim()) return [];
|
|
10121
|
+
try {
|
|
10122
|
+
return [JSON.parse(line)];
|
|
10123
|
+
} catch {
|
|
10124
|
+
return [];
|
|
10125
|
+
}
|
|
10126
|
+
});
|
|
10127
|
+
}
|
|
10128
|
+
function isAllow(decision) {
|
|
10129
|
+
return decision.startsWith("allow");
|
|
10130
|
+
}
|
|
10131
|
+
function isDlp(checkedBy) {
|
|
10132
|
+
return !!checkedBy?.includes("dlp");
|
|
10133
|
+
}
|
|
10134
|
+
function barStr(value, max, width) {
|
|
10135
|
+
if (max === 0 || width <= 0) return "\u2591".repeat(width);
|
|
10136
|
+
const filled = Math.max(1, Math.round(value / max * width));
|
|
10137
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
|
|
10138
|
+
}
|
|
10139
|
+
function colorBar(value, max, width) {
|
|
10140
|
+
const s = barStr(value, max, width);
|
|
10141
|
+
const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
|
|
10142
|
+
return chalk9.cyan(s.slice(0, filled)) + chalk9.dim(s.slice(filled));
|
|
10143
|
+
}
|
|
10144
|
+
function pct(num2, total) {
|
|
10145
|
+
if (total === 0) return "\u2013";
|
|
10146
|
+
return Math.round(num2 / total * 100) + "%";
|
|
10147
|
+
}
|
|
10148
|
+
function fmtDate(d) {
|
|
10149
|
+
const date = typeof d === "string" ? /* @__PURE__ */ new Date(d + "T12:00:00") : d;
|
|
10150
|
+
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
10151
|
+
}
|
|
10152
|
+
function num(n) {
|
|
10153
|
+
return n.toLocaleString();
|
|
10154
|
+
}
|
|
10155
|
+
function fmtCost(usd) {
|
|
10156
|
+
if (usd < 1e-3) return "< $0.001";
|
|
10157
|
+
if (usd < 1) return "$" + usd.toFixed(4);
|
|
10158
|
+
return "$" + usd.toFixed(2);
|
|
10159
|
+
}
|
|
10160
|
+
var CLAUDE_PRICING = {
|
|
10161
|
+
"claude-opus-4-6": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
|
|
10162
|
+
"claude-opus-4-5": { i: 5e-6, o: 25e-6, cw: 625e-8, cr: 5e-7 },
|
|
10163
|
+
"claude-opus-4": { i: 15e-6, o: 75e-6, cw: 1875e-8, cr: 15e-7 },
|
|
10164
|
+
"claude-sonnet-4-6": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
10165
|
+
"claude-sonnet-4-5": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
10166
|
+
"claude-sonnet-4": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
10167
|
+
"claude-3-7-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
10168
|
+
"claude-3-5-sonnet": { i: 3e-6, o: 15e-6, cw: 375e-8, cr: 3e-7 },
|
|
10169
|
+
"claude-haiku-4-5": { i: 1e-6, o: 5e-6, cw: 125e-8, cr: 1e-7 },
|
|
10170
|
+
"claude-3-5-haiku": { i: 8e-7, o: 4e-6, cw: 1e-6, cr: 8e-8 }
|
|
10171
|
+
};
|
|
10172
|
+
function claudeModelPrice(model) {
|
|
10173
|
+
const base = model.replace(/@.*$/, "").replace(/-\d{8}$/, "");
|
|
10174
|
+
for (const [key, p] of Object.entries(CLAUDE_PRICING)) {
|
|
10175
|
+
if (base === key || base.startsWith(key + "-") || base.startsWith(key)) return p;
|
|
10176
|
+
}
|
|
10177
|
+
return null;
|
|
10178
|
+
}
|
|
10179
|
+
function loadClaudeCost(start, end) {
|
|
10180
|
+
const projectsDir = path25.join(os19.homedir(), ".claude", "projects");
|
|
10181
|
+
if (!fs23.existsSync(projectsDir)) return { total: 0, byDay: /* @__PURE__ */ new Map() };
|
|
10182
|
+
let dirs;
|
|
10183
|
+
try {
|
|
10184
|
+
dirs = fs23.readdirSync(projectsDir);
|
|
10185
|
+
} catch {
|
|
10186
|
+
return { total: 0, byDay: /* @__PURE__ */ new Map() };
|
|
10187
|
+
}
|
|
10188
|
+
let total = 0;
|
|
10189
|
+
const byDay = /* @__PURE__ */ new Map();
|
|
10190
|
+
for (const proj of dirs) {
|
|
10191
|
+
const projPath = path25.join(projectsDir, proj);
|
|
10192
|
+
let files;
|
|
10193
|
+
try {
|
|
10194
|
+
const stat = fs23.statSync(projPath);
|
|
10195
|
+
if (!stat.isDirectory()) continue;
|
|
10196
|
+
files = fs23.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
|
|
10197
|
+
} catch {
|
|
10198
|
+
continue;
|
|
10199
|
+
}
|
|
10200
|
+
for (const file of files) {
|
|
10201
|
+
try {
|
|
10202
|
+
const raw = fs23.readFileSync(path25.join(projPath, file), "utf-8");
|
|
10203
|
+
for (const line of raw.split("\n")) {
|
|
10204
|
+
if (!line.trim()) continue;
|
|
10205
|
+
let entry;
|
|
10206
|
+
try {
|
|
10207
|
+
entry = JSON.parse(line);
|
|
10208
|
+
} catch {
|
|
10209
|
+
continue;
|
|
10210
|
+
}
|
|
10211
|
+
if (entry.type !== "assistant") continue;
|
|
10212
|
+
if (!entry.timestamp) continue;
|
|
10213
|
+
const ts = new Date(entry.timestamp);
|
|
10214
|
+
if (ts < start || ts > end) continue;
|
|
10215
|
+
const usage = entry.message?.usage;
|
|
10216
|
+
const model = entry.message?.model;
|
|
10217
|
+
if (!usage || !model) continue;
|
|
10218
|
+
const p = claudeModelPrice(model);
|
|
10219
|
+
if (!p) continue;
|
|
10220
|
+
const cost = (usage.input_tokens ?? 0) * p.i + (usage.output_tokens ?? 0) * p.o + (usage.cache_creation_input_tokens ?? 0) * p.cw + (usage.cache_read_input_tokens ?? 0) * p.cr;
|
|
10221
|
+
total += cost;
|
|
10222
|
+
const dateKey = entry.timestamp.slice(0, 10);
|
|
10223
|
+
byDay.set(dateKey, (byDay.get(dateKey) ?? 0) + cost);
|
|
10224
|
+
}
|
|
10225
|
+
} catch {
|
|
10226
|
+
continue;
|
|
10227
|
+
}
|
|
10228
|
+
}
|
|
10229
|
+
}
|
|
10230
|
+
return { total, byDay };
|
|
10231
|
+
}
|
|
10232
|
+
function registerReportCommand(program2) {
|
|
10233
|
+
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) => {
|
|
10234
|
+
const period = ["today", "7d", "30d", "month"].includes(
|
|
10235
|
+
options.period
|
|
10236
|
+
) ? options.period : "7d";
|
|
10237
|
+
const logPath = path25.join(os19.homedir(), ".node9", "audit.log");
|
|
10238
|
+
const allEntries = parseAuditLog(logPath);
|
|
10239
|
+
if (allEntries.length === 0) {
|
|
10240
|
+
console.log(
|
|
10241
|
+
chalk9.yellow("\n No audit data found. Run node9 with Claude Code to generate entries.\n")
|
|
10242
|
+
);
|
|
10243
|
+
return;
|
|
10244
|
+
}
|
|
10245
|
+
const { start, end } = getDateRange(period);
|
|
10246
|
+
const { total: costUSD, byDay: costByDay } = loadClaudeCost(start, end);
|
|
10247
|
+
const periodMs = end.getTime() - start.getTime();
|
|
10248
|
+
const priorEnd = new Date(start.getTime() - 1);
|
|
10249
|
+
const priorStart = new Date(start.getTime() - periodMs);
|
|
10250
|
+
const priorEntries = allEntries.filter((e) => {
|
|
10251
|
+
if (e.source === "post-hook") return false;
|
|
10252
|
+
const ts = new Date(e.ts);
|
|
10253
|
+
return ts >= priorStart && ts <= priorEnd;
|
|
10254
|
+
});
|
|
10255
|
+
const priorBlocked = priorEntries.filter((e) => !isAllow(e.decision)).length;
|
|
10256
|
+
const priorBlockRate = priorEntries.length > 0 ? priorBlocked / priorEntries.length : null;
|
|
10257
|
+
const excludeTests = options.tests === false;
|
|
10258
|
+
const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
|
|
10259
|
+
let filteredTestCount = 0;
|
|
10260
|
+
const entries = allEntries.filter((e) => {
|
|
10261
|
+
if (e.source === "post-hook") return false;
|
|
10262
|
+
const ts = new Date(e.ts);
|
|
10263
|
+
if (ts < start || ts > end) return false;
|
|
10264
|
+
if (excludeTests && isTestEntry(e, testTs)) {
|
|
10265
|
+
filteredTestCount++;
|
|
10266
|
+
return false;
|
|
10267
|
+
}
|
|
10268
|
+
return true;
|
|
10269
|
+
});
|
|
10270
|
+
if (entries.length === 0) {
|
|
10271
|
+
console.log(chalk9.yellow(`
|
|
10272
|
+
No activity for period "${period}".
|
|
10273
|
+
`));
|
|
10274
|
+
return;
|
|
10275
|
+
}
|
|
10276
|
+
let allowed = 0;
|
|
10277
|
+
let blocked = 0;
|
|
10278
|
+
let dlpHits = 0;
|
|
10279
|
+
let loopHits = 0;
|
|
10280
|
+
let testPasses = 0;
|
|
10281
|
+
let testFails = 0;
|
|
10282
|
+
const toolMap = /* @__PURE__ */ new Map();
|
|
10283
|
+
const blockMap = /* @__PURE__ */ new Map();
|
|
10284
|
+
const agentMap = /* @__PURE__ */ new Map();
|
|
10285
|
+
const mcpMap = /* @__PURE__ */ new Map();
|
|
10286
|
+
const dailyMap = /* @__PURE__ */ new Map();
|
|
10287
|
+
const hourMap = /* @__PURE__ */ new Map();
|
|
10288
|
+
for (const e of entries) {
|
|
10289
|
+
const allow = isAllow(e.decision);
|
|
10290
|
+
const dateKey = e.ts.slice(0, 10);
|
|
10291
|
+
if (allow) allowed++;
|
|
10292
|
+
else blocked++;
|
|
10293
|
+
if (isDlp(e.checkedBy)) dlpHits++;
|
|
10294
|
+
if (e.checkedBy === "loop-detected") loopHits++;
|
|
10295
|
+
const t = toolMap.get(e.tool) ?? { calls: 0, blocked: 0 };
|
|
10296
|
+
t.calls++;
|
|
10297
|
+
if (!allow) t.blocked++;
|
|
10298
|
+
toolMap.set(e.tool, t);
|
|
10299
|
+
if (!allow && e.checkedBy) {
|
|
10300
|
+
blockMap.set(e.checkedBy, (blockMap.get(e.checkedBy) ?? 0) + 1);
|
|
10301
|
+
}
|
|
10302
|
+
if (e.agent) agentMap.set(e.agent, (agentMap.get(e.agent) ?? 0) + 1);
|
|
10303
|
+
if (e.mcpServer) mcpMap.set(e.mcpServer, (mcpMap.get(e.mcpServer) ?? 0) + 1);
|
|
10304
|
+
const hour = new Date(e.ts).getHours();
|
|
10305
|
+
hourMap.set(hour, (hourMap.get(hour) ?? 0) + 1);
|
|
10306
|
+
const d = dailyMap.get(dateKey) ?? { calls: 0, blocked: 0 };
|
|
10307
|
+
d.calls++;
|
|
10308
|
+
if (!allow) d.blocked++;
|
|
10309
|
+
dailyMap.set(dateKey, d);
|
|
10310
|
+
}
|
|
10311
|
+
for (const e of allEntries) {
|
|
10312
|
+
if (e.source !== "test-result") continue;
|
|
10313
|
+
const ts = new Date(e.ts);
|
|
10314
|
+
if (ts < start || ts > end) continue;
|
|
10315
|
+
if (e.testResult === "pass") testPasses++;
|
|
10316
|
+
else if (e.testResult === "fail") testFails++;
|
|
10317
|
+
}
|
|
10318
|
+
const total = entries.length;
|
|
10319
|
+
const topTools = [...toolMap.entries()].sort((a, b) => b[1].calls - a[1].calls).slice(0, 8);
|
|
10320
|
+
const topBlocks = [...blockMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6);
|
|
10321
|
+
const dailyList = [...dailyMap.entries()].sort((a, b) => a[0].localeCompare(b[0])).slice(-14);
|
|
10322
|
+
const maxTool = Math.max(...topTools.map(([, v]) => v.calls), 1);
|
|
10323
|
+
const maxBlock = Math.max(...topBlocks.map(([, v]) => v), 1);
|
|
10324
|
+
const maxDaily = Math.max(...dailyList.map(([, v]) => v.calls), 1);
|
|
10325
|
+
const W = Math.min(process.stdout.columns || 80, 100);
|
|
10326
|
+
const INNER = W - 4;
|
|
10327
|
+
const COL = Math.floor(INNER / 2) - 1;
|
|
10328
|
+
const LABEL = 24;
|
|
10329
|
+
const BAR = Math.max(6, Math.min(14, COL - LABEL - 8));
|
|
10330
|
+
const TOOL_COUNT_W = Math.max(...topTools.map(([, v]) => num(v.calls).length), 1);
|
|
10331
|
+
const BLOCK_COUNT_W = Math.max(...topBlocks.map(([, v]) => num(v).length), 1);
|
|
10332
|
+
const line = chalk9.dim("\u2500".repeat(W - 2));
|
|
10333
|
+
const periodLabel = {
|
|
10334
|
+
today: "Today",
|
|
10335
|
+
"7d": "Last 7 Days",
|
|
10336
|
+
"30d": "Last 30 Days",
|
|
10337
|
+
month: "This Month"
|
|
10338
|
+
};
|
|
10339
|
+
console.log("");
|
|
10340
|
+
console.log(
|
|
10341
|
+
" " + 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})`) : "")
|
|
10342
|
+
);
|
|
10343
|
+
console.log(" " + line);
|
|
10344
|
+
console.log("");
|
|
10345
|
+
const blockLabel = blocked > 0 ? chalk9.red(`\u{1F6D1} ${num(blocked)} blocked`) : chalk9.dim("\u{1F6D1} 0 blocked");
|
|
10346
|
+
const dlpLabel = dlpHits > 0 ? chalk9.yellow(`\u{1F6A8} ${dlpHits} DLP hits`) : chalk9.dim("\u{1F6A8} 0 DLP hits");
|
|
10347
|
+
const loopLabel = loopHits > 0 ? chalk9.yellow(`\u{1F504} ${loopHits} loops`) : chalk9.dim("\u{1F504} 0 loops");
|
|
10348
|
+
const costLabel = costUSD > 0 ? chalk9.magenta(`\u{1F4B0} ${fmtCost(costUSD)}`) : chalk9.dim("\u{1F4B0} \u2013");
|
|
10349
|
+
const currentRate = total > 0 ? blocked / total : 0;
|
|
10350
|
+
const trendLabel = (() => {
|
|
10351
|
+
if (priorBlockRate === null) return chalk9.dim(`${pct(blocked, total)} block rate`);
|
|
10352
|
+
const delta = Math.round((currentRate - priorBlockRate) * 100);
|
|
10353
|
+
const arrow = delta > 0 ? chalk9.red(`\u25B2${delta}%`) : delta < 0 ? chalk9.green(`\u25BC${Math.abs(delta)}%`) : chalk9.dim("\u2013");
|
|
10354
|
+
return chalk9.dim(`${pct(blocked, total)} block rate `) + arrow + chalk9.dim(" vs prior");
|
|
10355
|
+
})();
|
|
10356
|
+
const reads = toolMap.get("Read")?.calls ?? 0;
|
|
10357
|
+
const edits = (toolMap.get("Edit")?.calls ?? 0) + (toolMap.get("Write")?.calls ?? 0);
|
|
10358
|
+
const ratioLabel = reads > 0 ? chalk9.dim(`edit/read ${(edits / reads).toFixed(1)}`) : chalk9.dim("edit/read \u2013");
|
|
10359
|
+
const testLabel = testPasses + testFails > 0 ? chalk9.dim("tests ") + chalk9.green(`${testPasses}\u2713`) + (testFails > 0 ? " " + chalk9.red(`${testFails}\u2717`) : "") : chalk9.dim("tests \u2013");
|
|
10360
|
+
console.log(
|
|
10361
|
+
" " + chalk9.green(`\u2705 ${num(allowed)} allowed`) + " " + blockLabel + " " + dlpLabel + " " + loopLabel + " " + trendLabel + " " + costLabel
|
|
10362
|
+
);
|
|
10363
|
+
console.log(" " + ratioLabel + " " + testLabel);
|
|
10364
|
+
console.log("");
|
|
10365
|
+
const toolHeaderRaw = "Top Tools";
|
|
10366
|
+
const blockHeaderRaw = "Top Blocks";
|
|
10367
|
+
console.log(
|
|
10368
|
+
" " + chalk9.bold(toolHeaderRaw) + " ".repeat(COL - toolHeaderRaw.length) + " " + chalk9.bold(blockHeaderRaw)
|
|
10369
|
+
);
|
|
10370
|
+
console.log(" " + chalk9.dim("\u2500".repeat(COL)) + " " + chalk9.dim("\u2500".repeat(COL)));
|
|
10371
|
+
const rows = Math.max(topTools.length, topBlocks.length, 1);
|
|
10372
|
+
for (let i = 0; i < rows; i++) {
|
|
10373
|
+
let leftStyled = " ".repeat(COL);
|
|
10374
|
+
if (i < topTools.length) {
|
|
10375
|
+
const [tool, { calls }] = topTools[i];
|
|
10376
|
+
const label = tool.length > LABEL - 1 ? tool.slice(0, LABEL - 2) + "\u2026" : tool;
|
|
10377
|
+
const countStr = num(calls).padStart(TOOL_COUNT_W);
|
|
10378
|
+
const b = colorBar(calls, maxTool, BAR);
|
|
10379
|
+
const rawLen = LABEL + BAR + 1 + TOOL_COUNT_W;
|
|
10380
|
+
const pad = Math.max(0, COL - rawLen);
|
|
10381
|
+
leftStyled = chalk9.white(label.padEnd(LABEL)) + b + " " + chalk9.white(countStr) + " ".repeat(pad);
|
|
10382
|
+
}
|
|
10383
|
+
let rightStyled = "";
|
|
10384
|
+
if (i < topBlocks.length) {
|
|
10385
|
+
const [reason, count] = topBlocks[i];
|
|
10386
|
+
const label = reason.length > LABEL - 1 ? reason.slice(0, LABEL - 2) + "\u2026" : reason;
|
|
10387
|
+
const countStr = num(count).padStart(BLOCK_COUNT_W);
|
|
10388
|
+
const b = colorBar(count, maxBlock, BAR);
|
|
10389
|
+
rightStyled = chalk9.white(label.padEnd(LABEL)) + b + " " + chalk9.red(countStr);
|
|
10390
|
+
}
|
|
10391
|
+
console.log(" " + leftStyled + " " + rightStyled);
|
|
10392
|
+
}
|
|
10393
|
+
if (topBlocks.length === 0) {
|
|
10394
|
+
console.log(" " + " ".repeat(COL) + " " + chalk9.dim("nothing blocked \u2713"));
|
|
10395
|
+
}
|
|
10396
|
+
if (agentMap.size > 1) {
|
|
10397
|
+
console.log("");
|
|
10398
|
+
console.log(" " + chalk9.bold("Agents"));
|
|
10399
|
+
console.log(" " + chalk9.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
10400
|
+
const maxAgent = Math.max(...agentMap.values(), 1);
|
|
10401
|
+
for (const [agent, count] of [...agentMap.entries()].sort((a, b) => b[1] - a[1])) {
|
|
10402
|
+
const label = agent.slice(0, LABEL - 1);
|
|
10403
|
+
const b = colorBar(count, maxAgent, BAR);
|
|
10404
|
+
console.log(" " + chalk9.white(label.padEnd(LABEL)) + b + " " + chalk9.white(num(count)));
|
|
10405
|
+
}
|
|
10406
|
+
}
|
|
10407
|
+
if (mcpMap.size > 0) {
|
|
10408
|
+
console.log("");
|
|
10409
|
+
console.log(" " + chalk9.bold("MCP Servers"));
|
|
10410
|
+
console.log(" " + chalk9.dim("\u2500".repeat(Math.min(50, W - 4))));
|
|
10411
|
+
const maxMcp = Math.max(...mcpMap.values(), 1);
|
|
10412
|
+
for (const [server, count] of [...mcpMap.entries()].sort((a, b) => b[1] - a[1])) {
|
|
10413
|
+
const label = server.slice(0, LABEL - 1).padEnd(LABEL);
|
|
10414
|
+
const b = colorBar(count, maxMcp, BAR);
|
|
10415
|
+
console.log(" " + chalk9.white(label) + b + " " + chalk9.white(num(count)));
|
|
10416
|
+
}
|
|
10417
|
+
}
|
|
10418
|
+
if (hourMap.size > 0) {
|
|
10419
|
+
const BLOCKS = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
10420
|
+
const maxHour = Math.max(...hourMap.values(), 1);
|
|
10421
|
+
const bar = Array.from({ length: 24 }, (_, h) => {
|
|
10422
|
+
const v = hourMap.get(h) ?? 0;
|
|
10423
|
+
return BLOCKS[Math.round(v / maxHour * 8)];
|
|
10424
|
+
}).join("");
|
|
10425
|
+
console.log("");
|
|
10426
|
+
console.log(" " + chalk9.bold("Hour of Day") + chalk9.dim(" (local, 0h \u2013 23h)"));
|
|
10427
|
+
console.log(" " + chalk9.cyan(bar));
|
|
10428
|
+
console.log(" " + chalk9.dim("0h" + " ".repeat(10) + "12h" + " ".repeat(7) + "23h"));
|
|
10429
|
+
}
|
|
10430
|
+
if (dailyList.length > 1) {
|
|
10431
|
+
console.log("");
|
|
10432
|
+
console.log(" " + chalk9.bold("Daily Activity"));
|
|
10433
|
+
console.log(" " + chalk9.dim("\u2500".repeat(W - 2)));
|
|
10434
|
+
const DAY_BAR = Math.max(8, Math.min(30, W - 36));
|
|
10435
|
+
for (const [dateKey, { calls, blocked: db }] of dailyList) {
|
|
10436
|
+
const label = fmtDate(dateKey).padEnd(10);
|
|
10437
|
+
const b = colorBar(calls, maxDaily, DAY_BAR);
|
|
10438
|
+
const dayCost = costByDay.get(dateKey);
|
|
10439
|
+
const costNote = dayCost ? chalk9.magenta(` ${fmtCost(dayCost)}`) : "";
|
|
10440
|
+
const blockNote = db > 0 ? chalk9.red(` ${db} blocked`) : "";
|
|
10441
|
+
console.log(
|
|
10442
|
+
" " + chalk9.dim(label) + " " + b + " " + chalk9.white(num(calls)) + blockNote + costNote
|
|
10443
|
+
);
|
|
10444
|
+
}
|
|
10445
|
+
}
|
|
10446
|
+
console.log("");
|
|
10447
|
+
console.log(
|
|
10448
|
+
" " + chalk9.dim("node9 audit --deny") + chalk9.dim(" \xB7 ") + chalk9.dim("node9 report --period today|7d|30d|month --no-tests")
|
|
10449
|
+
);
|
|
10450
|
+
console.log("");
|
|
10451
|
+
});
|
|
10452
|
+
}
|
|
10453
|
+
|
|
9942
10454
|
// src/cli/commands/daemon-cmd.ts
|
|
9943
10455
|
init_daemon2();
|
|
9944
10456
|
init_daemon();
|
|
9945
|
-
import
|
|
10457
|
+
import chalk10 from "chalk";
|
|
9946
10458
|
import { spawn as spawn7 } from "child_process";
|
|
9947
10459
|
function registerDaemonCommand(program2) {
|
|
9948
10460
|
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 +10467,7 @@ function registerDaemonCommand(program2) {
|
|
|
9955
10467
|
if (cmd === "status") return daemonStatus();
|
|
9956
10468
|
if (cmd !== "start" && action !== void 0) {
|
|
9957
10469
|
console.error(
|
|
9958
|
-
|
|
10470
|
+
chalk10.red(`Unknown daemon action: "${action}". Use: start | stop | status`)
|
|
9959
10471
|
);
|
|
9960
10472
|
process.exit(1);
|
|
9961
10473
|
}
|
|
@@ -9963,7 +10475,7 @@ function registerDaemonCommand(program2) {
|
|
|
9963
10475
|
process.env.NODE9_WATCH_MODE = "1";
|
|
9964
10476
|
setTimeout(() => {
|
|
9965
10477
|
openBrowserLocal();
|
|
9966
|
-
console.log(
|
|
10478
|
+
console.log(chalk10.cyan(`\u{1F6F0}\uFE0F Flight Recorder: http://${DAEMON_HOST}:${DAEMON_PORT}/`));
|
|
9967
10479
|
}, 600);
|
|
9968
10480
|
startDaemon();
|
|
9969
10481
|
return;
|
|
@@ -9971,7 +10483,7 @@ function registerDaemonCommand(program2) {
|
|
|
9971
10483
|
if (options.openui) {
|
|
9972
10484
|
if (isDaemonRunning()) {
|
|
9973
10485
|
openBrowserLocal();
|
|
9974
|
-
console.log(
|
|
10486
|
+
console.log(chalk10.green(`\u{1F310} Opened browser: http://${DAEMON_HOST}:${DAEMON_PORT}/`));
|
|
9975
10487
|
process.exit(0);
|
|
9976
10488
|
}
|
|
9977
10489
|
const child = spawn7(process.execPath, [process.argv[1], "daemon"], {
|
|
@@ -9984,7 +10496,7 @@ function registerDaemonCommand(program2) {
|
|
|
9984
10496
|
if (isDaemonRunning()) break;
|
|
9985
10497
|
}
|
|
9986
10498
|
openBrowserLocal();
|
|
9987
|
-
console.log(
|
|
10499
|
+
console.log(chalk10.green(`
|
|
9988
10500
|
\u{1F6E1}\uFE0F Node9 daemon started + browser opened`));
|
|
9989
10501
|
process.exit(0);
|
|
9990
10502
|
}
|
|
@@ -9994,7 +10506,7 @@ function registerDaemonCommand(program2) {
|
|
|
9994
10506
|
stdio: "ignore"
|
|
9995
10507
|
});
|
|
9996
10508
|
child.unref();
|
|
9997
|
-
console.log(
|
|
10509
|
+
console.log(chalk10.green(`
|
|
9998
10510
|
\u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
|
|
9999
10511
|
process.exit(0);
|
|
10000
10512
|
}
|
|
@@ -10006,13 +10518,13 @@ function registerDaemonCommand(program2) {
|
|
|
10006
10518
|
// src/cli/commands/status.ts
|
|
10007
10519
|
init_core();
|
|
10008
10520
|
init_daemon();
|
|
10009
|
-
import
|
|
10010
|
-
import
|
|
10011
|
-
import
|
|
10012
|
-
import
|
|
10521
|
+
import chalk11 from "chalk";
|
|
10522
|
+
import fs24 from "fs";
|
|
10523
|
+
import path26 from "path";
|
|
10524
|
+
import os20 from "os";
|
|
10013
10525
|
function readJson2(filePath) {
|
|
10014
10526
|
try {
|
|
10015
|
-
if (
|
|
10527
|
+
if (fs24.existsSync(filePath)) return JSON.parse(fs24.readFileSync(filePath, "utf-8"));
|
|
10016
10528
|
} catch {
|
|
10017
10529
|
}
|
|
10018
10530
|
return null;
|
|
@@ -10026,21 +10538,21 @@ function wrappedMcpServers(servers) {
|
|
|
10026
10538
|
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
10539
|
}
|
|
10028
10540
|
function printAgentSection(label, hookPairs, wrapped) {
|
|
10029
|
-
console.log(
|
|
10541
|
+
console.log(chalk11.bold(` ${label}`));
|
|
10030
10542
|
for (const { name, present } of hookPairs) {
|
|
10031
10543
|
if (present) {
|
|
10032
|
-
console.log(
|
|
10544
|
+
console.log(chalk11.green(` \u2713 ${name}`));
|
|
10033
10545
|
} else {
|
|
10034
|
-
console.log(
|
|
10546
|
+
console.log(chalk11.red(` \u2717 ${name}`) + chalk11.gray(" (not wired)"));
|
|
10035
10547
|
}
|
|
10036
10548
|
}
|
|
10037
10549
|
if (wrapped.length > 0) {
|
|
10038
|
-
console.log(
|
|
10550
|
+
console.log(chalk11.cyan(` MCP proxied:`));
|
|
10039
10551
|
for (const entry of wrapped) {
|
|
10040
|
-
console.log(
|
|
10552
|
+
console.log(chalk11.gray(` \u2022 ${entry}`));
|
|
10041
10553
|
}
|
|
10042
10554
|
} else {
|
|
10043
|
-
console.log(
|
|
10555
|
+
console.log(chalk11.gray(` MCP proxied: none`));
|
|
10044
10556
|
}
|
|
10045
10557
|
}
|
|
10046
10558
|
function registerStatusCommand(program2) {
|
|
@@ -10051,58 +10563,58 @@ function registerStatusCommand(program2) {
|
|
|
10051
10563
|
const settings = mergedConfig.settings;
|
|
10052
10564
|
console.log("");
|
|
10053
10565
|
if (creds && settings.approvers.cloud) {
|
|
10054
|
-
console.log(
|
|
10566
|
+
console.log(chalk11.green(" \u25CF Agent mode") + chalk11.gray(" \u2014 cloud team policy enforced"));
|
|
10055
10567
|
} else if (creds && !settings.approvers.cloud) {
|
|
10056
10568
|
console.log(
|
|
10057
|
-
|
|
10569
|
+
chalk11.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + chalk11.gray(" \u2014 all decisions stay on this machine")
|
|
10058
10570
|
);
|
|
10059
10571
|
} else {
|
|
10060
10572
|
console.log(
|
|
10061
|
-
|
|
10573
|
+
chalk11.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + chalk11.gray(" \u2014 no API key (Local rules only)")
|
|
10062
10574
|
);
|
|
10063
10575
|
}
|
|
10064
10576
|
console.log("");
|
|
10065
10577
|
if (daemonRunning) {
|
|
10066
10578
|
console.log(
|
|
10067
|
-
|
|
10579
|
+
chalk11.green(" \u25CF Daemon running") + chalk11.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT}/`)
|
|
10068
10580
|
);
|
|
10069
10581
|
} else {
|
|
10070
|
-
console.log(
|
|
10582
|
+
console.log(chalk11.gray(" \u25CB Daemon stopped"));
|
|
10071
10583
|
}
|
|
10072
10584
|
if (settings.enableUndo) {
|
|
10073
10585
|
console.log(
|
|
10074
|
-
|
|
10586
|
+
chalk11.magenta(" \u25CF Undo Engine") + chalk11.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
|
|
10075
10587
|
);
|
|
10076
10588
|
}
|
|
10077
10589
|
console.log("");
|
|
10078
|
-
const modeLabel = settings.mode === "audit" ?
|
|
10590
|
+
const modeLabel = settings.mode === "audit" ? chalk11.blue("audit") : settings.mode === "strict" ? chalk11.red("strict") : chalk11.white("standard");
|
|
10079
10591
|
console.log(` Mode: ${modeLabel}`);
|
|
10080
|
-
const projectConfig =
|
|
10081
|
-
const globalConfig =
|
|
10592
|
+
const projectConfig = path26.join(process.cwd(), "node9.config.json");
|
|
10593
|
+
const globalConfig = path26.join(os20.homedir(), ".node9", "config.json");
|
|
10082
10594
|
console.log(
|
|
10083
|
-
` Local: ${
|
|
10595
|
+
` Local: ${fs24.existsSync(projectConfig) ? chalk11.green("Active (node9.config.json)") : chalk11.gray("Not present")}`
|
|
10084
10596
|
);
|
|
10085
10597
|
console.log(
|
|
10086
|
-
` Global: ${
|
|
10598
|
+
` Global: ${fs24.existsSync(globalConfig) ? chalk11.green("Active (~/.node9/config.json)") : chalk11.gray("Not present")}`
|
|
10087
10599
|
);
|
|
10088
10600
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
10089
10601
|
console.log(
|
|
10090
|
-
` Sandbox: ${
|
|
10602
|
+
` Sandbox: ${chalk11.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
10091
10603
|
);
|
|
10092
10604
|
}
|
|
10093
|
-
const homeDir2 =
|
|
10605
|
+
const homeDir2 = os20.homedir();
|
|
10094
10606
|
const claudeSettings = readJson2(
|
|
10095
|
-
|
|
10607
|
+
path26.join(homeDir2, ".claude", "settings.json")
|
|
10096
10608
|
);
|
|
10097
|
-
const claudeConfig = readJson2(
|
|
10609
|
+
const claudeConfig = readJson2(path26.join(homeDir2, ".claude.json"));
|
|
10098
10610
|
const geminiSettings = readJson2(
|
|
10099
|
-
|
|
10611
|
+
path26.join(homeDir2, ".gemini", "settings.json")
|
|
10100
10612
|
);
|
|
10101
|
-
const cursorConfig = readJson2(
|
|
10613
|
+
const cursorConfig = readJson2(path26.join(homeDir2, ".cursor", "mcp.json"));
|
|
10102
10614
|
const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
|
|
10103
10615
|
if (agentFound) {
|
|
10104
10616
|
console.log("");
|
|
10105
|
-
console.log(
|
|
10617
|
+
console.log(chalk11.bold(" Agent Wiring:"));
|
|
10106
10618
|
console.log("");
|
|
10107
10619
|
if (claudeSettings || claudeConfig) {
|
|
10108
10620
|
const preHook = claudeSettings?.hooks?.PreToolUse?.some(
|
|
@@ -10148,7 +10660,7 @@ function registerStatusCommand(program2) {
|
|
|
10148
10660
|
const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
|
|
10149
10661
|
console.log("");
|
|
10150
10662
|
console.log(
|
|
10151
|
-
|
|
10663
|
+
chalk11.yellow(` \u23F8 PAUSED until ${expiresAt}`) + chalk11.gray(" \u2014 all tool calls allowed")
|
|
10152
10664
|
);
|
|
10153
10665
|
}
|
|
10154
10666
|
console.log("");
|
|
@@ -10157,10 +10669,10 @@ function registerStatusCommand(program2) {
|
|
|
10157
10669
|
|
|
10158
10670
|
// src/cli/commands/init.ts
|
|
10159
10671
|
init_core();
|
|
10160
|
-
import
|
|
10161
|
-
import
|
|
10162
|
-
import
|
|
10163
|
-
import
|
|
10672
|
+
import chalk12 from "chalk";
|
|
10673
|
+
import fs25 from "fs";
|
|
10674
|
+
import path27 from "path";
|
|
10675
|
+
import os21 from "os";
|
|
10164
10676
|
import https2 from "https";
|
|
10165
10677
|
init_shields();
|
|
10166
10678
|
var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
|
|
@@ -10195,7 +10707,7 @@ function fireTelemetryPing(agents) {
|
|
|
10195
10707
|
}
|
|
10196
10708
|
function registerInitCommand(program2) {
|
|
10197
10709
|
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(
|
|
10710
|
+
console.log(chalk12.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
|
|
10199
10711
|
let chosenMode = options.mode.toLowerCase();
|
|
10200
10712
|
if (!["standard", "strict", "audit"].includes(chosenMode)) {
|
|
10201
10713
|
chosenMode = DEFAULT_CONFIG.settings.mode;
|
|
@@ -10214,37 +10726,37 @@ function registerInitCommand(program2) {
|
|
|
10214
10726
|
const hasNewShields = DEFAULT_SHIELDS.some((s) => !current.includes(s));
|
|
10215
10727
|
if (hasNewShields) writeActiveShields(merged);
|
|
10216
10728
|
} catch (err2) {
|
|
10217
|
-
console.log(
|
|
10729
|
+
console.log(chalk12.yellow(` \u26A0\uFE0F Could not update shields: ${String(err2)}`));
|
|
10218
10730
|
}
|
|
10219
10731
|
}
|
|
10220
10732
|
console.log("");
|
|
10221
10733
|
}
|
|
10222
|
-
const configPath =
|
|
10223
|
-
if (
|
|
10734
|
+
const configPath = path27.join(os21.homedir(), ".node9", "config.json");
|
|
10735
|
+
if (fs25.existsSync(configPath) && !options.force) {
|
|
10224
10736
|
try {
|
|
10225
|
-
const existing = JSON.parse(
|
|
10737
|
+
const existing = JSON.parse(fs25.readFileSync(configPath, "utf-8"));
|
|
10226
10738
|
const settings = existing.settings ?? {};
|
|
10227
10739
|
if (settings.mode !== chosenMode) {
|
|
10228
10740
|
settings.mode = chosenMode;
|
|
10229
10741
|
existing.settings = settings;
|
|
10230
|
-
|
|
10231
|
-
console.log(
|
|
10742
|
+
fs25.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
|
|
10743
|
+
console.log(chalk12.green(`\u2705 Mode updated: ${chosenMode}`));
|
|
10232
10744
|
} else {
|
|
10233
|
-
console.log(
|
|
10745
|
+
console.log(chalk12.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
10234
10746
|
}
|
|
10235
10747
|
} catch {
|
|
10236
|
-
console.log(
|
|
10748
|
+
console.log(chalk12.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
10237
10749
|
}
|
|
10238
10750
|
} else {
|
|
10239
10751
|
const configToSave = {
|
|
10240
10752
|
...DEFAULT_CONFIG,
|
|
10241
10753
|
settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
|
|
10242
10754
|
};
|
|
10243
|
-
const dir =
|
|
10244
|
-
if (!
|
|
10245
|
-
|
|
10246
|
-
console.log(
|
|
10247
|
-
console.log(
|
|
10755
|
+
const dir = path27.dirname(configPath);
|
|
10756
|
+
if (!fs25.existsSync(dir)) fs25.mkdirSync(dir, { recursive: true });
|
|
10757
|
+
fs25.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
|
|
10758
|
+
console.log(chalk12.green(`\u2705 Config created: ${configPath}`));
|
|
10759
|
+
console.log(chalk12.gray(` Mode: ${chosenMode}`));
|
|
10248
10760
|
}
|
|
10249
10761
|
if (options.skipSetup) return;
|
|
10250
10762
|
console.log("");
|
|
@@ -10254,18 +10766,18 @@ function registerInitCommand(program2) {
|
|
|
10254
10766
|
);
|
|
10255
10767
|
if (found.length === 0) {
|
|
10256
10768
|
console.log(
|
|
10257
|
-
|
|
10769
|
+
chalk12.gray("No AI agents detected. Install Claude Code, Gemini CLI, Cursor, or Codex")
|
|
10258
10770
|
);
|
|
10259
|
-
console.log(
|
|
10771
|
+
console.log(chalk12.gray("then run: node9 addto <claude|gemini|cursor|codex>"));
|
|
10260
10772
|
return;
|
|
10261
10773
|
}
|
|
10262
|
-
console.log(
|
|
10774
|
+
console.log(chalk12.bold("Detected agents:"));
|
|
10263
10775
|
for (const agent of found) {
|
|
10264
|
-
console.log(
|
|
10776
|
+
console.log(chalk12.green(` \u2713 ${agent}`));
|
|
10265
10777
|
}
|
|
10266
10778
|
console.log("");
|
|
10267
10779
|
for (const agent of found) {
|
|
10268
|
-
console.log(
|
|
10780
|
+
console.log(chalk12.bold(`Wiring ${agent}...`));
|
|
10269
10781
|
if (agent === "claude") await setupClaude();
|
|
10270
10782
|
else if (agent === "gemini") await setupGemini();
|
|
10271
10783
|
else if (agent === "cursor") await setupCursor();
|
|
@@ -10282,26 +10794,26 @@ function registerInitCommand(program2) {
|
|
|
10282
10794
|
console.log("");
|
|
10283
10795
|
}
|
|
10284
10796
|
const agentList = found.join(", ");
|
|
10285
|
-
console.log(
|
|
10797
|
+
console.log(chalk12.green.bold(`\u{1F6E1}\uFE0F Node9 is protecting ${agentList}!`));
|
|
10286
10798
|
console.log("");
|
|
10287
|
-
console.log(
|
|
10288
|
-
console.log(
|
|
10799
|
+
console.log(chalk12.white(" Watch live: ") + chalk12.cyan("node9 tail"));
|
|
10800
|
+
console.log(chalk12.white(" Local UI: ") + chalk12.cyan("node9 daemon --openui"));
|
|
10289
10801
|
console.log("");
|
|
10290
|
-
console.log(
|
|
10802
|
+
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
10803
|
console.log(
|
|
10292
|
-
|
|
10804
|
+
chalk12.white(" Team dashboard + full audit trail \u2192 ") + chalk12.cyan.bold("https://node9.ai")
|
|
10293
10805
|
);
|
|
10294
|
-
console.log(
|
|
10806
|
+
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
10807
|
});
|
|
10296
10808
|
}
|
|
10297
10809
|
|
|
10298
10810
|
// src/cli/commands/undo.ts
|
|
10299
|
-
import
|
|
10300
|
-
import
|
|
10811
|
+
import path28 from "path";
|
|
10812
|
+
import chalk14 from "chalk";
|
|
10301
10813
|
|
|
10302
10814
|
// src/tui/undo-navigator.ts
|
|
10303
10815
|
import readline2 from "readline";
|
|
10304
|
-
import
|
|
10816
|
+
import chalk13 from "chalk";
|
|
10305
10817
|
var RESET = "\x1B[0m";
|
|
10306
10818
|
var BOLD = "\x1B[1m";
|
|
10307
10819
|
var CLEAR_SCREEN = "\x1B[2J\x1B[H";
|
|
@@ -10319,15 +10831,15 @@ function renderDiff(raw) {
|
|
|
10319
10831
|
);
|
|
10320
10832
|
for (const line of lines) {
|
|
10321
10833
|
if (line.startsWith("+++") || line.startsWith("---")) {
|
|
10322
|
-
process.stdout.write(
|
|
10834
|
+
process.stdout.write(chalk13.bold(line) + "\n");
|
|
10323
10835
|
} else if (line.startsWith("+")) {
|
|
10324
|
-
process.stdout.write(
|
|
10836
|
+
process.stdout.write(chalk13.green(line) + "\n");
|
|
10325
10837
|
} else if (line.startsWith("-")) {
|
|
10326
|
-
process.stdout.write(
|
|
10838
|
+
process.stdout.write(chalk13.red(line) + "\n");
|
|
10327
10839
|
} else if (line.startsWith("@@")) {
|
|
10328
|
-
process.stdout.write(
|
|
10840
|
+
process.stdout.write(chalk13.cyan(line) + "\n");
|
|
10329
10841
|
} else {
|
|
10330
|
-
process.stdout.write(
|
|
10842
|
+
process.stdout.write(chalk13.gray(line) + "\n");
|
|
10331
10843
|
}
|
|
10332
10844
|
}
|
|
10333
10845
|
}
|
|
@@ -10346,23 +10858,23 @@ function render(entries, idx) {
|
|
|
10346
10858
|
const step = idx + 1;
|
|
10347
10859
|
process.stdout.write(CLEAR_SCREEN);
|
|
10348
10860
|
process.stdout.write(
|
|
10349
|
-
|
|
10861
|
+
chalk13.magenta.bold(`\u23EA Node9 Undo`) + chalk13.gray(` \u2500\u2500 step ${step} of ${total}`) + (entry.files?.length ? chalk13.gray(
|
|
10350
10862
|
` \u2500\u2500 ${entry.files.slice(0, 2).join(", ")}${entry.files.length > 2 ? ` +${entry.files.length - 2} more` : ""}`
|
|
10351
10863
|
) : "") + "\n\n"
|
|
10352
10864
|
);
|
|
10353
10865
|
process.stdout.write(
|
|
10354
|
-
` ${BOLD}Tool:${RESET} ${
|
|
10866
|
+
` ${BOLD}Tool:${RESET} ${chalk13.cyan(entry.tool)}` + (entry.argsSummary ? chalk13.gray(" \u2192 " + entry.argsSummary) : "") + "\n"
|
|
10355
10867
|
);
|
|
10356
|
-
process.stdout.write(` ${BOLD}When:${RESET} ${
|
|
10868
|
+
process.stdout.write(` ${BOLD}When:${RESET} ${chalk13.gray(formatAge(entry.timestamp))}
|
|
10357
10869
|
`);
|
|
10358
|
-
process.stdout.write(` ${BOLD}Dir: ${RESET} ${
|
|
10870
|
+
process.stdout.write(` ${BOLD}Dir: ${RESET} ${chalk13.gray(entry.cwd)}
|
|
10359
10871
|
`);
|
|
10360
10872
|
if (entry.files && entry.files.length > 0) {
|
|
10361
|
-
process.stdout.write(` ${BOLD}Files:${RESET} ${
|
|
10873
|
+
process.stdout.write(` ${BOLD}Files:${RESET} ${chalk13.gray(entry.files.join(", "))}
|
|
10362
10874
|
`);
|
|
10363
10875
|
}
|
|
10364
10876
|
if (idx < total - 1 && isSessionBoundary(entries, idx + 1)) {
|
|
10365
|
-
process.stdout.write(
|
|
10877
|
+
process.stdout.write(chalk13.gray("\n \u2500\u2500 session boundary above \u2500\u2500\n"));
|
|
10366
10878
|
}
|
|
10367
10879
|
process.stdout.write("\n");
|
|
10368
10880
|
const diff = entry.diff ?? computeUndoDiff(entry.hash, entry.cwd);
|
|
@@ -10370,12 +10882,12 @@ function render(entries, idx) {
|
|
|
10370
10882
|
renderDiff(diff);
|
|
10371
10883
|
} else {
|
|
10372
10884
|
process.stdout.write(
|
|
10373
|
-
|
|
10885
|
+
chalk13.gray(" (no diff \u2014 working tree may already match this snapshot)\n")
|
|
10374
10886
|
);
|
|
10375
10887
|
}
|
|
10376
10888
|
process.stdout.write("\n");
|
|
10377
10889
|
process.stdout.write(
|
|
10378
|
-
|
|
10890
|
+
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
10891
|
);
|
|
10380
10892
|
}
|
|
10381
10893
|
async function runUndoNavigator(entries) {
|
|
@@ -10429,19 +10941,19 @@ async function runUndoNavigator(entries) {
|
|
|
10429
10941
|
cleanup();
|
|
10430
10942
|
process.stdout.write(CLEAR_SCREEN);
|
|
10431
10943
|
const entry = display[idx];
|
|
10432
|
-
process.stdout.write(
|
|
10944
|
+
process.stdout.write(chalk13.magenta.bold("\n\u23EA Restoring snapshot...\n\n"));
|
|
10433
10945
|
if (applyUndo(entry.hash, entry.cwd)) {
|
|
10434
|
-
process.stdout.write(
|
|
10946
|
+
process.stdout.write(chalk13.green("\u2705 Reverted successfully.\n\n"));
|
|
10435
10947
|
resolve({ restored: true });
|
|
10436
10948
|
} else {
|
|
10437
|
-
process.stdout.write(
|
|
10949
|
+
process.stdout.write(chalk13.red("\u274C Undo failed.\n\n"));
|
|
10438
10950
|
resolve({ restored: false });
|
|
10439
10951
|
}
|
|
10440
10952
|
} else if (name === "q" || key?.ctrl && name === "c") {
|
|
10441
10953
|
done = true;
|
|
10442
10954
|
cleanup();
|
|
10443
10955
|
process.stdout.write(CLEAR_SCREEN);
|
|
10444
|
-
process.stdout.write(
|
|
10956
|
+
process.stdout.write(chalk13.gray("\nCancelled.\n\n"));
|
|
10445
10957
|
resolve({ restored: false });
|
|
10446
10958
|
}
|
|
10447
10959
|
};
|
|
@@ -10455,7 +10967,7 @@ function findMatchingCwd(startDir, history) {
|
|
|
10455
10967
|
let dir = startDir;
|
|
10456
10968
|
while (true) {
|
|
10457
10969
|
if (cwds.has(dir)) return dir;
|
|
10458
|
-
const parent =
|
|
10970
|
+
const parent = path28.dirname(dir);
|
|
10459
10971
|
if (parent === dir) return null;
|
|
10460
10972
|
dir = parent;
|
|
10461
10973
|
}
|
|
@@ -10477,39 +10989,39 @@ function registerUndoCommand(program2) {
|
|
|
10477
10989
|
if (history.length === 0) {
|
|
10478
10990
|
if (!options.all && allHistory.length > 0) {
|
|
10479
10991
|
console.log(
|
|
10480
|
-
|
|
10992
|
+
chalk14.yellow(
|
|
10481
10993
|
`
|
|
10482
10994
|
\u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
|
|
10483
|
-
Run ${
|
|
10995
|
+
Run ${chalk14.cyan("node9 undo --all")} to see snapshots from all projects.
|
|
10484
10996
|
`
|
|
10485
10997
|
)
|
|
10486
10998
|
);
|
|
10487
10999
|
} else {
|
|
10488
|
-
console.log(
|
|
11000
|
+
console.log(chalk14.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
|
|
10489
11001
|
}
|
|
10490
11002
|
return;
|
|
10491
11003
|
}
|
|
10492
11004
|
if (options.list) {
|
|
10493
|
-
console.log(
|
|
11005
|
+
console.log(chalk14.magenta.bold("\n\u23EA Snapshot History\n"));
|
|
10494
11006
|
console.log(
|
|
10495
|
-
|
|
11007
|
+
chalk14.gray(
|
|
10496
11008
|
` ${"#".padEnd(3)} ${"File / Command".padEnd(30)} ${"Tool".padEnd(8)} ${"When".padEnd(10)} Dir`
|
|
10497
11009
|
)
|
|
10498
11010
|
);
|
|
10499
|
-
console.log(
|
|
11011
|
+
console.log(chalk14.gray(" " + "\u2500".repeat(80)));
|
|
10500
11012
|
const display = [...history].reverse();
|
|
10501
11013
|
let prevTs = null;
|
|
10502
11014
|
for (let i = 0; i < display.length; i++) {
|
|
10503
11015
|
const e = display[i];
|
|
10504
11016
|
const isGap = prevTs !== null && prevTs - e.timestamp > 6e4;
|
|
10505
|
-
if (isGap) console.log(
|
|
11017
|
+
if (isGap) console.log(chalk14.gray(" \u2500\u2500 earlier \u2500\u2500"));
|
|
10506
11018
|
const label = (e.argsSummary || e.files?.[0] || "\u2014").slice(0, 30).padEnd(30);
|
|
10507
11019
|
const tool = e.tool.slice(0, 8).padEnd(8);
|
|
10508
11020
|
const when = formatAge2(e.timestamp).padEnd(10);
|
|
10509
11021
|
const dir = e.cwd.length > 30 ? "\u2026" + e.cwd.slice(-29) : e.cwd;
|
|
10510
11022
|
console.log(
|
|
10511
|
-
|
|
10512
|
-
` ${String(i + 1).padEnd(3)} ${label} ${
|
|
11023
|
+
chalk14.white(
|
|
11024
|
+
` ${String(i + 1).padEnd(3)} ${label} ${chalk14.cyan(tool)} ${chalk14.gray(when)} ${chalk14.gray(dir)}`
|
|
10513
11025
|
)
|
|
10514
11026
|
);
|
|
10515
11027
|
prevTs = e.timestamp;
|
|
@@ -10522,7 +11034,7 @@ function registerUndoCommand(program2) {
|
|
|
10522
11034
|
const idx = history.length - steps;
|
|
10523
11035
|
if (idx < 0) {
|
|
10524
11036
|
console.log(
|
|
10525
|
-
|
|
11037
|
+
chalk14.yellow(
|
|
10526
11038
|
`
|
|
10527
11039
|
\u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
|
|
10528
11040
|
`
|
|
@@ -10533,47 +11045,47 @@ function registerUndoCommand(program2) {
|
|
|
10533
11045
|
const snapshot = history[idx];
|
|
10534
11046
|
const ageStr = formatAge2(snapshot.timestamp);
|
|
10535
11047
|
console.log(
|
|
10536
|
-
|
|
11048
|
+
chalk14.magenta.bold(`
|
|
10537
11049
|
\u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`)
|
|
10538
11050
|
);
|
|
10539
11051
|
console.log(
|
|
10540
|
-
|
|
10541
|
-
` Tool: ${
|
|
11052
|
+
chalk14.white(
|
|
11053
|
+
` Tool: ${chalk14.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk14.gray(" \u2192 " + snapshot.argsSummary) : ""}`
|
|
10542
11054
|
)
|
|
10543
11055
|
);
|
|
10544
|
-
console.log(
|
|
10545
|
-
console.log(
|
|
11056
|
+
console.log(chalk14.white(` When: ${chalk14.gray(ageStr)}`));
|
|
11057
|
+
console.log(chalk14.white(` Dir: ${chalk14.gray(snapshot.cwd)}`));
|
|
10546
11058
|
if (steps > 1)
|
|
10547
11059
|
console.log(
|
|
10548
|
-
|
|
11060
|
+
chalk14.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
|
|
10549
11061
|
);
|
|
10550
11062
|
console.log("");
|
|
10551
11063
|
const diff = snapshot.diff ?? computeUndoDiff(snapshot.hash, snapshot.cwd);
|
|
10552
11064
|
if (diff) {
|
|
10553
11065
|
const lines = diff.split("\n").filter((l) => !l.startsWith("diff --git") && !l.startsWith("index "));
|
|
10554
11066
|
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(
|
|
11067
|
+
if (line.startsWith("+++") || line.startsWith("---")) console.log(chalk14.bold(line));
|
|
11068
|
+
else if (line.startsWith("+")) console.log(chalk14.green(line));
|
|
11069
|
+
else if (line.startsWith("-")) console.log(chalk14.red(line));
|
|
11070
|
+
else if (line.startsWith("@@")) console.log(chalk14.cyan(line));
|
|
11071
|
+
else console.log(chalk14.gray(line));
|
|
10560
11072
|
}
|
|
10561
11073
|
console.log("");
|
|
10562
11074
|
} else {
|
|
10563
11075
|
console.log(
|
|
10564
|
-
|
|
11076
|
+
chalk14.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
|
|
10565
11077
|
);
|
|
10566
11078
|
}
|
|
10567
11079
|
const { confirm: confirm3 } = await import("@inquirer/prompts");
|
|
10568
11080
|
const proceed = await confirm3({ message: `Revert to this snapshot?`, default: false });
|
|
10569
11081
|
if (proceed) {
|
|
10570
11082
|
if (applyUndo(snapshot.hash, snapshot.cwd)) {
|
|
10571
|
-
console.log(
|
|
11083
|
+
console.log(chalk14.green("\n\u2705 Reverted successfully.\n"));
|
|
10572
11084
|
} else {
|
|
10573
|
-
console.error(
|
|
11085
|
+
console.error(chalk14.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
|
|
10574
11086
|
}
|
|
10575
11087
|
} else {
|
|
10576
|
-
console.log(
|
|
11088
|
+
console.log(chalk14.gray("\nCancelled.\n"));
|
|
10577
11089
|
}
|
|
10578
11090
|
return;
|
|
10579
11091
|
}
|
|
@@ -10583,7 +11095,7 @@ function registerUndoCommand(program2) {
|
|
|
10583
11095
|
|
|
10584
11096
|
// src/cli/commands/watch.ts
|
|
10585
11097
|
init_daemon();
|
|
10586
|
-
import
|
|
11098
|
+
import chalk15 from "chalk";
|
|
10587
11099
|
import { spawn as spawn8, spawnSync as spawnSync5 } from "child_process";
|
|
10588
11100
|
function registerWatchCommand(program2) {
|
|
10589
11101
|
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 +11111,7 @@ function registerWatchCommand(program2) {
|
|
|
10599
11111
|
throw new Error("not running");
|
|
10600
11112
|
}
|
|
10601
11113
|
} catch {
|
|
10602
|
-
console.error(
|
|
11114
|
+
console.error(chalk15.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
|
|
10603
11115
|
const child = spawn8(process.execPath, [process.argv[1], "daemon"], {
|
|
10604
11116
|
detached: true,
|
|
10605
11117
|
stdio: "ignore",
|
|
@@ -10621,12 +11133,12 @@ function registerWatchCommand(program2) {
|
|
|
10621
11133
|
}
|
|
10622
11134
|
}
|
|
10623
11135
|
if (!ready) {
|
|
10624
|
-
console.error(
|
|
11136
|
+
console.error(chalk15.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
10625
11137
|
process.exit(1);
|
|
10626
11138
|
}
|
|
10627
11139
|
}
|
|
10628
11140
|
console.error(
|
|
10629
|
-
|
|
11141
|
+
chalk15.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + chalk15.dim(` \u2192 localhost:${port}`) + chalk15.dim(
|
|
10630
11142
|
"\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
|
|
10631
11143
|
)
|
|
10632
11144
|
);
|
|
@@ -10635,7 +11147,7 @@ function registerWatchCommand(program2) {
|
|
|
10635
11147
|
env: { ...process.env, NODE9_WATCH_MODE: "1" }
|
|
10636
11148
|
});
|
|
10637
11149
|
if (result.error) {
|
|
10638
|
-
console.error(
|
|
11150
|
+
console.error(chalk15.red(`\u274C Failed to run command: ${result.error.message}`));
|
|
10639
11151
|
process.exit(1);
|
|
10640
11152
|
}
|
|
10641
11153
|
process.exit(result.status ?? 0);
|
|
@@ -10645,18 +11157,18 @@ function registerWatchCommand(program2) {
|
|
|
10645
11157
|
// src/mcp-gateway/index.ts
|
|
10646
11158
|
init_orchestrator();
|
|
10647
11159
|
import readline3 from "readline";
|
|
10648
|
-
import
|
|
11160
|
+
import chalk16 from "chalk";
|
|
10649
11161
|
import { spawn as spawn9 } from "child_process";
|
|
10650
11162
|
import { execa as execa2 } from "execa";
|
|
10651
11163
|
init_provenance();
|
|
10652
11164
|
|
|
10653
11165
|
// src/mcp-pin.ts
|
|
10654
|
-
import
|
|
10655
|
-
import
|
|
10656
|
-
import
|
|
10657
|
-
import
|
|
11166
|
+
import fs26 from "fs";
|
|
11167
|
+
import path29 from "path";
|
|
11168
|
+
import os22 from "os";
|
|
11169
|
+
import crypto4 from "crypto";
|
|
10658
11170
|
function getPinsFilePath() {
|
|
10659
|
-
return
|
|
11171
|
+
return path29.join(os22.homedir(), ".node9", "mcp-pins.json");
|
|
10660
11172
|
}
|
|
10661
11173
|
function hashToolDefinitions(tools) {
|
|
10662
11174
|
const sorted = [...tools].sort((a, b) => {
|
|
@@ -10665,15 +11177,15 @@ function hashToolDefinitions(tools) {
|
|
|
10665
11177
|
return nameA.localeCompare(nameB);
|
|
10666
11178
|
});
|
|
10667
11179
|
const canonical = JSON.stringify(sorted);
|
|
10668
|
-
return
|
|
11180
|
+
return crypto4.createHash("sha256").update(canonical).digest("hex");
|
|
10669
11181
|
}
|
|
10670
11182
|
function getServerKey(upstreamCommand) {
|
|
10671
|
-
return
|
|
11183
|
+
return crypto4.createHash("sha256").update(upstreamCommand).digest("hex").slice(0, 16);
|
|
10672
11184
|
}
|
|
10673
11185
|
function readMcpPinsSafe() {
|
|
10674
11186
|
const filePath = getPinsFilePath();
|
|
10675
11187
|
try {
|
|
10676
|
-
const raw =
|
|
11188
|
+
const raw = fs26.readFileSync(filePath, "utf-8");
|
|
10677
11189
|
if (!raw.trim()) {
|
|
10678
11190
|
return { ok: false, reason: "corrupt", detail: "empty file" };
|
|
10679
11191
|
}
|
|
@@ -10697,10 +11209,10 @@ function readMcpPins() {
|
|
|
10697
11209
|
}
|
|
10698
11210
|
function writeMcpPins(data) {
|
|
10699
11211
|
const filePath = getPinsFilePath();
|
|
10700
|
-
|
|
10701
|
-
const tmp = `${filePath}.${
|
|
10702
|
-
|
|
10703
|
-
|
|
11212
|
+
fs26.mkdirSync(path29.dirname(filePath), { recursive: true });
|
|
11213
|
+
const tmp = `${filePath}.${crypto4.randomBytes(6).toString("hex")}.tmp`;
|
|
11214
|
+
fs26.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 384 });
|
|
11215
|
+
fs26.renameSync(tmp, filePath);
|
|
10704
11216
|
}
|
|
10705
11217
|
function checkPin(serverKey, currentHash) {
|
|
10706
11218
|
const result = readMcpPinsSafe();
|
|
@@ -10792,13 +11304,13 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
10792
11304
|
const prov = checkProvenance(executable);
|
|
10793
11305
|
if (prov.trustLevel === "suspect") {
|
|
10794
11306
|
console.error(
|
|
10795
|
-
|
|
11307
|
+
chalk16.red(
|
|
10796
11308
|
`\u26A0\uFE0F Node9: Upstream MCP server binary is suspect \u2014 ${prov.reason} (${prov.resolvedPath})`
|
|
10797
11309
|
)
|
|
10798
11310
|
);
|
|
10799
|
-
console.error(
|
|
11311
|
+
console.error(chalk16.red(" Verify this binary is trusted before proceeding."));
|
|
10800
11312
|
}
|
|
10801
|
-
console.error(
|
|
11313
|
+
console.error(chalk16.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
|
|
10802
11314
|
const UPSTREAM_INJECTOR_VARS = /* @__PURE__ */ new Set([
|
|
10803
11315
|
"NODE_OPTIONS",
|
|
10804
11316
|
"NODE_PATH",
|
|
@@ -10901,10 +11413,10 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
10901
11413
|
mcpServer
|
|
10902
11414
|
});
|
|
10903
11415
|
if (!result.approved) {
|
|
10904
|
-
console.error(
|
|
11416
|
+
console.error(chalk16.red(`
|
|
10905
11417
|
\u{1F6D1} Node9 MCP Gateway: Action Blocked`));
|
|
10906
|
-
console.error(
|
|
10907
|
-
console.error(
|
|
11418
|
+
console.error(chalk16.gray(` Tool: ${toolName}`));
|
|
11419
|
+
console.error(chalk16.gray(` Reason: ${result.reason ?? "Security Policy"}
|
|
10908
11420
|
`));
|
|
10909
11421
|
const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
|
|
10910
11422
|
const isHumanDecision = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
|
|
@@ -10983,7 +11495,7 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
10983
11495
|
updatePin(serverKey, upstreamCommand, currentHash, toolNames);
|
|
10984
11496
|
pinState = "validated";
|
|
10985
11497
|
console.error(
|
|
10986
|
-
|
|
11498
|
+
chalk16.green(
|
|
10987
11499
|
`\u{1F512} Node9: Pinned ${toolNames.length} tool definition(s) for this MCP server`
|
|
10988
11500
|
)
|
|
10989
11501
|
);
|
|
@@ -10996,11 +11508,11 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
10996
11508
|
} else if (pinStatus === "corrupt") {
|
|
10997
11509
|
pinState = "quarantined";
|
|
10998
11510
|
console.error(
|
|
10999
|
-
|
|
11511
|
+
chalk16.red("\n\u{1F6A8} Node9: MCP pin file is corrupt or unreadable \u2014 session quarantined!")
|
|
11000
11512
|
);
|
|
11001
|
-
console.error(
|
|
11513
|
+
console.error(chalk16.red(" Tool calls are blocked until the pin file is repaired."));
|
|
11002
11514
|
console.error(
|
|
11003
|
-
|
|
11515
|
+
chalk16.yellow(` Run: node9 mcp pin reset (to clear and re-pin on next connect)
|
|
11004
11516
|
`)
|
|
11005
11517
|
);
|
|
11006
11518
|
const errorResponse = {
|
|
@@ -11017,13 +11529,13 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
11017
11529
|
} else {
|
|
11018
11530
|
pinState = "quarantined";
|
|
11019
11531
|
console.error(
|
|
11020
|
-
|
|
11532
|
+
chalk16.red("\n\u{1F6A8} Node9: MCP tool definitions have changed since last verified!")
|
|
11021
11533
|
);
|
|
11022
11534
|
console.error(
|
|
11023
|
-
|
|
11535
|
+
chalk16.red(" This could indicate a supply chain attack (tool poisoning / rug pull).")
|
|
11024
11536
|
);
|
|
11025
|
-
console.error(
|
|
11026
|
-
console.error(
|
|
11537
|
+
console.error(chalk16.red(" Session quarantined \u2014 all tool calls blocked."));
|
|
11538
|
+
console.error(chalk16.yellow(` Run: node9 mcp pin update ${serverKey}
|
|
11027
11539
|
`));
|
|
11028
11540
|
const errorResponse = {
|
|
11029
11541
|
jsonrpc: "2.0",
|
|
@@ -11072,9 +11584,9 @@ function registerMcpGatewayCommand(program2) {
|
|
|
11072
11584
|
|
|
11073
11585
|
// src/mcp-server/index.ts
|
|
11074
11586
|
import readline4 from "readline";
|
|
11075
|
-
import
|
|
11076
|
-
import
|
|
11077
|
-
import
|
|
11587
|
+
import fs27 from "fs";
|
|
11588
|
+
import os23 from "os";
|
|
11589
|
+
import path30 from "path";
|
|
11078
11590
|
init_core();
|
|
11079
11591
|
init_daemon();
|
|
11080
11592
|
init_shields();
|
|
@@ -11249,13 +11761,13 @@ function handleStatus() {
|
|
|
11249
11761
|
lines.push(`Active shields: ${activeShields.length > 0 ? activeShields.join(", ") : "none"}`);
|
|
11250
11762
|
lines.push(`Smart rules: ${config.policy.smartRules.length} loaded`);
|
|
11251
11763
|
lines.push(`DLP: ${config.policy.dlp?.enabled !== false ? "enabled" : "disabled"}`);
|
|
11252
|
-
const projectConfig =
|
|
11253
|
-
const globalConfig =
|
|
11764
|
+
const projectConfig = path30.join(process.cwd(), "node9.config.json");
|
|
11765
|
+
const globalConfig = path30.join(os23.homedir(), ".node9", "config.json");
|
|
11254
11766
|
lines.push(
|
|
11255
|
-
`Project config (node9.config.json): ${
|
|
11767
|
+
`Project config (node9.config.json): ${fs27.existsSync(projectConfig) ? "present" : "not found"}`
|
|
11256
11768
|
);
|
|
11257
11769
|
lines.push(
|
|
11258
|
-
`Global config (~/.node9/config.json): ${
|
|
11770
|
+
`Global config (~/.node9/config.json): ${fs27.existsSync(globalConfig) ? "present" : "not found"}`
|
|
11259
11771
|
);
|
|
11260
11772
|
return lines.join("\n");
|
|
11261
11773
|
}
|
|
@@ -11329,21 +11841,21 @@ function handleShieldDisable(args) {
|
|
|
11329
11841
|
writeActiveShields(active.filter((s) => s !== name));
|
|
11330
11842
|
return `Shield "${name}" disabled.`;
|
|
11331
11843
|
}
|
|
11332
|
-
var GLOBAL_CONFIG_PATH2 =
|
|
11844
|
+
var GLOBAL_CONFIG_PATH2 = path30.join(os23.homedir(), ".node9", "config.json");
|
|
11333
11845
|
var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
|
|
11334
11846
|
function readGlobalConfigRaw() {
|
|
11335
11847
|
try {
|
|
11336
|
-
if (
|
|
11337
|
-
return JSON.parse(
|
|
11848
|
+
if (fs27.existsSync(GLOBAL_CONFIG_PATH2)) {
|
|
11849
|
+
return JSON.parse(fs27.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
|
|
11338
11850
|
}
|
|
11339
11851
|
} catch {
|
|
11340
11852
|
}
|
|
11341
11853
|
return {};
|
|
11342
11854
|
}
|
|
11343
11855
|
function writeGlobalConfigRaw(data) {
|
|
11344
|
-
const dir =
|
|
11345
|
-
if (!
|
|
11346
|
-
|
|
11856
|
+
const dir = path30.dirname(GLOBAL_CONFIG_PATH2);
|
|
11857
|
+
if (!fs27.existsSync(dir)) fs27.mkdirSync(dir, { recursive: true });
|
|
11858
|
+
fs27.writeFileSync(GLOBAL_CONFIG_PATH2, JSON.stringify(data, null, 2) + "\n");
|
|
11347
11859
|
}
|
|
11348
11860
|
function handleApproverList() {
|
|
11349
11861
|
const config = getConfig();
|
|
@@ -11386,9 +11898,9 @@ function handleApproverSet(args) {
|
|
|
11386
11898
|
}
|
|
11387
11899
|
function handleAuditGet(args) {
|
|
11388
11900
|
const limit = Math.min(typeof args.limit === "number" ? args.limit : 20, 100);
|
|
11389
|
-
const auditPath =
|
|
11390
|
-
if (!
|
|
11391
|
-
const lines =
|
|
11901
|
+
const auditPath = path30.join(os23.homedir(), ".node9", "audit.log");
|
|
11902
|
+
if (!fs27.existsSync(auditPath)) return "No audit log found.";
|
|
11903
|
+
const lines = fs27.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
|
|
11392
11904
|
const recent = lines.slice(-limit);
|
|
11393
11905
|
const entries = recent.map((line) => {
|
|
11394
11906
|
try {
|
|
@@ -11576,7 +12088,7 @@ function registerMcpServerCommand(program2) {
|
|
|
11576
12088
|
|
|
11577
12089
|
// src/cli/commands/trust.ts
|
|
11578
12090
|
init_trusted_hosts();
|
|
11579
|
-
import
|
|
12091
|
+
import chalk17 from "chalk";
|
|
11580
12092
|
function isValidHost(host) {
|
|
11581
12093
|
return /^(\*\.)?[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/.test(host);
|
|
11582
12094
|
}
|
|
@@ -11586,51 +12098,51 @@ function registerTrustCommand(program2) {
|
|
|
11586
12098
|
const normalized = normalizeHost(host.trim());
|
|
11587
12099
|
if (!isValidHost(normalized)) {
|
|
11588
12100
|
console.error(
|
|
11589
|
-
|
|
12101
|
+
chalk17.red(`
|
|
11590
12102
|
\u274C Invalid host: "${host}"
|
|
11591
|
-
`) +
|
|
12103
|
+
`) + chalk17.gray(" Use an FQDN like api.mycompany.com or *.mycompany.com\n")
|
|
11592
12104
|
);
|
|
11593
12105
|
process.exit(1);
|
|
11594
12106
|
}
|
|
11595
12107
|
addTrustedHost(normalized);
|
|
11596
|
-
console.log(
|
|
12108
|
+
console.log(chalk17.green(`
|
|
11597
12109
|
\u2705 ${normalized} added to trusted hosts.`));
|
|
11598
12110
|
console.log(
|
|
11599
|
-
|
|
12111
|
+
chalk17.gray(" Pipe-chain blocks to this host: critical \u2192 review, high \u2192 allow\n")
|
|
11600
12112
|
);
|
|
11601
12113
|
});
|
|
11602
12114
|
trustCmd.command("remove <host>").description("Remove a trusted host").action((host) => {
|
|
11603
12115
|
const normalized = normalizeHost(host.trim());
|
|
11604
12116
|
const removed = removeTrustedHost(normalized);
|
|
11605
12117
|
if (!removed) {
|
|
11606
|
-
console.error(
|
|
12118
|
+
console.error(chalk17.yellow(`
|
|
11607
12119
|
\u26A0\uFE0F "${normalized}" is not in the trusted hosts list.
|
|
11608
12120
|
`));
|
|
11609
12121
|
process.exit(1);
|
|
11610
12122
|
}
|
|
11611
|
-
console.log(
|
|
12123
|
+
console.log(chalk17.green(`
|
|
11612
12124
|
\u2705 ${normalized} removed from trusted hosts.
|
|
11613
12125
|
`));
|
|
11614
12126
|
});
|
|
11615
12127
|
trustCmd.command("list").description("Show all trusted hosts").action(() => {
|
|
11616
12128
|
const hosts = readTrustedHosts();
|
|
11617
12129
|
if (hosts.length === 0) {
|
|
11618
|
-
console.log(
|
|
11619
|
-
console.log(` Add one: ${
|
|
12130
|
+
console.log(chalk17.gray("\n No trusted hosts configured.\n"));
|
|
12131
|
+
console.log(` Add one: ${chalk17.cyan("node9 trust add api.mycompany.com")}
|
|
11620
12132
|
`);
|
|
11621
12133
|
return;
|
|
11622
12134
|
}
|
|
11623
|
-
console.log(
|
|
12135
|
+
console.log(chalk17.bold("\n\u{1F513} Trusted Hosts\n"));
|
|
11624
12136
|
for (const entry of hosts) {
|
|
11625
12137
|
const date = new Date(entry.addedAt).toLocaleDateString();
|
|
11626
|
-
console.log(` ${
|
|
12138
|
+
console.log(` ${chalk17.cyan(entry.host.padEnd(40))} ${chalk17.gray(`added ${date}`)}`);
|
|
11627
12139
|
}
|
|
11628
12140
|
console.log("");
|
|
11629
12141
|
});
|
|
11630
12142
|
}
|
|
11631
12143
|
|
|
11632
12144
|
// src/cli/commands/mcp-pin.ts
|
|
11633
|
-
import
|
|
12145
|
+
import chalk18 from "chalk";
|
|
11634
12146
|
function registerMcpPinCommand(program2) {
|
|
11635
12147
|
const pinCmd = program2.command("mcp").description("Manage MCP server tool definition pinning (rug pull defense)");
|
|
11636
12148
|
const pinSubCmd = pinCmd.command("pin").description("Manage pinned MCP server tool definitions");
|
|
@@ -11638,31 +12150,31 @@ function registerMcpPinCommand(program2) {
|
|
|
11638
12150
|
const result = readMcpPinsSafe();
|
|
11639
12151
|
if (!result.ok) {
|
|
11640
12152
|
if (result.reason === "missing") {
|
|
11641
|
-
console.log(
|
|
12153
|
+
console.log(chalk18.gray("\nNo MCP servers are pinned yet."));
|
|
11642
12154
|
console.log(
|
|
11643
|
-
|
|
12155
|
+
chalk18.gray("Pins are created automatically when the MCP gateway first connects.\n")
|
|
11644
12156
|
);
|
|
11645
12157
|
return;
|
|
11646
12158
|
}
|
|
11647
|
-
console.error(
|
|
12159
|
+
console.error(chalk18.red(`
|
|
11648
12160
|
\u274C Pin file is corrupt: ${result.detail}`));
|
|
11649
|
-
console.error(
|
|
12161
|
+
console.error(chalk18.yellow(" Run: node9 mcp pin reset\n"));
|
|
11650
12162
|
process.exit(1);
|
|
11651
12163
|
}
|
|
11652
12164
|
const entries = Object.entries(result.pins.servers);
|
|
11653
12165
|
if (entries.length === 0) {
|
|
11654
|
-
console.log(
|
|
12166
|
+
console.log(chalk18.gray("\nNo MCP servers are pinned yet."));
|
|
11655
12167
|
console.log(
|
|
11656
|
-
|
|
12168
|
+
chalk18.gray("Pins are created automatically when the MCP gateway first connects.\n")
|
|
11657
12169
|
);
|
|
11658
12170
|
return;
|
|
11659
12171
|
}
|
|
11660
|
-
console.log(
|
|
12172
|
+
console.log(chalk18.bold("\n\u{1F512} Pinned MCP Servers\n"));
|
|
11661
12173
|
for (const [key, entry] of entries) {
|
|
11662
|
-
console.log(` ${
|
|
11663
|
-
console.log(` Tools (${entry.toolCount}): ${
|
|
11664
|
-
console.log(` Hash: ${
|
|
11665
|
-
console.log(` Pinned: ${
|
|
12174
|
+
console.log(` ${chalk18.cyan(key)} ${chalk18.gray(entry.label)}`);
|
|
12175
|
+
console.log(` Tools (${entry.toolCount}): ${chalk18.white(entry.toolNames.join(", "))}`);
|
|
12176
|
+
console.log(` Hash: ${chalk18.gray(entry.toolsHash.slice(0, 16))}...`);
|
|
12177
|
+
console.log(` Pinned: ${chalk18.gray(entry.pinnedAt)}`);
|
|
11666
12178
|
console.log("");
|
|
11667
12179
|
}
|
|
11668
12180
|
});
|
|
@@ -11673,55 +12185,55 @@ function registerMcpPinCommand(program2) {
|
|
|
11673
12185
|
try {
|
|
11674
12186
|
pins = readMcpPins();
|
|
11675
12187
|
} catch {
|
|
11676
|
-
console.error(
|
|
11677
|
-
console.error(
|
|
12188
|
+
console.error(chalk18.red("\n\u274C Pin file is corrupt."));
|
|
12189
|
+
console.error(chalk18.yellow(" Run: node9 mcp pin reset\n"));
|
|
11678
12190
|
process.exit(1);
|
|
11679
12191
|
}
|
|
11680
12192
|
if (!pins.servers[serverKey]) {
|
|
11681
|
-
console.error(
|
|
12193
|
+
console.error(chalk18.red(`
|
|
11682
12194
|
\u274C No pin found for server key "${serverKey}"
|
|
11683
12195
|
`));
|
|
11684
|
-
console.error(`Run ${
|
|
12196
|
+
console.error(`Run ${chalk18.cyan("node9 mcp pin list")} to see pinned servers.
|
|
11685
12197
|
`);
|
|
11686
12198
|
process.exit(1);
|
|
11687
12199
|
}
|
|
11688
12200
|
const label = pins.servers[serverKey].label;
|
|
11689
12201
|
removePin(serverKey);
|
|
11690
|
-
console.log(
|
|
11691
|
-
\u{1F513} Pin removed for ${
|
|
11692
|
-
console.log(
|
|
11693
|
-
console.log(
|
|
12202
|
+
console.log(chalk18.green(`
|
|
12203
|
+
\u{1F513} Pin removed for ${chalk18.cyan(serverKey)}`));
|
|
12204
|
+
console.log(chalk18.gray(` Server: ${label}`));
|
|
12205
|
+
console.log(chalk18.gray(" Next connection will re-pin with current tool definitions.\n"));
|
|
11694
12206
|
});
|
|
11695
12207
|
pinSubCmd.command("reset").description("Clear all MCP pins (next connection to each server will re-pin)").action(() => {
|
|
11696
12208
|
const result = readMcpPinsSafe();
|
|
11697
12209
|
if (!result.ok && result.reason === "missing") {
|
|
11698
|
-
console.log(
|
|
12210
|
+
console.log(chalk18.gray("\nNo pins to clear.\n"));
|
|
11699
12211
|
return;
|
|
11700
12212
|
}
|
|
11701
12213
|
const count = result.ok ? Object.keys(result.pins.servers).length : "?";
|
|
11702
12214
|
clearAllPins();
|
|
11703
|
-
console.log(
|
|
12215
|
+
console.log(chalk18.green(`
|
|
11704
12216
|
\u{1F513} Cleared ${count} MCP pin(s).`));
|
|
11705
|
-
console.log(
|
|
12217
|
+
console.log(chalk18.gray(" Next connection to each server will re-pin.\n"));
|
|
11706
12218
|
});
|
|
11707
12219
|
}
|
|
11708
12220
|
|
|
11709
12221
|
// src/cli.ts
|
|
11710
12222
|
var { version } = JSON.parse(
|
|
11711
|
-
|
|
12223
|
+
fs30.readFileSync(path33.join(__dirname, "../package.json"), "utf-8")
|
|
11712
12224
|
);
|
|
11713
12225
|
var program = new Command();
|
|
11714
12226
|
program.name("node9").description("The Sudo Command for AI Agents").version(version);
|
|
11715
12227
|
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
12228
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
11717
|
-
const credPath =
|
|
11718
|
-
if (!
|
|
11719
|
-
|
|
12229
|
+
const credPath = path33.join(os26.homedir(), ".node9", "credentials.json");
|
|
12230
|
+
if (!fs30.existsSync(path33.dirname(credPath)))
|
|
12231
|
+
fs30.mkdirSync(path33.dirname(credPath), { recursive: true });
|
|
11720
12232
|
const profileName = options.profile || "default";
|
|
11721
12233
|
let existingCreds = {};
|
|
11722
12234
|
try {
|
|
11723
|
-
if (
|
|
11724
|
-
const raw = JSON.parse(
|
|
12235
|
+
if (fs30.existsSync(credPath)) {
|
|
12236
|
+
const raw = JSON.parse(fs30.readFileSync(credPath, "utf-8"));
|
|
11725
12237
|
if (raw.apiKey) {
|
|
11726
12238
|
existingCreds = {
|
|
11727
12239
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
|
|
@@ -11733,13 +12245,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
11733
12245
|
} catch {
|
|
11734
12246
|
}
|
|
11735
12247
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
11736
|
-
|
|
12248
|
+
fs30.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
11737
12249
|
if (profileName === "default") {
|
|
11738
|
-
const configPath =
|
|
12250
|
+
const configPath = path33.join(os26.homedir(), ".node9", "config.json");
|
|
11739
12251
|
let config = {};
|
|
11740
12252
|
try {
|
|
11741
|
-
if (
|
|
11742
|
-
config = JSON.parse(
|
|
12253
|
+
if (fs30.existsSync(configPath))
|
|
12254
|
+
config = JSON.parse(fs30.readFileSync(configPath, "utf-8"));
|
|
11743
12255
|
} catch {
|
|
11744
12256
|
}
|
|
11745
12257
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -11754,19 +12266,19 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
11754
12266
|
approvers.cloud = false;
|
|
11755
12267
|
}
|
|
11756
12268
|
s.approvers = approvers;
|
|
11757
|
-
if (!
|
|
11758
|
-
|
|
11759
|
-
|
|
12269
|
+
if (!fs30.existsSync(path33.dirname(configPath)))
|
|
12270
|
+
fs30.mkdirSync(path33.dirname(configPath), { recursive: true });
|
|
12271
|
+
fs30.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
11760
12272
|
}
|
|
11761
12273
|
if (options.profile && profileName !== "default") {
|
|
11762
|
-
console.log(
|
|
11763
|
-
console.log(
|
|
12274
|
+
console.log(chalk20.green(`\u2705 Profile "${profileName}" saved`));
|
|
12275
|
+
console.log(chalk20.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
11764
12276
|
} else if (options.local) {
|
|
11765
|
-
console.log(
|
|
11766
|
-
console.log(
|
|
12277
|
+
console.log(chalk20.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
12278
|
+
console.log(chalk20.gray(` All decisions stay on this machine.`));
|
|
11767
12279
|
} else {
|
|
11768
|
-
console.log(
|
|
11769
|
-
console.log(
|
|
12280
|
+
console.log(chalk20.green(`\u2705 Logged in \u2014 agent mode`));
|
|
12281
|
+
console.log(chalk20.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
11770
12282
|
}
|
|
11771
12283
|
});
|
|
11772
12284
|
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 +12286,19 @@ program.command("addto").description("Integrate Node9 with an AI agent").addHelp
|
|
|
11774
12286
|
if (target === "claude") return await setupClaude();
|
|
11775
12287
|
if (target === "cursor") return await setupCursor();
|
|
11776
12288
|
if (target === "hud") return setupHud();
|
|
11777
|
-
console.error(
|
|
12289
|
+
console.error(chalk20.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
|
|
11778
12290
|
process.exit(1);
|
|
11779
12291
|
});
|
|
11780
12292
|
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
12293
|
if (!target) {
|
|
11782
|
-
console.log(
|
|
11783
|
-
console.log(" Usage: " +
|
|
12294
|
+
console.log(chalk20.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
12295
|
+
console.log(" Usage: " + chalk20.white("node9 setup <target>") + "\n");
|
|
11784
12296
|
console.log(" Targets:");
|
|
11785
|
-
console.log(" " +
|
|
11786
|
-
console.log(" " +
|
|
11787
|
-
console.log(" " +
|
|
12297
|
+
console.log(" " + chalk20.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
12298
|
+
console.log(" " + chalk20.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
12299
|
+
console.log(" " + chalk20.green("cursor") + " \u2014 Cursor (hook mode)");
|
|
11788
12300
|
process.stdout.write(
|
|
11789
|
-
" " +
|
|
12301
|
+
" " + chalk20.green("hud") + " \u2014 Claude Code security statusline\n"
|
|
11790
12302
|
);
|
|
11791
12303
|
console.log("");
|
|
11792
12304
|
return;
|
|
@@ -11796,7 +12308,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
11796
12308
|
if (t === "claude") return await setupClaude();
|
|
11797
12309
|
if (t === "cursor") return await setupCursor();
|
|
11798
12310
|
if (t === "hud") return setupHud();
|
|
11799
|
-
console.error(
|
|
12311
|
+
console.error(chalk20.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
|
|
11800
12312
|
process.exit(1);
|
|
11801
12313
|
});
|
|
11802
12314
|
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 +12319,31 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
|
|
|
11807
12319
|
else if (target === "hud") fn = teardownHud;
|
|
11808
12320
|
else {
|
|
11809
12321
|
console.error(
|
|
11810
|
-
|
|
12322
|
+
chalk20.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`)
|
|
11811
12323
|
);
|
|
11812
12324
|
process.exit(1);
|
|
11813
12325
|
}
|
|
11814
|
-
console.log(
|
|
12326
|
+
console.log(chalk20.cyan(`
|
|
11815
12327
|
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
11816
12328
|
`));
|
|
11817
12329
|
try {
|
|
11818
12330
|
fn();
|
|
11819
12331
|
} catch (err2) {
|
|
11820
|
-
console.error(
|
|
12332
|
+
console.error(chalk20.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
11821
12333
|
process.exit(1);
|
|
11822
12334
|
}
|
|
11823
|
-
console.log(
|
|
12335
|
+
console.log(chalk20.gray("\n Restart the agent for changes to take effect."));
|
|
11824
12336
|
});
|
|
11825
12337
|
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(
|
|
12338
|
+
console.log(chalk20.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
12339
|
+
console.log(chalk20.bold("Stopping daemon..."));
|
|
11828
12340
|
try {
|
|
11829
12341
|
stopDaemon();
|
|
11830
|
-
console.log(
|
|
12342
|
+
console.log(chalk20.green(" \u2705 Daemon stopped"));
|
|
11831
12343
|
} catch {
|
|
11832
|
-
console.log(
|
|
12344
|
+
console.log(chalk20.blue(" \u2139\uFE0F Daemon was not running"));
|
|
11833
12345
|
}
|
|
11834
|
-
console.log(
|
|
12346
|
+
console.log(chalk20.bold("\nRemoving hooks..."));
|
|
11835
12347
|
let teardownFailed = false;
|
|
11836
12348
|
for (const [label, fn] of [
|
|
11837
12349
|
["Claude", teardownClaude],
|
|
@@ -11843,45 +12355,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
11843
12355
|
} catch (err2) {
|
|
11844
12356
|
teardownFailed = true;
|
|
11845
12357
|
console.error(
|
|
11846
|
-
|
|
12358
|
+
chalk20.red(
|
|
11847
12359
|
` \u26A0\uFE0F Failed to remove ${label} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
11848
12360
|
)
|
|
11849
12361
|
);
|
|
11850
12362
|
}
|
|
11851
12363
|
}
|
|
11852
12364
|
if (options.purge) {
|
|
11853
|
-
const node9Dir =
|
|
11854
|
-
if (
|
|
12365
|
+
const node9Dir = path33.join(os26.homedir(), ".node9");
|
|
12366
|
+
if (fs30.existsSync(node9Dir)) {
|
|
11855
12367
|
const confirmed = await confirm2({
|
|
11856
12368
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
11857
12369
|
default: false
|
|
11858
12370
|
});
|
|
11859
12371
|
if (confirmed) {
|
|
11860
|
-
|
|
11861
|
-
if (
|
|
12372
|
+
fs30.rmSync(node9Dir, { recursive: true });
|
|
12373
|
+
if (fs30.existsSync(node9Dir)) {
|
|
11862
12374
|
console.error(
|
|
11863
|
-
|
|
12375
|
+
chalk20.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
11864
12376
|
);
|
|
11865
12377
|
} else {
|
|
11866
|
-
console.log(
|
|
12378
|
+
console.log(chalk20.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
11867
12379
|
}
|
|
11868
12380
|
} else {
|
|
11869
|
-
console.log(
|
|
12381
|
+
console.log(chalk20.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
11870
12382
|
}
|
|
11871
12383
|
} else {
|
|
11872
|
-
console.log(
|
|
12384
|
+
console.log(chalk20.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
11873
12385
|
}
|
|
11874
12386
|
} else {
|
|
11875
12387
|
console.log(
|
|
11876
|
-
|
|
12388
|
+
chalk20.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
11877
12389
|
);
|
|
11878
12390
|
}
|
|
11879
12391
|
if (teardownFailed) {
|
|
11880
|
-
console.error(
|
|
12392
|
+
console.error(chalk20.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
11881
12393
|
process.exit(1);
|
|
11882
12394
|
}
|
|
11883
|
-
console.log(
|
|
11884
|
-
console.log(
|
|
12395
|
+
console.log(chalk20.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
|
|
12396
|
+
console.log(chalk20.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
|
|
11885
12397
|
});
|
|
11886
12398
|
registerDoctorCommand(program, version);
|
|
11887
12399
|
program.command("explain").description(
|
|
@@ -11894,7 +12406,7 @@ program.command("explain").description(
|
|
|
11894
12406
|
try {
|
|
11895
12407
|
args = JSON.parse(trimmed);
|
|
11896
12408
|
} catch {
|
|
11897
|
-
console.error(
|
|
12409
|
+
console.error(chalk20.red(`
|
|
11898
12410
|
\u274C Invalid JSON: ${trimmed}
|
|
11899
12411
|
`));
|
|
11900
12412
|
process.exit(1);
|
|
@@ -11905,60 +12417,61 @@ program.command("explain").description(
|
|
|
11905
12417
|
}
|
|
11906
12418
|
const result = await explainPolicy(tool, args);
|
|
11907
12419
|
console.log("");
|
|
11908
|
-
console.log(
|
|
12420
|
+
console.log(chalk20.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
11909
12421
|
console.log("");
|
|
11910
|
-
console.log(` ${
|
|
12422
|
+
console.log(` ${chalk20.bold("Tool:")} ${chalk20.white(result.tool)}`);
|
|
11911
12423
|
if (argsRaw) {
|
|
11912
12424
|
const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
11913
|
-
console.log(` ${
|
|
12425
|
+
console.log(` ${chalk20.bold("Input:")} ${chalk20.gray(preview)}`);
|
|
11914
12426
|
}
|
|
11915
12427
|
console.log("");
|
|
11916
|
-
console.log(
|
|
12428
|
+
console.log(chalk20.bold("Config Sources (Waterfall):"));
|
|
11917
12429
|
for (const tier of result.waterfall) {
|
|
11918
|
-
const
|
|
12430
|
+
const num2 = chalk20.gray(` ${tier.tier}.`);
|
|
11919
12431
|
const label = tier.label.padEnd(16);
|
|
11920
12432
|
let statusStr;
|
|
11921
12433
|
if (tier.tier === 1) {
|
|
11922
|
-
statusStr =
|
|
12434
|
+
statusStr = chalk20.gray(tier.note ?? "");
|
|
11923
12435
|
} else if (tier.status === "active") {
|
|
11924
|
-
const loc = tier.path ?
|
|
11925
|
-
const note = tier.note ?
|
|
11926
|
-
statusStr =
|
|
12436
|
+
const loc = tier.path ? chalk20.gray(tier.path) : "";
|
|
12437
|
+
const note = tier.note ? chalk20.gray(`(${tier.note})`) : "";
|
|
12438
|
+
statusStr = chalk20.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
|
|
11927
12439
|
} else {
|
|
11928
|
-
statusStr =
|
|
12440
|
+
statusStr = chalk20.gray("\u25CB " + (tier.note ?? "not found"));
|
|
11929
12441
|
}
|
|
11930
|
-
console.log(`${
|
|
12442
|
+
console.log(`${num2} ${chalk20.white(label)} ${statusStr}`);
|
|
11931
12443
|
}
|
|
11932
12444
|
console.log("");
|
|
11933
|
-
console.log(
|
|
12445
|
+
console.log(chalk20.bold("Policy Evaluation:"));
|
|
11934
12446
|
for (const step of result.steps) {
|
|
11935
12447
|
const isFinal = step.isFinal;
|
|
11936
12448
|
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 =
|
|
12449
|
+
if (step.outcome === "allow") icon = chalk20.green(" \u2705");
|
|
12450
|
+
else if (step.outcome === "review") icon = chalk20.red(" \u{1F534}");
|
|
12451
|
+
else if (step.outcome === "skip") icon = chalk20.gray(" \u2500 ");
|
|
12452
|
+
else icon = chalk20.gray(" \u25CB ");
|
|
11941
12453
|
const name = step.name.padEnd(18);
|
|
11942
|
-
const nameStr = isFinal ?
|
|
11943
|
-
const detail = isFinal ?
|
|
11944
|
-
const arrow = isFinal ?
|
|
12454
|
+
const nameStr = isFinal ? chalk20.white.bold(name) : chalk20.white(name);
|
|
12455
|
+
const detail = isFinal ? chalk20.white(step.detail) : chalk20.gray(step.detail);
|
|
12456
|
+
const arrow = isFinal ? chalk20.yellow(" \u2190 STOP") : "";
|
|
11945
12457
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
11946
12458
|
}
|
|
11947
12459
|
console.log("");
|
|
11948
12460
|
if (result.decision === "allow") {
|
|
11949
|
-
console.log(
|
|
12461
|
+
console.log(chalk20.green.bold(" Decision: \u2705 ALLOW") + chalk20.gray(" \u2014 no approval needed"));
|
|
11950
12462
|
} else {
|
|
11951
12463
|
console.log(
|
|
11952
|
-
|
|
12464
|
+
chalk20.red.bold(" Decision: \u{1F534} REVIEW") + chalk20.gray(" \u2014 human approval required")
|
|
11953
12465
|
);
|
|
11954
12466
|
if (result.blockedByLabel) {
|
|
11955
|
-
console.log(
|
|
12467
|
+
console.log(chalk20.gray(` Reason: ${result.blockedByLabel}`));
|
|
11956
12468
|
}
|
|
11957
12469
|
}
|
|
11958
12470
|
console.log("");
|
|
11959
12471
|
});
|
|
11960
12472
|
registerInitCommand(program);
|
|
11961
12473
|
registerAuditCommand(program);
|
|
12474
|
+
registerReportCommand(program);
|
|
11962
12475
|
registerStatusCommand(program);
|
|
11963
12476
|
registerDaemonCommand(program);
|
|
11964
12477
|
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 +12479,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
|
|
|
11966
12479
|
try {
|
|
11967
12480
|
await startTail2(options);
|
|
11968
12481
|
} catch (err2) {
|
|
11969
|
-
console.error(
|
|
12482
|
+
console.error(chalk20.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
11970
12483
|
process.exit(1);
|
|
11971
12484
|
}
|
|
11972
12485
|
});
|
|
@@ -11998,14 +12511,14 @@ Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
|
|
|
11998
12511
|
Run "node9 addto claude" to register it as the statusLine.`
|
|
11999
12512
|
).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
12513
|
if (subcommand === "debug") {
|
|
12001
|
-
const flagFile =
|
|
12514
|
+
const flagFile = path33.join(os26.homedir(), ".node9", "hud-debug");
|
|
12002
12515
|
if (state === "on") {
|
|
12003
|
-
|
|
12004
|
-
|
|
12516
|
+
fs30.mkdirSync(path33.dirname(flagFile), { recursive: true });
|
|
12517
|
+
fs30.writeFileSync(flagFile, "");
|
|
12005
12518
|
console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
|
|
12006
12519
|
console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
|
|
12007
12520
|
} else if (state === "off") {
|
|
12008
|
-
if (
|
|
12521
|
+
if (fs30.existsSync(flagFile)) fs30.unlinkSync(flagFile);
|
|
12009
12522
|
console.log("HUD debug logging disabled.");
|
|
12010
12523
|
} else {
|
|
12011
12524
|
console.error("Usage: node9 hud debug on|off");
|
|
@@ -12020,7 +12533,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
12020
12533
|
const ms = parseDuration(options.duration);
|
|
12021
12534
|
if (ms === null) {
|
|
12022
12535
|
console.error(
|
|
12023
|
-
|
|
12536
|
+
chalk20.red(`
|
|
12024
12537
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
12025
12538
|
`)
|
|
12026
12539
|
);
|
|
@@ -12028,20 +12541,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
12028
12541
|
}
|
|
12029
12542
|
pauseNode9(ms, options.duration);
|
|
12030
12543
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
12031
|
-
console.log(
|
|
12544
|
+
console.log(chalk20.yellow(`
|
|
12032
12545
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
12033
|
-
console.log(
|
|
12034
|
-
console.log(
|
|
12546
|
+
console.log(chalk20.gray(` All tool calls will be allowed without review.`));
|
|
12547
|
+
console.log(chalk20.gray(` Run "node9 resume" to re-enable early.
|
|
12035
12548
|
`));
|
|
12036
12549
|
});
|
|
12037
12550
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
12038
12551
|
const { paused } = checkPause();
|
|
12039
12552
|
if (!paused) {
|
|
12040
|
-
console.log(
|
|
12553
|
+
console.log(chalk20.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
12041
12554
|
return;
|
|
12042
12555
|
}
|
|
12043
12556
|
resumeNode9();
|
|
12044
|
-
console.log(
|
|
12557
|
+
console.log(chalk20.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
12045
12558
|
});
|
|
12046
12559
|
var HOOK_BASED_AGENTS = {
|
|
12047
12560
|
claude: "claude",
|
|
@@ -12054,15 +12567,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
12054
12567
|
if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
|
|
12055
12568
|
const target = HOOK_BASED_AGENTS[firstArg2];
|
|
12056
12569
|
console.error(
|
|
12057
|
-
|
|
12570
|
+
chalk20.yellow(`
|
|
12058
12571
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
12059
12572
|
);
|
|
12060
|
-
console.error(
|
|
12573
|
+
console.error(chalk20.white(`
|
|
12061
12574
|
"${target}" uses its own hook system. Use:`));
|
|
12062
12575
|
console.error(
|
|
12063
|
-
|
|
12576
|
+
chalk20.green(` node9 addto ${target} `) + chalk20.gray("# one-time setup")
|
|
12064
12577
|
);
|
|
12065
|
-
console.error(
|
|
12578
|
+
console.error(chalk20.green(` ${target} `) + chalk20.gray("# run normally"));
|
|
12066
12579
|
process.exit(1);
|
|
12067
12580
|
}
|
|
12068
12581
|
const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
|
|
@@ -12079,7 +12592,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
12079
12592
|
}
|
|
12080
12593
|
);
|
|
12081
12594
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
12082
|
-
console.error(
|
|
12595
|
+
console.error(chalk20.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
12083
12596
|
const daemonReady = await autoStartDaemonAndWait();
|
|
12084
12597
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
12085
12598
|
}
|
|
@@ -12092,12 +12605,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
12092
12605
|
}
|
|
12093
12606
|
if (!result.approved) {
|
|
12094
12607
|
console.error(
|
|
12095
|
-
|
|
12608
|
+
chalk20.red(`
|
|
12096
12609
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
12097
12610
|
);
|
|
12098
12611
|
process.exit(1);
|
|
12099
12612
|
}
|
|
12100
|
-
console.error(
|
|
12613
|
+
console.error(chalk20.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
12101
12614
|
await runProxy(fullCommand);
|
|
12102
12615
|
} else {
|
|
12103
12616
|
program.help();
|
|
@@ -12112,9 +12625,9 @@ if (process.argv[2] !== "daemon") {
|
|
|
12112
12625
|
const isCheckHook = process.argv[2] === "check";
|
|
12113
12626
|
if (isCheckHook) {
|
|
12114
12627
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
12115
|
-
const logPath =
|
|
12628
|
+
const logPath = path33.join(os26.homedir(), ".node9", "hook-debug.log");
|
|
12116
12629
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
12117
|
-
|
|
12630
|
+
fs30.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
12118
12631
|
`);
|
|
12119
12632
|
}
|
|
12120
12633
|
process.exit(0);
|