@node9/proxy 1.9.2 → 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 +56 -7
- package/dist/cli.js +1154 -623
- package/dist/cli.mjs +1136 -605
- package/dist/index.js +127 -23
- package/dist/index.mjs +125 -21
- 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,
|
|
@@ -192,6 +200,7 @@ var init_config_schema = __esm({
|
|
|
192
200
|
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
193
201
|
}),
|
|
194
202
|
reason: z.string().optional(),
|
|
203
|
+
description: z.string().optional(),
|
|
195
204
|
// Unknown predicate names are filtered out rather than failing the whole rule.
|
|
196
205
|
// Failing the whole z.array() would cause sanitizeConfig to drop the entire
|
|
197
206
|
// `policy` top-level key, silently disabling ALL smart rules in the config.
|
|
@@ -238,6 +247,11 @@ var init_config_schema = __esm({
|
|
|
238
247
|
dlp: z.object({
|
|
239
248
|
enabled: z.boolean().optional(),
|
|
240
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()
|
|
241
255
|
}).optional()
|
|
242
256
|
}).optional(),
|
|
243
257
|
environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
@@ -535,7 +549,8 @@ function getConfig(cwd) {
|
|
|
535
549
|
onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
|
|
536
550
|
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
537
551
|
},
|
|
538
|
-
dlp: { ...DEFAULT_CONFIG.policy.dlp }
|
|
552
|
+
dlp: { ...DEFAULT_CONFIG.policy.dlp },
|
|
553
|
+
loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection }
|
|
539
554
|
};
|
|
540
555
|
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
541
556
|
const applyLayer = (source) => {
|
|
@@ -574,6 +589,13 @@ function getConfig(cwd) {
|
|
|
574
589
|
if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
|
|
575
590
|
if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
|
|
576
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
|
+
}
|
|
577
599
|
const envs = source.environments || {};
|
|
578
600
|
for (const [envName, envConfig] of Object.entries(envs)) {
|
|
579
601
|
if (envConfig && typeof envConfig === "object") {
|
|
@@ -769,7 +791,8 @@ var init_config = __esm({
|
|
|
769
791
|
}
|
|
770
792
|
],
|
|
771
793
|
verdict: "block",
|
|
772
|
-
reason: "Recursive delete of home directory is irreversible"
|
|
794
|
+
reason: "Recursive delete of home directory is irreversible",
|
|
795
|
+
description: "The AI wants to recursively delete your home directory. This will permanently destroy all your personal files and cannot be undone."
|
|
773
796
|
},
|
|
774
797
|
// ── SQL safety ────────────────────────────────────────────────────────
|
|
775
798
|
{
|
|
@@ -781,7 +804,8 @@ var init_config = __esm({
|
|
|
781
804
|
],
|
|
782
805
|
conditionMode: "all",
|
|
783
806
|
verdict: "review",
|
|
784
|
-
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
|
|
807
|
+
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table",
|
|
808
|
+
description: "The AI is running a SQL statement that will modify every row in the table \u2014 no WHERE filter was found. This could wipe or corrupt all your data."
|
|
785
809
|
},
|
|
786
810
|
{
|
|
787
811
|
name: "review-drop-truncate-shell",
|
|
@@ -796,7 +820,8 @@ var init_config = __esm({
|
|
|
796
820
|
],
|
|
797
821
|
conditionMode: "all",
|
|
798
822
|
verdict: "review",
|
|
799
|
-
reason: "SQL DDL destructive statement inside a shell command"
|
|
823
|
+
reason: "SQL DDL destructive statement inside a shell command",
|
|
824
|
+
description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
|
|
800
825
|
},
|
|
801
826
|
// ── Git safety ────────────────────────────────────────────────────────
|
|
802
827
|
{
|
|
@@ -812,7 +837,8 @@ var init_config = __esm({
|
|
|
812
837
|
],
|
|
813
838
|
conditionMode: "all",
|
|
814
839
|
verdict: "block",
|
|
815
|
-
reason: "Force push overwrites remote history and cannot be undone"
|
|
840
|
+
reason: "Force push overwrites remote history and cannot be undone",
|
|
841
|
+
description: "The AI wants to force push to a remote git branch. This rewrites shared history and can permanently destroy commits that teammates have already pulled."
|
|
816
842
|
},
|
|
817
843
|
{
|
|
818
844
|
name: "review-git-push",
|
|
@@ -827,7 +853,8 @@ var init_config = __esm({
|
|
|
827
853
|
],
|
|
828
854
|
conditionMode: "all",
|
|
829
855
|
verdict: "review",
|
|
830
|
-
reason: "git push sends changes to a shared remote"
|
|
856
|
+
reason: "git push sends changes to a shared remote",
|
|
857
|
+
description: "The AI wants to push commits to a remote repository. Once pushed, those changes are visible to everyone with access."
|
|
831
858
|
},
|
|
832
859
|
{
|
|
833
860
|
name: "review-git-destructive",
|
|
@@ -842,7 +869,8 @@ var init_config = __esm({
|
|
|
842
869
|
],
|
|
843
870
|
conditionMode: "all",
|
|
844
871
|
verdict: "review",
|
|
845
|
-
reason: "Destructive git operation \u2014 discards history or working-tree changes"
|
|
872
|
+
reason: "Destructive git operation \u2014 discards history or working-tree changes",
|
|
873
|
+
description: "The AI wants to run a destructive git operation (reset, rebase, clean, or branch delete) that can permanently discard commits or uncommitted work."
|
|
846
874
|
},
|
|
847
875
|
// ── Shell safety ──────────────────────────────────────────────────────
|
|
848
876
|
{
|
|
@@ -851,7 +879,8 @@ var init_config = __esm({
|
|
|
851
879
|
conditions: [{ field: "command", op: "matches", value: "\\bsudo\\s", flags: "i" }],
|
|
852
880
|
conditionMode: "all",
|
|
853
881
|
verdict: "review",
|
|
854
|
-
reason: "Command requires elevated privileges"
|
|
882
|
+
reason: "Command requires elevated privileges",
|
|
883
|
+
description: "The AI wants to run a command as root (sudo). Commands with root access can modify system files, install software, or change security settings."
|
|
855
884
|
},
|
|
856
885
|
{
|
|
857
886
|
name: "review-curl-pipe-shell",
|
|
@@ -866,10 +895,12 @@ var init_config = __esm({
|
|
|
866
895
|
],
|
|
867
896
|
conditionMode: "all",
|
|
868
897
|
verdict: "block",
|
|
869
|
-
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
898
|
+
reason: "Piping remote script into a shell is a supply-chain attack vector",
|
|
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."
|
|
870
900
|
}
|
|
871
901
|
],
|
|
872
|
-
dlp: { enabled: true, scanIgnoredTools: true }
|
|
902
|
+
dlp: { enabled: true, scanIgnoredTools: true },
|
|
903
|
+
loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 }
|
|
873
904
|
},
|
|
874
905
|
environments: {}
|
|
875
906
|
};
|
|
@@ -899,7 +930,8 @@ var init_config = __esm({
|
|
|
899
930
|
tool: "*",
|
|
900
931
|
conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
|
|
901
932
|
verdict: "review",
|
|
902
|
-
reason: "rm can permanently delete files \u2014 confirm the target path"
|
|
933
|
+
reason: "rm can permanently delete files \u2014 confirm the target path",
|
|
934
|
+
description: "The AI wants to delete files. Unlike moving to trash, rm is permanent \u2014 the files cannot be recovered without a backup."
|
|
903
935
|
},
|
|
904
936
|
// ── SQL safety (Safe by Default) ──────────────────────────────────────────
|
|
905
937
|
// These rules fire when an AI calls a database tool directly (e.g. MCP postgres,
|
|
@@ -911,14 +943,16 @@ var init_config = __esm({
|
|
|
911
943
|
tool: "*",
|
|
912
944
|
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
913
945
|
verdict: "review",
|
|
914
|
-
reason: "DROP TABLE is irreversible \u2014 enable the postgres shield to block instead"
|
|
946
|
+
reason: "DROP TABLE is irreversible \u2014 enable the postgres shield to block instead",
|
|
947
|
+
description: "The AI wants to drop a database table. This permanently deletes the table and all its data \u2014 there is no undo."
|
|
915
948
|
},
|
|
916
949
|
{
|
|
917
950
|
name: "review-truncate-sql",
|
|
918
951
|
tool: "*",
|
|
919
952
|
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
920
953
|
verdict: "review",
|
|
921
|
-
reason: "TRUNCATE removes all rows \u2014 enable the postgres shield to block instead"
|
|
954
|
+
reason: "TRUNCATE removes all rows \u2014 enable the postgres shield to block instead",
|
|
955
|
+
description: "The AI wants to truncate a database table, which instantly deletes every row. The table structure remains but all data is gone."
|
|
922
956
|
},
|
|
923
957
|
{
|
|
924
958
|
name: "review-drop-column-sql",
|
|
@@ -927,7 +961,8 @@ var init_config = __esm({
|
|
|
927
961
|
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
928
962
|
],
|
|
929
963
|
verdict: "review",
|
|
930
|
-
reason: "DROP COLUMN is irreversible \u2014 enable the postgres shield to block instead"
|
|
964
|
+
reason: "DROP COLUMN is irreversible \u2014 enable the postgres shield to block instead",
|
|
965
|
+
description: "The AI wants to drop a column from a database table. This permanently removes the column and all its data from every row."
|
|
931
966
|
}
|
|
932
967
|
];
|
|
933
968
|
cachedConfig = null;
|
|
@@ -1674,9 +1709,9 @@ function matchesPattern(text, patterns) {
|
|
|
1674
1709
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1675
1710
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1676
1711
|
}
|
|
1677
|
-
function getNestedValue(obj,
|
|
1712
|
+
function getNestedValue(obj, path34) {
|
|
1678
1713
|
if (!obj || typeof obj !== "object") return null;
|
|
1679
|
-
return
|
|
1714
|
+
return path34.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1680
1715
|
}
|
|
1681
1716
|
function shouldSnapshot(toolName, args, config) {
|
|
1682
1717
|
if (!config.settings.enableUndo) return false;
|
|
@@ -1827,6 +1862,9 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1827
1862
|
reason: matchedRule.reason,
|
|
1828
1863
|
tier: 2,
|
|
1829
1864
|
ruleName: matchedRule.name ?? matchedRule.tool,
|
|
1865
|
+
...(matchedRule.description ?? matchedRule.reason) && {
|
|
1866
|
+
ruleDescription: matchedRule.description ?? matchedRule.reason
|
|
1867
|
+
},
|
|
1830
1868
|
...matchedRule.verdict === "block" && matchedRule.dependsOnState?.length && {
|
|
1831
1869
|
dependsOnStatePredicates: matchedRule.dependsOnState
|
|
1832
1870
|
},
|
|
@@ -3040,6 +3078,58 @@ var init_cloud = __esm({
|
|
|
3040
3078
|
}
|
|
3041
3079
|
});
|
|
3042
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
|
+
|
|
3043
3133
|
// src/auth/orchestrator.ts
|
|
3044
3134
|
import { randomUUID } from "crypto";
|
|
3045
3135
|
function isWriteTool(toolName) {
|
|
@@ -3128,6 +3218,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3128
3218
|
let explainableLabel = "Local Config";
|
|
3129
3219
|
let policyMatchedField;
|
|
3130
3220
|
let policyMatchedWord;
|
|
3221
|
+
let policyRuleDescription;
|
|
3131
3222
|
let riskMetadata;
|
|
3132
3223
|
let statefulRecoveryCommand;
|
|
3133
3224
|
let localSmartRuleMatched = false;
|
|
@@ -3221,6 +3312,21 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3221
3312
|
return { approved: true, checkedBy: "audit" };
|
|
3222
3313
|
}
|
|
3223
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
|
+
}
|
|
3224
3330
|
if (getActiveTrustSession(toolName)) {
|
|
3225
3331
|
if (approvers.cloud && creds?.apiKey)
|
|
3226
3332
|
await auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
@@ -3267,7 +3373,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3267
3373
|
blockedBy: "local-config",
|
|
3268
3374
|
blockedByLabel: policyResult.blockedByLabel,
|
|
3269
3375
|
ruleHit: policyResult.ruleName,
|
|
3270
|
-
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand }
|
|
3376
|
+
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
|
|
3377
|
+
...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }
|
|
3271
3378
|
};
|
|
3272
3379
|
}
|
|
3273
3380
|
}
|
|
@@ -3275,6 +3382,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3275
3382
|
policyMatchedField = policyResult.matchedField;
|
|
3276
3383
|
policyMatchedWord = policyResult.matchedWord;
|
|
3277
3384
|
if (policyResult.ruleName) localSmartRuleMatched = true;
|
|
3385
|
+
if (policyResult.ruleDescription) policyRuleDescription = policyResult.ruleDescription;
|
|
3386
|
+
else if (policyResult.reason) policyRuleDescription = policyResult.reason;
|
|
3278
3387
|
riskMetadata = computeRiskMetadata(
|
|
3279
3388
|
args,
|
|
3280
3389
|
policyResult.tier ?? 6,
|
|
@@ -3538,7 +3647,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
3538
3647
|
hashAuditArgs
|
|
3539
3648
|
);
|
|
3540
3649
|
}
|
|
3541
|
-
|
|
3650
|
+
const enrichedResult = !finalResult.approved && policyRuleDescription && !finalResult.ruleDescription ? { ...finalResult, ruleDescription: policyRuleDescription } : finalResult;
|
|
3651
|
+
return enrichedResult;
|
|
3542
3652
|
}
|
|
3543
3653
|
var WRITE_TOOLS;
|
|
3544
3654
|
var init_orchestrator = __esm({
|
|
@@ -3553,6 +3663,7 @@ var init_orchestrator = __esm({
|
|
|
3553
3663
|
init_state();
|
|
3554
3664
|
init_daemon();
|
|
3555
3665
|
init_cloud();
|
|
3666
|
+
init_loop_detector();
|
|
3556
3667
|
WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
3557
3668
|
"write",
|
|
3558
3669
|
"write_file",
|
|
@@ -5290,8 +5401,8 @@ var init_suggestion_tracker = __esm({
|
|
|
5290
5401
|
});
|
|
5291
5402
|
|
|
5292
5403
|
// src/daemon/taint-store.ts
|
|
5293
|
-
import
|
|
5294
|
-
import
|
|
5404
|
+
import fs13 from "fs";
|
|
5405
|
+
import path16 from "path";
|
|
5295
5406
|
var DEFAULT_TTL_MS, TaintStore;
|
|
5296
5407
|
var init_taint_store = __esm({
|
|
5297
5408
|
"src/daemon/taint-store.ts"() {
|
|
@@ -5360,9 +5471,9 @@ var init_taint_store = __esm({
|
|
|
5360
5471
|
/** Resolve to absolute path, falling back to path.resolve if file doesn't exist yet. */
|
|
5361
5472
|
_resolve(filePath) {
|
|
5362
5473
|
try {
|
|
5363
|
-
return
|
|
5474
|
+
return fs13.realpathSync.native(path16.resolve(filePath));
|
|
5364
5475
|
} catch {
|
|
5365
|
-
return
|
|
5476
|
+
return path16.resolve(filePath);
|
|
5366
5477
|
}
|
|
5367
5478
|
}
|
|
5368
5479
|
};
|
|
@@ -5479,15 +5590,15 @@ var init_session_history = __esm({
|
|
|
5479
5590
|
|
|
5480
5591
|
// src/daemon/state.ts
|
|
5481
5592
|
import net2 from "net";
|
|
5482
|
-
import
|
|
5483
|
-
import
|
|
5484
|
-
import
|
|
5593
|
+
import fs14 from "fs";
|
|
5594
|
+
import path17 from "path";
|
|
5595
|
+
import os12 from "os";
|
|
5485
5596
|
import { spawn as spawn2 } from "child_process";
|
|
5486
5597
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
5487
5598
|
function loadInsightCounts() {
|
|
5488
5599
|
try {
|
|
5489
|
-
if (!
|
|
5490
|
-
const data = JSON.parse(
|
|
5600
|
+
if (!fs14.existsSync(INSIGHT_COUNTS_FILE)) return;
|
|
5601
|
+
const data = JSON.parse(fs14.readFileSync(INSIGHT_COUNTS_FILE, "utf-8"));
|
|
5491
5602
|
for (const [tool, count] of Object.entries(data)) {
|
|
5492
5603
|
if (typeof count === "number" && count > 0) insightCounts.set(tool, count);
|
|
5493
5604
|
}
|
|
@@ -5526,23 +5637,23 @@ function markRejectionHandlerRegistered() {
|
|
|
5526
5637
|
daemonRejectionHandlerRegistered = true;
|
|
5527
5638
|
}
|
|
5528
5639
|
function atomicWriteSync2(filePath, data, options) {
|
|
5529
|
-
const dir =
|
|
5530
|
-
if (!
|
|
5640
|
+
const dir = path17.dirname(filePath);
|
|
5641
|
+
if (!fs14.existsSync(dir)) fs14.mkdirSync(dir, { recursive: true });
|
|
5531
5642
|
const tmpPath = `${filePath}.${randomUUID3()}.tmp`;
|
|
5532
5643
|
try {
|
|
5533
|
-
|
|
5644
|
+
fs14.writeFileSync(tmpPath, data, options);
|
|
5534
5645
|
} catch (err2) {
|
|
5535
5646
|
try {
|
|
5536
|
-
|
|
5647
|
+
fs14.unlinkSync(tmpPath);
|
|
5537
5648
|
} catch {
|
|
5538
5649
|
}
|
|
5539
5650
|
throw err2;
|
|
5540
5651
|
}
|
|
5541
5652
|
try {
|
|
5542
|
-
|
|
5653
|
+
fs14.renameSync(tmpPath, filePath);
|
|
5543
5654
|
} catch (err2) {
|
|
5544
5655
|
try {
|
|
5545
|
-
|
|
5656
|
+
fs14.unlinkSync(tmpPath);
|
|
5546
5657
|
} catch {
|
|
5547
5658
|
}
|
|
5548
5659
|
throw err2;
|
|
@@ -5566,16 +5677,16 @@ function appendAuditLog(data) {
|
|
|
5566
5677
|
decision: data.decision,
|
|
5567
5678
|
source: "daemon"
|
|
5568
5679
|
};
|
|
5569
|
-
const dir =
|
|
5570
|
-
if (!
|
|
5571
|
-
|
|
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");
|
|
5572
5683
|
} catch {
|
|
5573
5684
|
}
|
|
5574
5685
|
}
|
|
5575
5686
|
function getAuditHistory(limit = 20) {
|
|
5576
5687
|
try {
|
|
5577
|
-
if (!
|
|
5578
|
-
const lines =
|
|
5688
|
+
if (!fs14.existsSync(AUDIT_LOG_FILE)) return [];
|
|
5689
|
+
const lines = fs14.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
|
|
5579
5690
|
if (lines.length === 1 && lines[0] === "") return [];
|
|
5580
5691
|
return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
|
|
5581
5692
|
} catch {
|
|
@@ -5584,19 +5695,19 @@ function getAuditHistory(limit = 20) {
|
|
|
5584
5695
|
}
|
|
5585
5696
|
function getOrgName() {
|
|
5586
5697
|
try {
|
|
5587
|
-
if (
|
|
5698
|
+
if (fs14.existsSync(CREDENTIALS_FILE)) return "Node9 Cloud";
|
|
5588
5699
|
} catch {
|
|
5589
5700
|
}
|
|
5590
5701
|
return null;
|
|
5591
5702
|
}
|
|
5592
5703
|
function hasStoredSlackKey() {
|
|
5593
|
-
return
|
|
5704
|
+
return fs14.existsSync(CREDENTIALS_FILE);
|
|
5594
5705
|
}
|
|
5595
5706
|
function writeGlobalSetting(key, value) {
|
|
5596
5707
|
let config = {};
|
|
5597
5708
|
try {
|
|
5598
|
-
if (
|
|
5599
|
-
config = JSON.parse(
|
|
5709
|
+
if (fs14.existsSync(GLOBAL_CONFIG_FILE)) {
|
|
5710
|
+
config = JSON.parse(fs14.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
|
|
5600
5711
|
}
|
|
5601
5712
|
} catch {
|
|
5602
5713
|
}
|
|
@@ -5608,8 +5719,8 @@ function writeTrustEntry(toolName, durationMs) {
|
|
|
5608
5719
|
try {
|
|
5609
5720
|
let trust = { entries: [] };
|
|
5610
5721
|
try {
|
|
5611
|
-
if (
|
|
5612
|
-
trust = JSON.parse(
|
|
5722
|
+
if (fs14.existsSync(TRUST_FILE2))
|
|
5723
|
+
trust = JSON.parse(fs14.readFileSync(TRUST_FILE2, "utf-8"));
|
|
5613
5724
|
} catch {
|
|
5614
5725
|
}
|
|
5615
5726
|
trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
|
|
@@ -5620,8 +5731,8 @@ function writeTrustEntry(toolName, durationMs) {
|
|
|
5620
5731
|
}
|
|
5621
5732
|
function readPersistentDecisions() {
|
|
5622
5733
|
try {
|
|
5623
|
-
if (
|
|
5624
|
-
return JSON.parse(
|
|
5734
|
+
if (fs14.existsSync(DECISIONS_FILE)) {
|
|
5735
|
+
return JSON.parse(fs14.readFileSync(DECISIONS_FILE, "utf-8"));
|
|
5625
5736
|
}
|
|
5626
5737
|
} catch {
|
|
5627
5738
|
}
|
|
@@ -5658,7 +5769,7 @@ function estimateToolCost(tool, args) {
|
|
|
5658
5769
|
const filePath = a.file_path ?? a.path;
|
|
5659
5770
|
if (filePath) {
|
|
5660
5771
|
try {
|
|
5661
|
-
const bytes =
|
|
5772
|
+
const bytes = fs14.statSync(filePath).size;
|
|
5662
5773
|
return bytes / BYTES_PER_TOKEN / 1e6 * INPUT_PRICE_PER_1M;
|
|
5663
5774
|
} catch {
|
|
5664
5775
|
}
|
|
@@ -5716,7 +5827,7 @@ function abandonPending() {
|
|
|
5716
5827
|
});
|
|
5717
5828
|
if (autoStarted) {
|
|
5718
5829
|
try {
|
|
5719
|
-
|
|
5830
|
+
fs14.unlinkSync(DAEMON_PID_FILE);
|
|
5720
5831
|
} catch {
|
|
5721
5832
|
}
|
|
5722
5833
|
setTimeout(() => {
|
|
@@ -5727,7 +5838,7 @@ function abandonPending() {
|
|
|
5727
5838
|
}
|
|
5728
5839
|
function startActivitySocket() {
|
|
5729
5840
|
try {
|
|
5730
|
-
|
|
5841
|
+
fs14.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
5731
5842
|
} catch {
|
|
5732
5843
|
}
|
|
5733
5844
|
const ACTIVITY_MAX_BYTES = 1024 * 1024;
|
|
@@ -5808,7 +5919,7 @@ function startActivitySocket() {
|
|
|
5808
5919
|
unixServer.listen(ACTIVITY_SOCKET_PATH2);
|
|
5809
5920
|
process.on("exit", () => {
|
|
5810
5921
|
try {
|
|
5811
|
-
|
|
5922
|
+
fs14.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
5812
5923
|
} catch {
|
|
5813
5924
|
}
|
|
5814
5925
|
});
|
|
@@ -5822,14 +5933,14 @@ var init_state2 = __esm({
|
|
|
5822
5933
|
init_taint_store();
|
|
5823
5934
|
init_session_counters();
|
|
5824
5935
|
init_session_history();
|
|
5825
|
-
homeDir =
|
|
5826
|
-
DAEMON_PID_FILE =
|
|
5827
|
-
DECISIONS_FILE =
|
|
5828
|
-
AUDIT_LOG_FILE =
|
|
5829
|
-
TRUST_FILE2 =
|
|
5830
|
-
GLOBAL_CONFIG_FILE =
|
|
5831
|
-
CREDENTIALS_FILE =
|
|
5832
|
-
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");
|
|
5833
5944
|
pending = /* @__PURE__ */ new Map();
|
|
5834
5945
|
sseClients = /* @__PURE__ */ new Set();
|
|
5835
5946
|
suggestionTracker = new SuggestionTracker(3);
|
|
@@ -5847,7 +5958,7 @@ var init_state2 = __esm({
|
|
|
5847
5958
|
"2h": 2 * 60 * 6e4
|
|
5848
5959
|
};
|
|
5849
5960
|
autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
5850
|
-
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");
|
|
5851
5962
|
ACTIVITY_RING_SIZE = 100;
|
|
5852
5963
|
activityRing = [];
|
|
5853
5964
|
SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
@@ -5869,14 +5980,14 @@ var init_state2 = __esm({
|
|
|
5869
5980
|
});
|
|
5870
5981
|
|
|
5871
5982
|
// src/config/patch.ts
|
|
5872
|
-
import
|
|
5873
|
-
import
|
|
5874
|
-
import
|
|
5983
|
+
import fs15 from "fs";
|
|
5984
|
+
import path18 from "path";
|
|
5985
|
+
import os13 from "os";
|
|
5875
5986
|
function patchConfig(configPath, patch) {
|
|
5876
5987
|
let config = {};
|
|
5877
5988
|
try {
|
|
5878
|
-
if (
|
|
5879
|
-
config = JSON.parse(
|
|
5989
|
+
if (fs15.existsSync(configPath)) {
|
|
5990
|
+
config = JSON.parse(fs15.readFileSync(configPath, "utf8"));
|
|
5880
5991
|
}
|
|
5881
5992
|
} catch {
|
|
5882
5993
|
throw new Error(`Cannot read config at ${configPath} \u2014 file may be corrupted`);
|
|
@@ -5895,23 +6006,23 @@ function patchConfig(configPath, patch) {
|
|
|
5895
6006
|
ignored.push(patch.toolName);
|
|
5896
6007
|
}
|
|
5897
6008
|
}
|
|
5898
|
-
const dir =
|
|
5899
|
-
|
|
6009
|
+
const dir = path18.dirname(configPath);
|
|
6010
|
+
fs15.mkdirSync(dir, { recursive: true });
|
|
5900
6011
|
const tmp = configPath + ".node9-tmp";
|
|
5901
6012
|
try {
|
|
5902
|
-
|
|
6013
|
+
fs15.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
|
|
5903
6014
|
} catch (err2) {
|
|
5904
6015
|
try {
|
|
5905
|
-
|
|
6016
|
+
fs15.unlinkSync(tmp);
|
|
5906
6017
|
} catch {
|
|
5907
6018
|
}
|
|
5908
6019
|
throw err2;
|
|
5909
6020
|
}
|
|
5910
6021
|
try {
|
|
5911
|
-
|
|
6022
|
+
fs15.renameSync(tmp, configPath);
|
|
5912
6023
|
} catch (err2) {
|
|
5913
6024
|
try {
|
|
5914
|
-
|
|
6025
|
+
fs15.unlinkSync(tmp);
|
|
5915
6026
|
} catch {
|
|
5916
6027
|
}
|
|
5917
6028
|
throw err2;
|
|
@@ -5921,14 +6032,14 @@ var GLOBAL_CONFIG_PATH;
|
|
|
5921
6032
|
var init_patch = __esm({
|
|
5922
6033
|
"src/config/patch.ts"() {
|
|
5923
6034
|
"use strict";
|
|
5924
|
-
GLOBAL_CONFIG_PATH =
|
|
6035
|
+
GLOBAL_CONFIG_PATH = path18.join(os13.homedir(), ".node9", "config.json");
|
|
5925
6036
|
}
|
|
5926
6037
|
});
|
|
5927
6038
|
|
|
5928
6039
|
// src/daemon/server.ts
|
|
5929
6040
|
import http from "http";
|
|
5930
|
-
import
|
|
5931
|
-
import
|
|
6041
|
+
import fs16 from "fs";
|
|
6042
|
+
import path19 from "path";
|
|
5932
6043
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
5933
6044
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
5934
6045
|
import chalk2 from "chalk";
|
|
@@ -5948,7 +6059,7 @@ function startDaemon() {
|
|
|
5948
6059
|
idleTimer = setTimeout(() => {
|
|
5949
6060
|
if (autoStarted) {
|
|
5950
6061
|
try {
|
|
5951
|
-
|
|
6062
|
+
fs16.unlinkSync(DAEMON_PID_FILE);
|
|
5952
6063
|
} catch {
|
|
5953
6064
|
}
|
|
5954
6065
|
}
|
|
@@ -6111,7 +6222,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
6111
6222
|
status: "pending"
|
|
6112
6223
|
});
|
|
6113
6224
|
}
|
|
6114
|
-
const projectCwd = typeof cwd === "string" &&
|
|
6225
|
+
const projectCwd = typeof cwd === "string" && path19.isAbsolute(cwd) ? cwd : void 0;
|
|
6115
6226
|
const projectConfig = getConfig(projectCwd);
|
|
6116
6227
|
const browserEnabled = projectConfig.settings.approvers?.browser !== false;
|
|
6117
6228
|
const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
|
|
@@ -6501,8 +6612,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
6501
6612
|
const body = await readBody(req);
|
|
6502
6613
|
const data = body ? JSON.parse(body) : {};
|
|
6503
6614
|
const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
|
|
6504
|
-
const node9Dir =
|
|
6505
|
-
if (!
|
|
6615
|
+
const node9Dir = path19.dirname(GLOBAL_CONFIG_PATH);
|
|
6616
|
+
if (!path19.resolve(configPath).startsWith(node9Dir + path19.sep)) {
|
|
6506
6617
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6507
6618
|
return res.end(
|
|
6508
6619
|
JSON.stringify({ error: "configPath must be within the node9 config directory" })
|
|
@@ -6613,14 +6724,14 @@ data: ${JSON.stringify(item.data)}
|
|
|
6613
6724
|
server.on("error", (e) => {
|
|
6614
6725
|
if (e.code === "EADDRINUSE") {
|
|
6615
6726
|
try {
|
|
6616
|
-
if (
|
|
6617
|
-
const { pid } = JSON.parse(
|
|
6727
|
+
if (fs16.existsSync(DAEMON_PID_FILE)) {
|
|
6728
|
+
const { pid } = JSON.parse(fs16.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
6618
6729
|
process.kill(pid, 0);
|
|
6619
6730
|
return process.exit(0);
|
|
6620
6731
|
}
|
|
6621
6732
|
} catch {
|
|
6622
6733
|
try {
|
|
6623
|
-
|
|
6734
|
+
fs16.unlinkSync(DAEMON_PID_FILE);
|
|
6624
6735
|
} catch {
|
|
6625
6736
|
}
|
|
6626
6737
|
server.listen(DAEMON_PORT, DAEMON_HOST);
|
|
@@ -6692,28 +6803,28 @@ var init_server = __esm({
|
|
|
6692
6803
|
});
|
|
6693
6804
|
|
|
6694
6805
|
// src/daemon/index.ts
|
|
6695
|
-
import
|
|
6806
|
+
import fs17 from "fs";
|
|
6696
6807
|
import chalk3 from "chalk";
|
|
6697
6808
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
6698
6809
|
function stopDaemon() {
|
|
6699
|
-
if (!
|
|
6810
|
+
if (!fs17.existsSync(DAEMON_PID_FILE)) return console.log(chalk3.yellow("Not running."));
|
|
6700
6811
|
try {
|
|
6701
|
-
const { pid } = JSON.parse(
|
|
6812
|
+
const { pid } = JSON.parse(fs17.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
6702
6813
|
process.kill(pid, "SIGTERM");
|
|
6703
6814
|
console.log(chalk3.green("\u2705 Stopped."));
|
|
6704
6815
|
} catch {
|
|
6705
6816
|
console.log(chalk3.gray("Cleaned up stale PID file."));
|
|
6706
6817
|
} finally {
|
|
6707
6818
|
try {
|
|
6708
|
-
|
|
6819
|
+
fs17.unlinkSync(DAEMON_PID_FILE);
|
|
6709
6820
|
} catch {
|
|
6710
6821
|
}
|
|
6711
6822
|
}
|
|
6712
6823
|
}
|
|
6713
6824
|
function daemonStatus() {
|
|
6714
|
-
if (
|
|
6825
|
+
if (fs17.existsSync(DAEMON_PID_FILE)) {
|
|
6715
6826
|
try {
|
|
6716
|
-
const { pid } = JSON.parse(
|
|
6827
|
+
const { pid } = JSON.parse(fs17.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
6717
6828
|
process.kill(pid, 0);
|
|
6718
6829
|
console.log(chalk3.green("Node9 daemon: running"));
|
|
6719
6830
|
return;
|
|
@@ -6747,10 +6858,10 @@ __export(tail_exports, {
|
|
|
6747
6858
|
startTail: () => startTail
|
|
6748
6859
|
});
|
|
6749
6860
|
import http2 from "http";
|
|
6750
|
-
import
|
|
6751
|
-
import
|
|
6752
|
-
import
|
|
6753
|
-
import
|
|
6861
|
+
import chalk19 from "chalk";
|
|
6862
|
+
import fs28 from "fs";
|
|
6863
|
+
import os24 from "os";
|
|
6864
|
+
import path31 from "path";
|
|
6754
6865
|
import readline5 from "readline";
|
|
6755
6866
|
import { spawn as spawn10, execSync as execSync3 } from "child_process";
|
|
6756
6867
|
function getIcon(tool) {
|
|
@@ -6760,44 +6871,64 @@ function getIcon(tool) {
|
|
|
6760
6871
|
}
|
|
6761
6872
|
return "\u{1F6E0}\uFE0F";
|
|
6762
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
|
+
}
|
|
6763
6883
|
function formatBase(activity) {
|
|
6764
6884
|
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
6765
6885
|
const icon = getIcon(activity.tool);
|
|
6766
6886
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
6767
|
-
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(
|
|
6887
|
+
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os24.homedir(), "~");
|
|
6768
6888
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
6769
|
-
return `${
|
|
6889
|
+
return `${chalk19.gray(time)} ${icon} ${chalk19.white.bold(toolName)} ${chalk19.dim(argsPreview)}`;
|
|
6770
6890
|
}
|
|
6771
6891
|
function renderResult(activity, result) {
|
|
6772
6892
|
const base = formatBase(activity);
|
|
6773
6893
|
let status;
|
|
6774
6894
|
if (result.status === "allow") {
|
|
6775
|
-
status =
|
|
6895
|
+
status = chalk19.green("\u2713 ALLOW");
|
|
6776
6896
|
} else if (result.status === "dlp") {
|
|
6777
|
-
status =
|
|
6897
|
+
status = chalk19.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
6778
6898
|
} else {
|
|
6779
|
-
status =
|
|
6899
|
+
status = chalk19.red("\u2717 BLOCK");
|
|
6780
6900
|
}
|
|
6781
6901
|
const cost = result.costEstimate ?? activity.costEstimate;
|
|
6782
|
-
const costSuffix = cost == null ? "" :
|
|
6902
|
+
const costSuffix = cost == null ? "" : chalk19.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
|
|
6783
6903
|
if (process.stdout.isTTY) {
|
|
6784
|
-
|
|
6785
|
-
|
|
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;
|
|
6786
6914
|
}
|
|
6787
6915
|
console.log(`${base} ${status}${costSuffix}`);
|
|
6788
6916
|
}
|
|
6789
6917
|
function renderPending(activity) {
|
|
6790
6918
|
if (!process.stdout.isTTY) return;
|
|
6791
|
-
|
|
6919
|
+
const line = `${formatBase(activity)} ${chalk19.yellow("\u25CF \u2026")}`;
|
|
6920
|
+
pendingShownForId = activity.id;
|
|
6921
|
+
pendingWrappedLines = wrappedLineCount(line);
|
|
6922
|
+
process.stdout.write(`${line}\r`);
|
|
6792
6923
|
}
|
|
6793
6924
|
async function ensureDaemon() {
|
|
6794
6925
|
let pidPort = null;
|
|
6795
|
-
if (
|
|
6926
|
+
if (fs28.existsSync(PID_FILE)) {
|
|
6796
6927
|
try {
|
|
6797
|
-
const { port } = JSON.parse(
|
|
6928
|
+
const { port } = JSON.parse(fs28.readFileSync(PID_FILE, "utf-8"));
|
|
6798
6929
|
pidPort = port;
|
|
6799
6930
|
} catch {
|
|
6800
|
-
console.error(
|
|
6931
|
+
console.error(chalk19.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
6801
6932
|
}
|
|
6802
6933
|
}
|
|
6803
6934
|
const checkPort = pidPort ?? DAEMON_PORT;
|
|
@@ -6808,7 +6939,7 @@ async function ensureDaemon() {
|
|
|
6808
6939
|
if (res.ok) return checkPort;
|
|
6809
6940
|
} catch {
|
|
6810
6941
|
}
|
|
6811
|
-
console.log(
|
|
6942
|
+
console.log(chalk19.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
6812
6943
|
const child = spawn10(process.execPath, [process.argv[1], "daemon"], {
|
|
6813
6944
|
detached: true,
|
|
6814
6945
|
stdio: "ignore",
|
|
@@ -6825,7 +6956,7 @@ async function ensureDaemon() {
|
|
|
6825
6956
|
} catch {
|
|
6826
6957
|
}
|
|
6827
6958
|
}
|
|
6828
|
-
console.error(
|
|
6959
|
+
console.error(chalk19.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
6829
6960
|
process.exit(1);
|
|
6830
6961
|
}
|
|
6831
6962
|
function postDecisionHttp(id, decision, csrfToken, port, opts) {
|
|
@@ -6914,9 +7045,9 @@ function buildRecoveryCardLines(req) {
|
|
|
6914
7045
|
];
|
|
6915
7046
|
}
|
|
6916
7047
|
function readApproversFromDisk() {
|
|
6917
|
-
const configPath =
|
|
7048
|
+
const configPath = path31.join(os24.homedir(), ".node9", "config.json");
|
|
6918
7049
|
try {
|
|
6919
|
-
const raw = JSON.parse(
|
|
7050
|
+
const raw = JSON.parse(fs28.readFileSync(configPath, "utf-8"));
|
|
6920
7051
|
const settings = raw.settings ?? {};
|
|
6921
7052
|
return settings.approvers ?? {};
|
|
6922
7053
|
} catch {
|
|
@@ -6927,20 +7058,20 @@ function approverStatusLine() {
|
|
|
6927
7058
|
const a = readApproversFromDisk();
|
|
6928
7059
|
const fmt = (label, key) => {
|
|
6929
7060
|
const on = a[key] !== false;
|
|
6930
|
-
return `[${key[0]}]${label.slice(1)} ${on ?
|
|
7061
|
+
return `[${key[0]}]${label.slice(1)} ${on ? chalk19.green("\u2713") : chalk19.dim("\u2717")}`;
|
|
6931
7062
|
};
|
|
6932
7063
|
return `${fmt("native", "native")} ${fmt("browser", "browser")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
|
|
6933
7064
|
}
|
|
6934
7065
|
function toggleApprover(channel) {
|
|
6935
|
-
const configPath =
|
|
7066
|
+
const configPath = path31.join(os24.homedir(), ".node9", "config.json");
|
|
6936
7067
|
try {
|
|
6937
|
-
const raw = JSON.parse(
|
|
7068
|
+
const raw = JSON.parse(fs28.readFileSync(configPath, "utf-8"));
|
|
6938
7069
|
const settings = raw.settings ?? {};
|
|
6939
7070
|
const approvers = settings.approvers ?? {};
|
|
6940
7071
|
approvers[channel] = approvers[channel] === false;
|
|
6941
7072
|
settings.approvers = approvers;
|
|
6942
7073
|
raw.settings = settings;
|
|
6943
|
-
|
|
7074
|
+
fs28.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
|
|
6944
7075
|
} catch (err2) {
|
|
6945
7076
|
process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
|
|
6946
7077
|
`);
|
|
@@ -6972,7 +7103,7 @@ async function startTail(options = {}) {
|
|
|
6972
7103
|
req2.end();
|
|
6973
7104
|
});
|
|
6974
7105
|
if (result.ok) {
|
|
6975
|
-
console.log(
|
|
7106
|
+
console.log(chalk19.green("\u2713 Flight Recorder buffer cleared."));
|
|
6976
7107
|
} else if (result.code === "ECONNREFUSED") {
|
|
6977
7108
|
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
6978
7109
|
} else if (result.code === "ETIMEDOUT") {
|
|
@@ -7016,7 +7147,7 @@ async function startTail(options = {}) {
|
|
|
7016
7147
|
const channel = name === "n" ? "native" : name === "b" ? "browser" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
|
|
7017
7148
|
if (channel) {
|
|
7018
7149
|
toggleApprover(channel);
|
|
7019
|
-
console.log(
|
|
7150
|
+
console.log(chalk19.dim(` Approvers: ${approverStatusLine()}`));
|
|
7020
7151
|
}
|
|
7021
7152
|
};
|
|
7022
7153
|
process.stdin.on("keypress", idleKeypressHandler);
|
|
@@ -7082,7 +7213,7 @@ async function startTail(options = {}) {
|
|
|
7082
7213
|
localAllowCounts.get(req2.toolName) ?? 0
|
|
7083
7214
|
)
|
|
7084
7215
|
);
|
|
7085
|
-
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");
|
|
7086
7217
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
|
|
7087
7218
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
7088
7219
|
process.stdout.write(SHOW_CURSOR);
|
|
@@ -7110,8 +7241,8 @@ async function startTail(options = {}) {
|
|
|
7110
7241
|
}
|
|
7111
7242
|
postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err2) => {
|
|
7112
7243
|
try {
|
|
7113
|
-
|
|
7114
|
-
|
|
7244
|
+
fs28.appendFileSync(
|
|
7245
|
+
path31.join(os24.homedir(), ".node9", "hook-debug.log"),
|
|
7115
7246
|
`[tail] POST /decision failed: ${String(err2)}
|
|
7116
7247
|
`
|
|
7117
7248
|
);
|
|
@@ -7133,7 +7264,7 @@ async function startTail(options = {}) {
|
|
|
7133
7264
|
);
|
|
7134
7265
|
const stampedLines = buildCardLines(req2, priorCount);
|
|
7135
7266
|
if (externalDecision) {
|
|
7136
|
-
const source = externalDecision === "allow" ?
|
|
7267
|
+
const source = externalDecision === "allow" ? chalk19.green("\u2713 ALLOWED") : chalk19.red("\u2717 DENIED");
|
|
7137
7268
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
|
|
7138
7269
|
}
|
|
7139
7270
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
@@ -7192,16 +7323,16 @@ async function startTail(options = {}) {
|
|
|
7192
7323
|
}
|
|
7193
7324
|
} catch {
|
|
7194
7325
|
}
|
|
7195
|
-
console.log(
|
|
7196
|
-
\u{1F6F0}\uFE0F Node9 tail `) +
|
|
7326
|
+
console.log(chalk19.cyan.bold(`
|
|
7327
|
+
\u{1F6F0}\uFE0F Node9 tail `) + chalk19.dim(`\u2192 ${dashboardUrl}`));
|
|
7197
7328
|
if (canApprove) {
|
|
7198
|
-
console.log(
|
|
7199
|
-
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`));
|
|
7200
7331
|
}
|
|
7201
7332
|
if (options.history) {
|
|
7202
|
-
console.log(
|
|
7333
|
+
console.log(chalk19.dim("Showing history + live events.\n"));
|
|
7203
7334
|
} else {
|
|
7204
|
-
console.log(
|
|
7335
|
+
console.log(chalk19.dim("Showing live events only. Use --history to include past.\n"));
|
|
7205
7336
|
}
|
|
7206
7337
|
process.on("SIGINT", () => {
|
|
7207
7338
|
exitIdleMode();
|
|
@@ -7211,13 +7342,13 @@ async function startTail(options = {}) {
|
|
|
7211
7342
|
readline5.clearLine(process.stdout, 0);
|
|
7212
7343
|
readline5.cursorTo(process.stdout, 0);
|
|
7213
7344
|
}
|
|
7214
|
-
console.log(
|
|
7345
|
+
console.log(chalk19.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
7215
7346
|
process.exit(0);
|
|
7216
7347
|
});
|
|
7217
7348
|
const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
|
|
7218
7349
|
const req = http2.get(sseUrl, (res) => {
|
|
7219
7350
|
if (res.statusCode !== 200) {
|
|
7220
|
-
console.error(
|
|
7351
|
+
console.error(chalk19.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
7221
7352
|
process.exit(1);
|
|
7222
7353
|
}
|
|
7223
7354
|
if (canApprove) enterIdleMode();
|
|
@@ -7248,7 +7379,7 @@ async function startTail(options = {}) {
|
|
|
7248
7379
|
readline5.clearLine(process.stdout, 0);
|
|
7249
7380
|
readline5.cursorTo(process.stdout, 0);
|
|
7250
7381
|
}
|
|
7251
|
-
console.log(
|
|
7382
|
+
console.log(chalk19.red("\n\u274C Daemon disconnected."));
|
|
7252
7383
|
process.exit(1);
|
|
7253
7384
|
});
|
|
7254
7385
|
});
|
|
@@ -7340,9 +7471,9 @@ async function startTail(options = {}) {
|
|
|
7340
7471
|
const hash = data.hash ?? "";
|
|
7341
7472
|
const summary = data.argsSummary ?? data.tool;
|
|
7342
7473
|
const fileCount = data.fileCount ?? 0;
|
|
7343
|
-
const files = fileCount > 0 ?
|
|
7474
|
+
const files = fileCount > 0 ? chalk19.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
|
|
7344
7475
|
process.stdout.write(
|
|
7345
|
-
`${
|
|
7476
|
+
`${chalk19.dim(time)} ${chalk19.cyan("\u{1F4F8} snapshot")} ${chalk19.dim(hash)} ${summary}${files}
|
|
7346
7477
|
`
|
|
7347
7478
|
);
|
|
7348
7479
|
return;
|
|
@@ -7359,19 +7490,19 @@ async function startTail(options = {}) {
|
|
|
7359
7490
|
}
|
|
7360
7491
|
req.on("error", (err2) => {
|
|
7361
7492
|
const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
|
|
7362
|
-
console.error(
|
|
7493
|
+
console.error(chalk19.red(`
|
|
7363
7494
|
\u274C ${msg}`));
|
|
7364
7495
|
process.exit(1);
|
|
7365
7496
|
});
|
|
7366
7497
|
}
|
|
7367
|
-
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;
|
|
7368
7499
|
var init_tail = __esm({
|
|
7369
7500
|
"src/tui/tail.ts"() {
|
|
7370
7501
|
"use strict";
|
|
7371
7502
|
init_daemon2();
|
|
7372
7503
|
init_daemon();
|
|
7373
7504
|
init_core();
|
|
7374
|
-
PID_FILE =
|
|
7505
|
+
PID_FILE = path31.join(os24.homedir(), ".node9", "daemon.pid");
|
|
7375
7506
|
ICONS = {
|
|
7376
7507
|
bash: "\u{1F4BB}",
|
|
7377
7508
|
shell: "\u{1F4BB}",
|
|
@@ -7399,6 +7530,8 @@ var init_tail = __esm({
|
|
|
7399
7530
|
HIDE_CURSOR = "\x1B[?25l";
|
|
7400
7531
|
SHOW_CURSOR = "\x1B[?25h";
|
|
7401
7532
|
ERASE_DOWN = "\x1B[J";
|
|
7533
|
+
pendingShownForId = null;
|
|
7534
|
+
pendingWrappedLines = 0;
|
|
7402
7535
|
DIVIDER = "\u2500".repeat(60);
|
|
7403
7536
|
}
|
|
7404
7537
|
});
|
|
@@ -7410,9 +7543,9 @@ __export(hud_exports, {
|
|
|
7410
7543
|
main: () => main,
|
|
7411
7544
|
renderEnvironmentLine: () => renderEnvironmentLine
|
|
7412
7545
|
});
|
|
7413
|
-
import
|
|
7414
|
-
import
|
|
7415
|
-
import
|
|
7546
|
+
import fs29 from "fs";
|
|
7547
|
+
import path32 from "path";
|
|
7548
|
+
import os25 from "os";
|
|
7416
7549
|
import http3 from "http";
|
|
7417
7550
|
async function readStdin() {
|
|
7418
7551
|
const chunks = [];
|
|
@@ -7471,10 +7604,10 @@ function bold(s) {
|
|
|
7471
7604
|
function color(c, s) {
|
|
7472
7605
|
return `${c}${s}${RESET3}`;
|
|
7473
7606
|
}
|
|
7474
|
-
function progressBar(
|
|
7475
|
-
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);
|
|
7476
7609
|
const bar = BAR_FILLED.repeat(filled) + BAR_EMPTY.repeat(BAR_WIDTH - filled);
|
|
7477
|
-
const c =
|
|
7610
|
+
const c = pct2 >= critAt ? RED2 : pct2 >= warnAt ? YELLOW2 : GREEN2;
|
|
7478
7611
|
return `${c}${bar}${RESET3}`;
|
|
7479
7612
|
}
|
|
7480
7613
|
function formatTimeLeft(resetsAt) {
|
|
@@ -7488,9 +7621,9 @@ function formatTimeLeft(resetsAt) {
|
|
|
7488
7621
|
return ` (${m}m left)`;
|
|
7489
7622
|
}
|
|
7490
7623
|
function safeReadJson(filePath) {
|
|
7491
|
-
if (!
|
|
7624
|
+
if (!fs29.existsSync(filePath)) return null;
|
|
7492
7625
|
try {
|
|
7493
|
-
return JSON.parse(
|
|
7626
|
+
return JSON.parse(fs29.readFileSync(filePath, "utf-8"));
|
|
7494
7627
|
} catch {
|
|
7495
7628
|
return null;
|
|
7496
7629
|
}
|
|
@@ -7511,12 +7644,12 @@ function countHooksInFile(filePath) {
|
|
|
7511
7644
|
return Object.keys(cfg.hooks).length;
|
|
7512
7645
|
}
|
|
7513
7646
|
function countRulesInDir(rulesDir) {
|
|
7514
|
-
if (!
|
|
7647
|
+
if (!fs29.existsSync(rulesDir)) return 0;
|
|
7515
7648
|
let count = 0;
|
|
7516
7649
|
try {
|
|
7517
|
-
for (const entry of
|
|
7650
|
+
for (const entry of fs29.readdirSync(rulesDir, { withFileTypes: true })) {
|
|
7518
7651
|
if (entry.isDirectory()) {
|
|
7519
|
-
count += countRulesInDir(
|
|
7652
|
+
count += countRulesInDir(path32.join(rulesDir, entry.name));
|
|
7520
7653
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
7521
7654
|
count++;
|
|
7522
7655
|
}
|
|
@@ -7527,46 +7660,46 @@ function countRulesInDir(rulesDir) {
|
|
|
7527
7660
|
}
|
|
7528
7661
|
function isSamePath(a, b) {
|
|
7529
7662
|
try {
|
|
7530
|
-
return
|
|
7663
|
+
return path32.resolve(a) === path32.resolve(b);
|
|
7531
7664
|
} catch {
|
|
7532
7665
|
return false;
|
|
7533
7666
|
}
|
|
7534
7667
|
}
|
|
7535
7668
|
function countConfigs(cwd) {
|
|
7536
|
-
const homeDir2 =
|
|
7537
|
-
const claudeDir =
|
|
7669
|
+
const homeDir2 = os25.homedir();
|
|
7670
|
+
const claudeDir = path32.join(homeDir2, ".claude");
|
|
7538
7671
|
let claudeMdCount = 0;
|
|
7539
7672
|
let rulesCount = 0;
|
|
7540
7673
|
let hooksCount = 0;
|
|
7541
7674
|
const userMcpServers = /* @__PURE__ */ new Set();
|
|
7542
7675
|
const projectMcpServers = /* @__PURE__ */ new Set();
|
|
7543
|
-
if (
|
|
7544
|
-
rulesCount += countRulesInDir(
|
|
7545
|
-
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");
|
|
7546
7679
|
for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
|
|
7547
7680
|
hooksCount += countHooksInFile(userSettings);
|
|
7548
|
-
const userClaudeJson =
|
|
7681
|
+
const userClaudeJson = path32.join(homeDir2, ".claude.json");
|
|
7549
7682
|
for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
|
|
7550
7683
|
for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
|
|
7551
7684
|
userMcpServers.delete(name);
|
|
7552
7685
|
}
|
|
7553
7686
|
if (cwd) {
|
|
7554
|
-
if (
|
|
7555
|
-
if (
|
|
7556
|
-
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");
|
|
7557
7690
|
const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
|
|
7558
7691
|
if (!overlapsUserScope) {
|
|
7559
|
-
if (
|
|
7560
|
-
rulesCount += countRulesInDir(
|
|
7561
|
-
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");
|
|
7562
7695
|
for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
|
|
7563
7696
|
hooksCount += countHooksInFile(projSettings);
|
|
7564
7697
|
}
|
|
7565
|
-
if (
|
|
7566
|
-
const localSettings =
|
|
7698
|
+
if (fs29.existsSync(path32.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
|
|
7699
|
+
const localSettings = path32.join(projectClaudeDir, "settings.local.json");
|
|
7567
7700
|
for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
|
|
7568
7701
|
hooksCount += countHooksInFile(localSettings);
|
|
7569
|
-
const mcpJsonServers = getMcpServerNames(
|
|
7702
|
+
const mcpJsonServers = getMcpServerNames(path32.join(cwd, ".mcp.json"));
|
|
7570
7703
|
const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
|
|
7571
7704
|
for (const name of disabledMcpJson) mcpJsonServers.delete(name);
|
|
7572
7705
|
for (const name of mcpJsonServers) projectMcpServers.add(name);
|
|
@@ -7599,12 +7732,12 @@ function readActiveShieldsHud() {
|
|
|
7599
7732
|
return shieldsCache.value;
|
|
7600
7733
|
}
|
|
7601
7734
|
try {
|
|
7602
|
-
const shieldsPath =
|
|
7603
|
-
if (!
|
|
7735
|
+
const shieldsPath = path32.join(os25.homedir(), ".node9", "shields.json");
|
|
7736
|
+
if (!fs29.existsSync(shieldsPath)) {
|
|
7604
7737
|
shieldsCache = { value: [], ts: now };
|
|
7605
7738
|
return [];
|
|
7606
7739
|
}
|
|
7607
|
-
const parsed = JSON.parse(
|
|
7740
|
+
const parsed = JSON.parse(fs29.readFileSync(shieldsPath, "utf-8"));
|
|
7608
7741
|
if (!Array.isArray(parsed.active)) {
|
|
7609
7742
|
shieldsCache = { value: [], ts: now };
|
|
7610
7743
|
return [];
|
|
@@ -7690,15 +7823,15 @@ function renderContextLine(stdin) {
|
|
|
7690
7823
|
}
|
|
7691
7824
|
const rl = stdin.rate_limits;
|
|
7692
7825
|
if (rl?.five_hour?.used_percentage !== void 0) {
|
|
7693
|
-
const
|
|
7694
|
-
const bar = progressBar(
|
|
7826
|
+
const pct2 = Math.round(rl.five_hour.used_percentage);
|
|
7827
|
+
const bar = progressBar(pct2, 60, 80);
|
|
7695
7828
|
const left = formatTimeLeft(rl.five_hour.resets_at);
|
|
7696
|
-
parts.push(`${dim("\u2502")} 5h ${bar} ${
|
|
7829
|
+
parts.push(`${dim("\u2502")} 5h ${bar} ${pct2}%${left}`);
|
|
7697
7830
|
}
|
|
7698
7831
|
if (rl?.seven_day?.used_percentage !== void 0) {
|
|
7699
|
-
const
|
|
7700
|
-
const bar = progressBar(
|
|
7701
|
-
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}%`);
|
|
7702
7835
|
}
|
|
7703
7836
|
if (parts.length === 0) return null;
|
|
7704
7837
|
return parts.join(" ");
|
|
@@ -7706,17 +7839,17 @@ function renderContextLine(stdin) {
|
|
|
7706
7839
|
async function main() {
|
|
7707
7840
|
try {
|
|
7708
7841
|
const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
|
|
7709
|
-
if (
|
|
7842
|
+
if (fs29.existsSync(path32.join(os25.homedir(), ".node9", "hud-debug"))) {
|
|
7710
7843
|
try {
|
|
7711
|
-
const logPath =
|
|
7844
|
+
const logPath = path32.join(os25.homedir(), ".node9", "hud-debug.log");
|
|
7712
7845
|
const MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
7713
7846
|
let size = 0;
|
|
7714
7847
|
try {
|
|
7715
|
-
size =
|
|
7848
|
+
size = fs29.statSync(logPath).size;
|
|
7716
7849
|
} catch {
|
|
7717
7850
|
}
|
|
7718
7851
|
if (size < MAX_LOG_SIZE) {
|
|
7719
|
-
|
|
7852
|
+
fs29.appendFileSync(
|
|
7720
7853
|
logPath,
|
|
7721
7854
|
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
|
|
7722
7855
|
);
|
|
@@ -7737,11 +7870,11 @@ async function main() {
|
|
|
7737
7870
|
try {
|
|
7738
7871
|
const cwd = stdin.cwd ?? process.cwd();
|
|
7739
7872
|
for (const configPath of [
|
|
7740
|
-
|
|
7741
|
-
|
|
7873
|
+
path32.join(cwd, "node9.config.json"),
|
|
7874
|
+
path32.join(os25.homedir(), ".node9", "config.json")
|
|
7742
7875
|
]) {
|
|
7743
|
-
if (!
|
|
7744
|
-
const cfg = JSON.parse(
|
|
7876
|
+
if (!fs29.existsSync(configPath)) continue;
|
|
7877
|
+
const cfg = JSON.parse(fs29.readFileSync(configPath, "utf-8"));
|
|
7745
7878
|
const hud = cfg.settings?.hud;
|
|
7746
7879
|
if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
|
|
7747
7880
|
}
|
|
@@ -7787,9 +7920,9 @@ init_core();
|
|
|
7787
7920
|
import { Command } from "commander";
|
|
7788
7921
|
|
|
7789
7922
|
// src/setup.ts
|
|
7790
|
-
import
|
|
7791
|
-
import
|
|
7792
|
-
import
|
|
7923
|
+
import fs12 from "fs";
|
|
7924
|
+
import path15 from "path";
|
|
7925
|
+
import os11 from "os";
|
|
7793
7926
|
import chalk from "chalk";
|
|
7794
7927
|
import { confirm } from "@inquirer/prompts";
|
|
7795
7928
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
@@ -7817,26 +7950,26 @@ function fullPathCommand(subcommand) {
|
|
|
7817
7950
|
}
|
|
7818
7951
|
function readJson(filePath) {
|
|
7819
7952
|
try {
|
|
7820
|
-
if (
|
|
7821
|
-
return JSON.parse(
|
|
7953
|
+
if (fs12.existsSync(filePath)) {
|
|
7954
|
+
return JSON.parse(fs12.readFileSync(filePath, "utf-8"));
|
|
7822
7955
|
}
|
|
7823
7956
|
} catch {
|
|
7824
7957
|
}
|
|
7825
7958
|
return null;
|
|
7826
7959
|
}
|
|
7827
7960
|
function writeJson(filePath, data) {
|
|
7828
|
-
const dir =
|
|
7829
|
-
if (!
|
|
7830
|
-
|
|
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");
|
|
7831
7964
|
}
|
|
7832
7965
|
function isNode9Hook(cmd) {
|
|
7833
7966
|
if (!cmd) return false;
|
|
7834
7967
|
return /(?:^|[\s/\\])node9 (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js (?:check|log)/.test(cmd);
|
|
7835
7968
|
}
|
|
7836
7969
|
function teardownClaude() {
|
|
7837
|
-
const homeDir2 =
|
|
7838
|
-
const hooksPath =
|
|
7839
|
-
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");
|
|
7840
7973
|
let changed = false;
|
|
7841
7974
|
const settings = readJson(hooksPath);
|
|
7842
7975
|
if (settings?.hooks) {
|
|
@@ -7884,8 +8017,8 @@ function teardownClaude() {
|
|
|
7884
8017
|
}
|
|
7885
8018
|
}
|
|
7886
8019
|
function teardownGemini() {
|
|
7887
|
-
const homeDir2 =
|
|
7888
|
-
const settingsPath =
|
|
8020
|
+
const homeDir2 = os11.homedir();
|
|
8021
|
+
const settingsPath = path15.join(homeDir2, ".gemini", "settings.json");
|
|
7889
8022
|
const settings = readJson(settingsPath);
|
|
7890
8023
|
if (!settings) {
|
|
7891
8024
|
console.log(chalk.blue(" \u2139\uFE0F ~/.gemini/settings.json not found \u2014 nothing to remove"));
|
|
@@ -7928,8 +8061,8 @@ function teardownGemini() {
|
|
|
7928
8061
|
}
|
|
7929
8062
|
}
|
|
7930
8063
|
function teardownCursor() {
|
|
7931
|
-
const homeDir2 =
|
|
7932
|
-
const mcpPath =
|
|
8064
|
+
const homeDir2 = os11.homedir();
|
|
8065
|
+
const mcpPath = path15.join(homeDir2, ".cursor", "mcp.json");
|
|
7933
8066
|
const mcpConfig = readJson(mcpPath);
|
|
7934
8067
|
if (!mcpConfig?.mcpServers) {
|
|
7935
8068
|
console.log(chalk.blue(" \u2139\uFE0F ~/.cursor/mcp.json not found \u2014 nothing to remove"));
|
|
@@ -7960,9 +8093,9 @@ function teardownCursor() {
|
|
|
7960
8093
|
}
|
|
7961
8094
|
}
|
|
7962
8095
|
async function setupClaude() {
|
|
7963
|
-
const homeDir2 =
|
|
7964
|
-
const mcpPath =
|
|
7965
|
-
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");
|
|
7966
8099
|
const claudeConfig = readJson(mcpPath) ?? {};
|
|
7967
8100
|
const settings = readJson(hooksPath) ?? {};
|
|
7968
8101
|
const servers = claudeConfig.mcpServers ?? {};
|
|
@@ -8059,8 +8192,8 @@ async function setupClaude() {
|
|
|
8059
8192
|
}
|
|
8060
8193
|
}
|
|
8061
8194
|
async function setupGemini() {
|
|
8062
|
-
const homeDir2 =
|
|
8063
|
-
const settingsPath =
|
|
8195
|
+
const homeDir2 = os11.homedir();
|
|
8196
|
+
const settingsPath = path15.join(homeDir2, ".gemini", "settings.json");
|
|
8064
8197
|
const settings = readJson(settingsPath) ?? {};
|
|
8065
8198
|
const servers = settings.mcpServers ?? {};
|
|
8066
8199
|
let hooksChanged = false;
|
|
@@ -8155,10 +8288,10 @@ async function setupGemini() {
|
|
|
8155
8288
|
printDaemonTip();
|
|
8156
8289
|
}
|
|
8157
8290
|
}
|
|
8158
|
-
function detectAgents(homeDir2 =
|
|
8291
|
+
function detectAgents(homeDir2 = os11.homedir()) {
|
|
8159
8292
|
const exists = (p) => {
|
|
8160
8293
|
try {
|
|
8161
|
-
return
|
|
8294
|
+
return fs12.existsSync(p);
|
|
8162
8295
|
} catch (err2) {
|
|
8163
8296
|
const code = err2.code;
|
|
8164
8297
|
if (code !== "ENOENT") {
|
|
@@ -8169,15 +8302,15 @@ function detectAgents(homeDir2 = os10.homedir()) {
|
|
|
8169
8302
|
}
|
|
8170
8303
|
};
|
|
8171
8304
|
return {
|
|
8172
|
-
claude: exists(
|
|
8173
|
-
gemini: exists(
|
|
8174
|
-
cursor: exists(
|
|
8175
|
-
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"))
|
|
8176
8309
|
};
|
|
8177
8310
|
}
|
|
8178
8311
|
async function setupCursor() {
|
|
8179
|
-
const homeDir2 =
|
|
8180
|
-
const mcpPath =
|
|
8312
|
+
const homeDir2 = os11.homedir();
|
|
8313
|
+
const mcpPath = path15.join(homeDir2, ".cursor", "mcp.json");
|
|
8181
8314
|
const mcpConfig = readJson(mcpPath) ?? {};
|
|
8182
8315
|
const servers = mcpConfig.mcpServers ?? {};
|
|
8183
8316
|
let anythingChanged = false;
|
|
@@ -8243,21 +8376,21 @@ async function setupCursor() {
|
|
|
8243
8376
|
}
|
|
8244
8377
|
function readToml(filePath) {
|
|
8245
8378
|
try {
|
|
8246
|
-
if (
|
|
8247
|
-
return parseToml(
|
|
8379
|
+
if (fs12.existsSync(filePath)) {
|
|
8380
|
+
return parseToml(fs12.readFileSync(filePath, "utf-8"));
|
|
8248
8381
|
}
|
|
8249
8382
|
} catch {
|
|
8250
8383
|
}
|
|
8251
8384
|
return null;
|
|
8252
8385
|
}
|
|
8253
8386
|
function writeToml(filePath, data) {
|
|
8254
|
-
const dir =
|
|
8255
|
-
if (!
|
|
8256
|
-
|
|
8387
|
+
const dir = path15.dirname(filePath);
|
|
8388
|
+
if (!fs12.existsSync(dir)) fs12.mkdirSync(dir, { recursive: true });
|
|
8389
|
+
fs12.writeFileSync(filePath, stringifyToml(data));
|
|
8257
8390
|
}
|
|
8258
8391
|
async function setupCodex() {
|
|
8259
|
-
const homeDir2 =
|
|
8260
|
-
const configPath =
|
|
8392
|
+
const homeDir2 = os11.homedir();
|
|
8393
|
+
const configPath = path15.join(homeDir2, ".codex", "config.toml");
|
|
8261
8394
|
const config = readToml(configPath) ?? {};
|
|
8262
8395
|
const servers = config.mcp_servers ?? {};
|
|
8263
8396
|
let anythingChanged = false;
|
|
@@ -8322,8 +8455,8 @@ async function setupCodex() {
|
|
|
8322
8455
|
}
|
|
8323
8456
|
}
|
|
8324
8457
|
function setupHud() {
|
|
8325
|
-
const homeDir2 =
|
|
8326
|
-
const hooksPath =
|
|
8458
|
+
const homeDir2 = os11.homedir();
|
|
8459
|
+
const hooksPath = path15.join(homeDir2, ".claude", "settings.json");
|
|
8327
8460
|
const settings = readJson(hooksPath) ?? {};
|
|
8328
8461
|
const hudCommand = fullPathCommand("hud");
|
|
8329
8462
|
const statusLineObj = { type: "command", command: hudCommand };
|
|
@@ -8349,8 +8482,8 @@ function setupHud() {
|
|
|
8349
8482
|
console.log(chalk.gray(" Restart Claude Code to activate."));
|
|
8350
8483
|
}
|
|
8351
8484
|
function teardownHud() {
|
|
8352
|
-
const homeDir2 =
|
|
8353
|
-
const hooksPath =
|
|
8485
|
+
const homeDir2 = os11.homedir();
|
|
8486
|
+
const hooksPath = path15.join(homeDir2, ".claude", "settings.json");
|
|
8354
8487
|
const settings = readJson(hooksPath);
|
|
8355
8488
|
if (!settings) {
|
|
8356
8489
|
console.log(chalk.blue(" \u2139\uFE0F ~/.claude/settings.json not found \u2014 nothing to remove"));
|
|
@@ -8370,10 +8503,10 @@ function teardownHud() {
|
|
|
8370
8503
|
|
|
8371
8504
|
// src/cli.ts
|
|
8372
8505
|
init_daemon2();
|
|
8373
|
-
import
|
|
8374
|
-
import
|
|
8375
|
-
import
|
|
8376
|
-
import
|
|
8506
|
+
import chalk20 from "chalk";
|
|
8507
|
+
import fs30 from "fs";
|
|
8508
|
+
import path33 from "path";
|
|
8509
|
+
import os26 from "os";
|
|
8377
8510
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
8378
8511
|
|
|
8379
8512
|
// src/utils/duration.ts
|
|
@@ -8602,19 +8735,19 @@ init_daemon();
|
|
|
8602
8735
|
init_config();
|
|
8603
8736
|
init_policy();
|
|
8604
8737
|
import chalk5 from "chalk";
|
|
8605
|
-
import
|
|
8738
|
+
import fs19 from "fs";
|
|
8606
8739
|
import { spawn as spawn6 } from "child_process";
|
|
8607
|
-
import
|
|
8608
|
-
import
|
|
8740
|
+
import path21 from "path";
|
|
8741
|
+
import os15 from "os";
|
|
8609
8742
|
|
|
8610
8743
|
// src/undo.ts
|
|
8611
8744
|
import { spawnSync as spawnSync4, spawn as spawn5 } from "child_process";
|
|
8612
|
-
import
|
|
8613
|
-
import
|
|
8745
|
+
import crypto3 from "crypto";
|
|
8746
|
+
import fs18 from "fs";
|
|
8614
8747
|
import net3 from "net";
|
|
8615
|
-
import
|
|
8616
|
-
import
|
|
8617
|
-
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");
|
|
8618
8751
|
function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
|
|
8619
8752
|
try {
|
|
8620
8753
|
const payload = JSON.stringify({
|
|
@@ -8634,22 +8767,22 @@ function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
|
|
|
8634
8767
|
} catch {
|
|
8635
8768
|
}
|
|
8636
8769
|
}
|
|
8637
|
-
var SNAPSHOT_STACK_PATH =
|
|
8638
|
-
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");
|
|
8639
8772
|
var MAX_SNAPSHOTS = 10;
|
|
8640
8773
|
var GIT_TIMEOUT = 15e3;
|
|
8641
8774
|
function readStack() {
|
|
8642
8775
|
try {
|
|
8643
|
-
if (
|
|
8644
|
-
return JSON.parse(
|
|
8776
|
+
if (fs18.existsSync(SNAPSHOT_STACK_PATH))
|
|
8777
|
+
return JSON.parse(fs18.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
|
|
8645
8778
|
} catch {
|
|
8646
8779
|
}
|
|
8647
8780
|
return [];
|
|
8648
8781
|
}
|
|
8649
8782
|
function writeStack(stack) {
|
|
8650
|
-
const dir =
|
|
8651
|
-
if (!
|
|
8652
|
-
|
|
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));
|
|
8653
8786
|
}
|
|
8654
8787
|
function extractFilePath(args) {
|
|
8655
8788
|
if (!args || typeof args !== "object") return null;
|
|
@@ -8669,12 +8802,12 @@ function buildArgsSummary(tool, args) {
|
|
|
8669
8802
|
return "";
|
|
8670
8803
|
}
|
|
8671
8804
|
function findProjectRoot(filePath) {
|
|
8672
|
-
let dir =
|
|
8805
|
+
let dir = path20.dirname(filePath);
|
|
8673
8806
|
while (true) {
|
|
8674
|
-
if (
|
|
8807
|
+
if (fs18.existsSync(path20.join(dir, ".git")) || fs18.existsSync(path20.join(dir, "package.json"))) {
|
|
8675
8808
|
return dir;
|
|
8676
8809
|
}
|
|
8677
|
-
const parent =
|
|
8810
|
+
const parent = path20.dirname(dir);
|
|
8678
8811
|
if (parent === dir) return process.cwd();
|
|
8679
8812
|
dir = parent;
|
|
8680
8813
|
}
|
|
@@ -8682,7 +8815,7 @@ function findProjectRoot(filePath) {
|
|
|
8682
8815
|
function normalizeCwdForHash(cwd) {
|
|
8683
8816
|
let normalized;
|
|
8684
8817
|
try {
|
|
8685
|
-
normalized =
|
|
8818
|
+
normalized = fs18.realpathSync(cwd);
|
|
8686
8819
|
} catch {
|
|
8687
8820
|
normalized = cwd;
|
|
8688
8821
|
}
|
|
@@ -8691,17 +8824,17 @@ function normalizeCwdForHash(cwd) {
|
|
|
8691
8824
|
return normalized;
|
|
8692
8825
|
}
|
|
8693
8826
|
function getShadowRepoDir(cwd) {
|
|
8694
|
-
const hash =
|
|
8695
|
-
return
|
|
8827
|
+
const hash = crypto3.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
|
|
8828
|
+
return path20.join(os14.homedir(), ".node9", "snapshots", hash);
|
|
8696
8829
|
}
|
|
8697
8830
|
function cleanOrphanedIndexFiles(shadowDir) {
|
|
8698
8831
|
try {
|
|
8699
8832
|
const cutoff = Date.now() - 6e4;
|
|
8700
|
-
for (const f of
|
|
8833
|
+
for (const f of fs18.readdirSync(shadowDir)) {
|
|
8701
8834
|
if (f.startsWith("index_")) {
|
|
8702
|
-
const fp =
|
|
8835
|
+
const fp = path20.join(shadowDir, f);
|
|
8703
8836
|
try {
|
|
8704
|
-
if (
|
|
8837
|
+
if (fs18.statSync(fp).mtimeMs < cutoff) fs18.unlinkSync(fp);
|
|
8705
8838
|
} catch {
|
|
8706
8839
|
}
|
|
8707
8840
|
}
|
|
@@ -8713,7 +8846,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
|
|
|
8713
8846
|
const hardcoded = [".git", ".node9"];
|
|
8714
8847
|
const lines = [...hardcoded, ...ignorePaths].join("\n");
|
|
8715
8848
|
try {
|
|
8716
|
-
|
|
8849
|
+
fs18.writeFileSync(path20.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
|
|
8717
8850
|
} catch {
|
|
8718
8851
|
}
|
|
8719
8852
|
}
|
|
@@ -8726,25 +8859,25 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
8726
8859
|
timeout: 3e3
|
|
8727
8860
|
});
|
|
8728
8861
|
if (check.status === 0) {
|
|
8729
|
-
const ptPath =
|
|
8862
|
+
const ptPath = path20.join(shadowDir, "project-path.txt");
|
|
8730
8863
|
try {
|
|
8731
|
-
const stored =
|
|
8864
|
+
const stored = fs18.readFileSync(ptPath, "utf8").trim();
|
|
8732
8865
|
if (stored === normalizedCwd) return true;
|
|
8733
8866
|
if (process.env.NODE9_DEBUG === "1")
|
|
8734
8867
|
console.error(
|
|
8735
8868
|
`[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
|
|
8736
8869
|
);
|
|
8737
|
-
|
|
8870
|
+
fs18.rmSync(shadowDir, { recursive: true, force: true });
|
|
8738
8871
|
} catch {
|
|
8739
8872
|
try {
|
|
8740
|
-
|
|
8873
|
+
fs18.writeFileSync(ptPath, normalizedCwd, "utf8");
|
|
8741
8874
|
} catch {
|
|
8742
8875
|
}
|
|
8743
8876
|
return true;
|
|
8744
8877
|
}
|
|
8745
8878
|
}
|
|
8746
8879
|
try {
|
|
8747
|
-
|
|
8880
|
+
fs18.mkdirSync(shadowDir, { recursive: true });
|
|
8748
8881
|
} catch {
|
|
8749
8882
|
}
|
|
8750
8883
|
const init = spawnSync4("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
|
|
@@ -8753,7 +8886,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
8753
8886
|
if (process.env.NODE9_DEBUG === "1") console.error("[Node9] git init --bare failed:", reason);
|
|
8754
8887
|
return false;
|
|
8755
8888
|
}
|
|
8756
|
-
const configFile =
|
|
8889
|
+
const configFile = path20.join(shadowDir, "config");
|
|
8757
8890
|
spawnSync4("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
|
|
8758
8891
|
timeout: 3e3
|
|
8759
8892
|
});
|
|
@@ -8761,7 +8894,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
8761
8894
|
timeout: 3e3
|
|
8762
8895
|
});
|
|
8763
8896
|
try {
|
|
8764
|
-
|
|
8897
|
+
fs18.writeFileSync(path20.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
|
|
8765
8898
|
} catch {
|
|
8766
8899
|
}
|
|
8767
8900
|
return true;
|
|
@@ -8781,12 +8914,12 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
8781
8914
|
let indexFile = null;
|
|
8782
8915
|
try {
|
|
8783
8916
|
const rawFilePath = extractFilePath(args);
|
|
8784
|
-
const absFilePath = rawFilePath &&
|
|
8917
|
+
const absFilePath = rawFilePath && path20.isAbsolute(rawFilePath) ? rawFilePath : null;
|
|
8785
8918
|
const cwd = absFilePath ? findProjectRoot(absFilePath) : process.cwd();
|
|
8786
8919
|
const shadowDir = getShadowRepoDir(cwd);
|
|
8787
8920
|
if (!ensureShadowRepo(shadowDir, cwd)) return null;
|
|
8788
8921
|
writeShadowExcludes(shadowDir, ignorePaths);
|
|
8789
|
-
indexFile =
|
|
8922
|
+
indexFile = path20.join(shadowDir, `index_${process.pid}_${Date.now()}`);
|
|
8790
8923
|
const shadowEnv = {
|
|
8791
8924
|
...process.env,
|
|
8792
8925
|
GIT_DIR: shadowDir,
|
|
@@ -8858,7 +8991,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
8858
8991
|
writeStack(stack);
|
|
8859
8992
|
const entry = stack[stack.length - 1];
|
|
8860
8993
|
notifySnapshotTaken(commitHash.slice(0, 7), tool, entry.argsSummary, capturedFiles.length);
|
|
8861
|
-
|
|
8994
|
+
fs18.writeFileSync(UNDO_LATEST_PATH, commitHash);
|
|
8862
8995
|
if (shouldGc) {
|
|
8863
8996
|
spawn5("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
|
|
8864
8997
|
}
|
|
@@ -8869,7 +9002,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
8869
9002
|
} finally {
|
|
8870
9003
|
if (indexFile) {
|
|
8871
9004
|
try {
|
|
8872
|
-
|
|
9005
|
+
fs18.unlinkSync(indexFile);
|
|
8873
9006
|
} catch {
|
|
8874
9007
|
}
|
|
8875
9008
|
}
|
|
@@ -8945,9 +9078,9 @@ function applyUndo(hash, cwd) {
|
|
|
8945
9078
|
timeout: GIT_TIMEOUT
|
|
8946
9079
|
}).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
8947
9080
|
for (const file of [...tracked, ...untracked]) {
|
|
8948
|
-
const fullPath =
|
|
8949
|
-
if (!snapshotFiles.has(file) &&
|
|
8950
|
-
|
|
9081
|
+
const fullPath = path20.join(dir, file);
|
|
9082
|
+
if (!snapshotFiles.has(file) && fs18.existsSync(fullPath)) {
|
|
9083
|
+
fs18.unlinkSync(fullPath);
|
|
8951
9084
|
}
|
|
8952
9085
|
}
|
|
8953
9086
|
return true;
|
|
@@ -8971,9 +9104,9 @@ function registerCheckCommand(program2) {
|
|
|
8971
9104
|
} catch (err2) {
|
|
8972
9105
|
const tempConfig = getConfig();
|
|
8973
9106
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
8974
|
-
const logPath =
|
|
9107
|
+
const logPath = path21.join(os15.homedir(), ".node9", "hook-debug.log");
|
|
8975
9108
|
const errMsg = err2 instanceof Error ? err2.message : String(err2);
|
|
8976
|
-
|
|
9109
|
+
fs19.appendFileSync(
|
|
8977
9110
|
logPath,
|
|
8978
9111
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
8979
9112
|
RAW: ${raw}
|
|
@@ -8986,10 +9119,10 @@ RAW: ${raw}
|
|
|
8986
9119
|
if (config.settings.autoStartDaemon && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON) {
|
|
8987
9120
|
try {
|
|
8988
9121
|
const scriptPath = process.argv[1];
|
|
8989
|
-
if (typeof scriptPath !== "string" || !
|
|
9122
|
+
if (typeof scriptPath !== "string" || !path21.isAbsolute(scriptPath))
|
|
8990
9123
|
throw new Error("node9: argv[1] is not an absolute path");
|
|
8991
|
-
const resolvedScript =
|
|
8992
|
-
const expectedCli =
|
|
9124
|
+
const resolvedScript = fs19.realpathSync(scriptPath);
|
|
9125
|
+
const expectedCli = fs19.realpathSync(path21.resolve(__dirname, "../../cli.js"));
|
|
8993
9126
|
if (resolvedScript !== expectedCli)
|
|
8994
9127
|
throw new Error(
|
|
8995
9128
|
"node9: daemon spawn aborted \u2014 argv[1] does not resolve to the node9 CLI"
|
|
@@ -9015,10 +9148,10 @@ RAW: ${raw}
|
|
|
9015
9148
|
}
|
|
9016
9149
|
}
|
|
9017
9150
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
9018
|
-
const logPath =
|
|
9019
|
-
if (!
|
|
9020
|
-
|
|
9021
|
-
|
|
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}
|
|
9022
9155
|
`);
|
|
9023
9156
|
}
|
|
9024
9157
|
const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
|
|
@@ -9031,8 +9164,8 @@ RAW: ${raw}
|
|
|
9031
9164
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
9032
9165
|
let ttyFd = null;
|
|
9033
9166
|
try {
|
|
9034
|
-
ttyFd =
|
|
9035
|
-
const writeTty = (line) =>
|
|
9167
|
+
ttyFd = fs19.openSync("/dev/tty", "w");
|
|
9168
|
+
const writeTty = (line) => fs19.writeSync(ttyFd, line + "\n");
|
|
9036
9169
|
if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
|
|
9037
9170
|
writeTty(chalk5.bgRed.white.bold(`
|
|
9038
9171
|
\u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
@@ -9041,6 +9174,7 @@ RAW: ${raw}
|
|
|
9041
9174
|
writeTty(chalk5.red(`
|
|
9042
9175
|
\u{1F6D1} Node9 blocked "${toolName}"`));
|
|
9043
9176
|
}
|
|
9177
|
+
if (result2?.ruleDescription) writeTty(chalk5.white(` ${result2.ruleDescription}`));
|
|
9044
9178
|
writeTty(chalk5.gray(` Triggered by: ${blockedByContext}`));
|
|
9045
9179
|
if (result2?.changeHint) writeTty(chalk5.cyan(` To change: ${result2.changeHint}`));
|
|
9046
9180
|
if (result2?.recoveryCommand)
|
|
@@ -9050,7 +9184,7 @@ RAW: ${raw}
|
|
|
9050
9184
|
} finally {
|
|
9051
9185
|
if (ttyFd !== null)
|
|
9052
9186
|
try {
|
|
9053
|
-
|
|
9187
|
+
fs19.closeSync(ttyFd);
|
|
9054
9188
|
} catch {
|
|
9055
9189
|
}
|
|
9056
9190
|
}
|
|
@@ -9082,7 +9216,7 @@ RAW: ${raw}
|
|
|
9082
9216
|
if (shouldSnapshot(toolName, toolInput, config)) {
|
|
9083
9217
|
await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
|
|
9084
9218
|
}
|
|
9085
|
-
const safeCwdForAuth = typeof payload.cwd === "string" &&
|
|
9219
|
+
const safeCwdForAuth = typeof payload.cwd === "string" && path21.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
9086
9220
|
const result = await authorizeHeadless(toolName, toolInput, meta, {
|
|
9087
9221
|
cwd: safeCwdForAuth
|
|
9088
9222
|
});
|
|
@@ -9094,12 +9228,12 @@ RAW: ${raw}
|
|
|
9094
9228
|
}
|
|
9095
9229
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
9096
9230
|
try {
|
|
9097
|
-
const tty =
|
|
9098
|
-
|
|
9231
|
+
const tty = fs19.openSync("/dev/tty", "w");
|
|
9232
|
+
fs19.writeSync(
|
|
9099
9233
|
tty,
|
|
9100
9234
|
chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
|
|
9101
9235
|
);
|
|
9102
|
-
|
|
9236
|
+
fs19.closeSync(tty);
|
|
9103
9237
|
} catch {
|
|
9104
9238
|
}
|
|
9105
9239
|
const daemonReady = await autoStartDaemonAndWait();
|
|
@@ -9126,9 +9260,9 @@ RAW: ${raw}
|
|
|
9126
9260
|
});
|
|
9127
9261
|
} catch (err2) {
|
|
9128
9262
|
if (process.env.NODE9_DEBUG === "1") {
|
|
9129
|
-
const logPath =
|
|
9263
|
+
const logPath = path21.join(os15.homedir(), ".node9", "hook-debug.log");
|
|
9130
9264
|
const errMsg = err2 instanceof Error ? err2.message : String(err2);
|
|
9131
|
-
|
|
9265
|
+
fs19.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
9132
9266
|
`);
|
|
9133
9267
|
}
|
|
9134
9268
|
process.exit(0);
|
|
@@ -9165,9 +9299,9 @@ RAW: ${raw}
|
|
|
9165
9299
|
init_audit();
|
|
9166
9300
|
init_config();
|
|
9167
9301
|
init_policy();
|
|
9168
|
-
import
|
|
9169
|
-
import
|
|
9170
|
-
import
|
|
9302
|
+
import fs20 from "fs";
|
|
9303
|
+
import path22 from "path";
|
|
9304
|
+
import os16 from "os";
|
|
9171
9305
|
init_daemon();
|
|
9172
9306
|
|
|
9173
9307
|
// src/utils/cp-mv-parser.ts
|
|
@@ -9208,9 +9342,9 @@ function containsShellMetachar(token) {
|
|
|
9208
9342
|
}
|
|
9209
9343
|
|
|
9210
9344
|
// src/cli/commands/log.ts
|
|
9211
|
-
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;
|
|
9212
9346
|
function detectTestResult(command, output) {
|
|
9213
|
-
if (!
|
|
9347
|
+
if (!TEST_COMMAND_RE2.test(command)) return null;
|
|
9214
9348
|
const out = output.toLowerCase();
|
|
9215
9349
|
if (/\b(tests?\s+passed|all\s+tests?\s+passed|passing|test\s+suites?.*passed|ok\b|\d+\s+passed)/i.test(
|
|
9216
9350
|
out
|
|
@@ -9240,10 +9374,10 @@ function registerLogCommand(program2) {
|
|
|
9240
9374
|
decision: "allowed",
|
|
9241
9375
|
source: "post-hook"
|
|
9242
9376
|
};
|
|
9243
|
-
const logPath =
|
|
9244
|
-
if (!
|
|
9245
|
-
|
|
9246
|
-
|
|
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");
|
|
9247
9381
|
if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
|
|
9248
9382
|
const command = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
|
|
9249
9383
|
if (command) {
|
|
@@ -9259,16 +9393,24 @@ function registerLogCommand(program2) {
|
|
|
9259
9393
|
if (bashCommand && output) {
|
|
9260
9394
|
const testResult = detectTestResult(bashCommand, output);
|
|
9261
9395
|
if (testResult) {
|
|
9262
|
-
|
|
9263
|
-
|
|
9264
|
-
ts: Date.now(),
|
|
9396
|
+
appendToLog(LOCAL_AUDIT_LOG, {
|
|
9397
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9265
9398
|
tool,
|
|
9266
|
-
|
|
9399
|
+
testResult,
|
|
9400
|
+
source: "test-result"
|
|
9267
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
|
+
}
|
|
9268
9410
|
}
|
|
9269
9411
|
}
|
|
9270
9412
|
}
|
|
9271
|
-
const safeCwd = typeof payload.cwd === "string" &&
|
|
9413
|
+
const safeCwd = typeof payload.cwd === "string" && path22.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
9272
9414
|
const config = getConfig(safeCwd);
|
|
9273
9415
|
if (shouldSnapshot(tool, {}, config)) {
|
|
9274
9416
|
await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
|
|
@@ -9277,9 +9419,9 @@ function registerLogCommand(program2) {
|
|
|
9277
9419
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
9278
9420
|
process.stderr.write(`[Node9] audit log error: ${msg}
|
|
9279
9421
|
`);
|
|
9280
|
-
const debugPath =
|
|
9422
|
+
const debugPath = path22.join(os16.homedir(), ".node9", "hook-debug.log");
|
|
9281
9423
|
try {
|
|
9282
|
-
|
|
9424
|
+
fs20.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
|
|
9283
9425
|
`);
|
|
9284
9426
|
} catch {
|
|
9285
9427
|
}
|
|
@@ -9680,13 +9822,13 @@ function registerConfigShowCommand(program2) {
|
|
|
9680
9822
|
// src/cli/commands/doctor.ts
|
|
9681
9823
|
init_daemon();
|
|
9682
9824
|
import chalk7 from "chalk";
|
|
9683
|
-
import
|
|
9684
|
-
import
|
|
9685
|
-
import
|
|
9825
|
+
import fs21 from "fs";
|
|
9826
|
+
import path23 from "path";
|
|
9827
|
+
import os17 from "os";
|
|
9686
9828
|
import { execSync as execSync2 } from "child_process";
|
|
9687
9829
|
function registerDoctorCommand(program2, version2) {
|
|
9688
9830
|
program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
9689
|
-
const homeDir2 =
|
|
9831
|
+
const homeDir2 = os17.homedir();
|
|
9690
9832
|
let failures = 0;
|
|
9691
9833
|
function pass(msg) {
|
|
9692
9834
|
console.log(chalk7.green(" \u2705 ") + msg);
|
|
@@ -9735,10 +9877,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9735
9877
|
);
|
|
9736
9878
|
}
|
|
9737
9879
|
section("Configuration");
|
|
9738
|
-
const globalConfigPath =
|
|
9739
|
-
if (
|
|
9880
|
+
const globalConfigPath = path23.join(homeDir2, ".node9", "config.json");
|
|
9881
|
+
if (fs21.existsSync(globalConfigPath)) {
|
|
9740
9882
|
try {
|
|
9741
|
-
JSON.parse(
|
|
9883
|
+
JSON.parse(fs21.readFileSync(globalConfigPath, "utf-8"));
|
|
9742
9884
|
pass("~/.node9/config.json found and valid");
|
|
9743
9885
|
} catch {
|
|
9744
9886
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -9746,10 +9888,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9746
9888
|
} else {
|
|
9747
9889
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
9748
9890
|
}
|
|
9749
|
-
const projectConfigPath =
|
|
9750
|
-
if (
|
|
9891
|
+
const projectConfigPath = path23.join(process.cwd(), "node9.config.json");
|
|
9892
|
+
if (fs21.existsSync(projectConfigPath)) {
|
|
9751
9893
|
try {
|
|
9752
|
-
JSON.parse(
|
|
9894
|
+
JSON.parse(fs21.readFileSync(projectConfigPath, "utf-8"));
|
|
9753
9895
|
pass("node9.config.json found and valid (project)");
|
|
9754
9896
|
} catch {
|
|
9755
9897
|
fail(
|
|
@@ -9758,8 +9900,8 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9758
9900
|
);
|
|
9759
9901
|
}
|
|
9760
9902
|
}
|
|
9761
|
-
const credsPath =
|
|
9762
|
-
if (
|
|
9903
|
+
const credsPath = path23.join(homeDir2, ".node9", "credentials.json");
|
|
9904
|
+
if (fs21.existsSync(credsPath)) {
|
|
9763
9905
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
9764
9906
|
} else {
|
|
9765
9907
|
warn(
|
|
@@ -9768,10 +9910,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9768
9910
|
);
|
|
9769
9911
|
}
|
|
9770
9912
|
section("Agent Hooks");
|
|
9771
|
-
const claudeSettingsPath =
|
|
9772
|
-
if (
|
|
9913
|
+
const claudeSettingsPath = path23.join(homeDir2, ".claude", "settings.json");
|
|
9914
|
+
if (fs21.existsSync(claudeSettingsPath)) {
|
|
9773
9915
|
try {
|
|
9774
|
-
const cs = JSON.parse(
|
|
9916
|
+
const cs = JSON.parse(fs21.readFileSync(claudeSettingsPath, "utf-8"));
|
|
9775
9917
|
const hasHook = cs.hooks?.PreToolUse?.some(
|
|
9776
9918
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
9777
9919
|
);
|
|
@@ -9787,10 +9929,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9787
9929
|
} else {
|
|
9788
9930
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
9789
9931
|
}
|
|
9790
|
-
const geminiSettingsPath =
|
|
9791
|
-
if (
|
|
9932
|
+
const geminiSettingsPath = path23.join(homeDir2, ".gemini", "settings.json");
|
|
9933
|
+
if (fs21.existsSync(geminiSettingsPath)) {
|
|
9792
9934
|
try {
|
|
9793
|
-
const gs = JSON.parse(
|
|
9935
|
+
const gs = JSON.parse(fs21.readFileSync(geminiSettingsPath, "utf-8"));
|
|
9794
9936
|
const hasHook = gs.hooks?.BeforeTool?.some(
|
|
9795
9937
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
9796
9938
|
);
|
|
@@ -9806,10 +9948,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9806
9948
|
} else {
|
|
9807
9949
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
9808
9950
|
}
|
|
9809
|
-
const cursorHooksPath =
|
|
9810
|
-
if (
|
|
9951
|
+
const cursorHooksPath = path23.join(homeDir2, ".cursor", "hooks.json");
|
|
9952
|
+
if (fs21.existsSync(cursorHooksPath)) {
|
|
9811
9953
|
try {
|
|
9812
|
-
const cur = JSON.parse(
|
|
9954
|
+
const cur = JSON.parse(fs21.readFileSync(cursorHooksPath, "utf-8"));
|
|
9813
9955
|
const hasHook = cur.hooks?.preToolUse?.some(
|
|
9814
9956
|
(h) => h.command?.includes("node9") || h.command?.includes("cli.js")
|
|
9815
9957
|
);
|
|
@@ -9847,9 +9989,9 @@ function registerDoctorCommand(program2, version2) {
|
|
|
9847
9989
|
|
|
9848
9990
|
// src/cli/commands/audit.ts
|
|
9849
9991
|
import chalk8 from "chalk";
|
|
9850
|
-
import
|
|
9851
|
-
import
|
|
9852
|
-
import
|
|
9992
|
+
import fs22 from "fs";
|
|
9993
|
+
import path24 from "path";
|
|
9994
|
+
import os18 from "os";
|
|
9853
9995
|
function formatRelativeTime(timestamp) {
|
|
9854
9996
|
const diff = Date.now() - new Date(timestamp).getTime();
|
|
9855
9997
|
const sec = Math.floor(diff / 1e3);
|
|
@@ -9862,14 +10004,14 @@ function formatRelativeTime(timestamp) {
|
|
|
9862
10004
|
}
|
|
9863
10005
|
function registerAuditCommand(program2) {
|
|
9864
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) => {
|
|
9865
|
-
const logPath =
|
|
9866
|
-
if (!
|
|
10007
|
+
const logPath = path24.join(os18.homedir(), ".node9", "audit.log");
|
|
10008
|
+
if (!fs22.existsSync(logPath)) {
|
|
9867
10009
|
console.log(
|
|
9868
10010
|
chalk8.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
9869
10011
|
);
|
|
9870
10012
|
return;
|
|
9871
10013
|
}
|
|
9872
|
-
const raw =
|
|
10014
|
+
const raw = fs22.readFileSync(logPath, "utf-8");
|
|
9873
10015
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
9874
10016
|
let entries = lines.flatMap((line) => {
|
|
9875
10017
|
try {
|
|
@@ -9921,10 +10063,398 @@ function registerAuditCommand(program2) {
|
|
|
9921
10063
|
});
|
|
9922
10064
|
}
|
|
9923
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
|
+
|
|
9924
10454
|
// src/cli/commands/daemon-cmd.ts
|
|
9925
10455
|
init_daemon2();
|
|
9926
10456
|
init_daemon();
|
|
9927
|
-
import
|
|
10457
|
+
import chalk10 from "chalk";
|
|
9928
10458
|
import { spawn as spawn7 } from "child_process";
|
|
9929
10459
|
function registerDaemonCommand(program2) {
|
|
9930
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(
|
|
@@ -9937,7 +10467,7 @@ function registerDaemonCommand(program2) {
|
|
|
9937
10467
|
if (cmd === "status") return daemonStatus();
|
|
9938
10468
|
if (cmd !== "start" && action !== void 0) {
|
|
9939
10469
|
console.error(
|
|
9940
|
-
|
|
10470
|
+
chalk10.red(`Unknown daemon action: "${action}". Use: start | stop | status`)
|
|
9941
10471
|
);
|
|
9942
10472
|
process.exit(1);
|
|
9943
10473
|
}
|
|
@@ -9945,7 +10475,7 @@ function registerDaemonCommand(program2) {
|
|
|
9945
10475
|
process.env.NODE9_WATCH_MODE = "1";
|
|
9946
10476
|
setTimeout(() => {
|
|
9947
10477
|
openBrowserLocal();
|
|
9948
|
-
console.log(
|
|
10478
|
+
console.log(chalk10.cyan(`\u{1F6F0}\uFE0F Flight Recorder: http://${DAEMON_HOST}:${DAEMON_PORT}/`));
|
|
9949
10479
|
}, 600);
|
|
9950
10480
|
startDaemon();
|
|
9951
10481
|
return;
|
|
@@ -9953,7 +10483,7 @@ function registerDaemonCommand(program2) {
|
|
|
9953
10483
|
if (options.openui) {
|
|
9954
10484
|
if (isDaemonRunning()) {
|
|
9955
10485
|
openBrowserLocal();
|
|
9956
|
-
console.log(
|
|
10486
|
+
console.log(chalk10.green(`\u{1F310} Opened browser: http://${DAEMON_HOST}:${DAEMON_PORT}/`));
|
|
9957
10487
|
process.exit(0);
|
|
9958
10488
|
}
|
|
9959
10489
|
const child = spawn7(process.execPath, [process.argv[1], "daemon"], {
|
|
@@ -9966,7 +10496,7 @@ function registerDaemonCommand(program2) {
|
|
|
9966
10496
|
if (isDaemonRunning()) break;
|
|
9967
10497
|
}
|
|
9968
10498
|
openBrowserLocal();
|
|
9969
|
-
console.log(
|
|
10499
|
+
console.log(chalk10.green(`
|
|
9970
10500
|
\u{1F6E1}\uFE0F Node9 daemon started + browser opened`));
|
|
9971
10501
|
process.exit(0);
|
|
9972
10502
|
}
|
|
@@ -9976,7 +10506,7 @@ function registerDaemonCommand(program2) {
|
|
|
9976
10506
|
stdio: "ignore"
|
|
9977
10507
|
});
|
|
9978
10508
|
child.unref();
|
|
9979
|
-
console.log(
|
|
10509
|
+
console.log(chalk10.green(`
|
|
9980
10510
|
\u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
|
|
9981
10511
|
process.exit(0);
|
|
9982
10512
|
}
|
|
@@ -9988,13 +10518,13 @@ function registerDaemonCommand(program2) {
|
|
|
9988
10518
|
// src/cli/commands/status.ts
|
|
9989
10519
|
init_core();
|
|
9990
10520
|
init_daemon();
|
|
9991
|
-
import
|
|
9992
|
-
import
|
|
9993
|
-
import
|
|
9994
|
-
import
|
|
10521
|
+
import chalk11 from "chalk";
|
|
10522
|
+
import fs24 from "fs";
|
|
10523
|
+
import path26 from "path";
|
|
10524
|
+
import os20 from "os";
|
|
9995
10525
|
function readJson2(filePath) {
|
|
9996
10526
|
try {
|
|
9997
|
-
if (
|
|
10527
|
+
if (fs24.existsSync(filePath)) return JSON.parse(fs24.readFileSync(filePath, "utf-8"));
|
|
9998
10528
|
} catch {
|
|
9999
10529
|
}
|
|
10000
10530
|
return null;
|
|
@@ -10008,21 +10538,21 @@ function wrappedMcpServers(servers) {
|
|
|
10008
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(" ")}`);
|
|
10009
10539
|
}
|
|
10010
10540
|
function printAgentSection(label, hookPairs, wrapped) {
|
|
10011
|
-
console.log(
|
|
10541
|
+
console.log(chalk11.bold(` ${label}`));
|
|
10012
10542
|
for (const { name, present } of hookPairs) {
|
|
10013
10543
|
if (present) {
|
|
10014
|
-
console.log(
|
|
10544
|
+
console.log(chalk11.green(` \u2713 ${name}`));
|
|
10015
10545
|
} else {
|
|
10016
|
-
console.log(
|
|
10546
|
+
console.log(chalk11.red(` \u2717 ${name}`) + chalk11.gray(" (not wired)"));
|
|
10017
10547
|
}
|
|
10018
10548
|
}
|
|
10019
10549
|
if (wrapped.length > 0) {
|
|
10020
|
-
console.log(
|
|
10550
|
+
console.log(chalk11.cyan(` MCP proxied:`));
|
|
10021
10551
|
for (const entry of wrapped) {
|
|
10022
|
-
console.log(
|
|
10552
|
+
console.log(chalk11.gray(` \u2022 ${entry}`));
|
|
10023
10553
|
}
|
|
10024
10554
|
} else {
|
|
10025
|
-
console.log(
|
|
10555
|
+
console.log(chalk11.gray(` MCP proxied: none`));
|
|
10026
10556
|
}
|
|
10027
10557
|
}
|
|
10028
10558
|
function registerStatusCommand(program2) {
|
|
@@ -10033,58 +10563,58 @@ function registerStatusCommand(program2) {
|
|
|
10033
10563
|
const settings = mergedConfig.settings;
|
|
10034
10564
|
console.log("");
|
|
10035
10565
|
if (creds && settings.approvers.cloud) {
|
|
10036
|
-
console.log(
|
|
10566
|
+
console.log(chalk11.green(" \u25CF Agent mode") + chalk11.gray(" \u2014 cloud team policy enforced"));
|
|
10037
10567
|
} else if (creds && !settings.approvers.cloud) {
|
|
10038
10568
|
console.log(
|
|
10039
|
-
|
|
10569
|
+
chalk11.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + chalk11.gray(" \u2014 all decisions stay on this machine")
|
|
10040
10570
|
);
|
|
10041
10571
|
} else {
|
|
10042
10572
|
console.log(
|
|
10043
|
-
|
|
10573
|
+
chalk11.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + chalk11.gray(" \u2014 no API key (Local rules only)")
|
|
10044
10574
|
);
|
|
10045
10575
|
}
|
|
10046
10576
|
console.log("");
|
|
10047
10577
|
if (daemonRunning) {
|
|
10048
10578
|
console.log(
|
|
10049
|
-
|
|
10579
|
+
chalk11.green(" \u25CF Daemon running") + chalk11.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT}/`)
|
|
10050
10580
|
);
|
|
10051
10581
|
} else {
|
|
10052
|
-
console.log(
|
|
10582
|
+
console.log(chalk11.gray(" \u25CB Daemon stopped"));
|
|
10053
10583
|
}
|
|
10054
10584
|
if (settings.enableUndo) {
|
|
10055
10585
|
console.log(
|
|
10056
|
-
|
|
10586
|
+
chalk11.magenta(" \u25CF Undo Engine") + chalk11.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
|
|
10057
10587
|
);
|
|
10058
10588
|
}
|
|
10059
10589
|
console.log("");
|
|
10060
|
-
const modeLabel = settings.mode === "audit" ?
|
|
10590
|
+
const modeLabel = settings.mode === "audit" ? chalk11.blue("audit") : settings.mode === "strict" ? chalk11.red("strict") : chalk11.white("standard");
|
|
10061
10591
|
console.log(` Mode: ${modeLabel}`);
|
|
10062
|
-
const projectConfig =
|
|
10063
|
-
const globalConfig =
|
|
10592
|
+
const projectConfig = path26.join(process.cwd(), "node9.config.json");
|
|
10593
|
+
const globalConfig = path26.join(os20.homedir(), ".node9", "config.json");
|
|
10064
10594
|
console.log(
|
|
10065
|
-
` Local: ${
|
|
10595
|
+
` Local: ${fs24.existsSync(projectConfig) ? chalk11.green("Active (node9.config.json)") : chalk11.gray("Not present")}`
|
|
10066
10596
|
);
|
|
10067
10597
|
console.log(
|
|
10068
|
-
` Global: ${
|
|
10598
|
+
` Global: ${fs24.existsSync(globalConfig) ? chalk11.green("Active (~/.node9/config.json)") : chalk11.gray("Not present")}`
|
|
10069
10599
|
);
|
|
10070
10600
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
10071
10601
|
console.log(
|
|
10072
|
-
` Sandbox: ${
|
|
10602
|
+
` Sandbox: ${chalk11.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
10073
10603
|
);
|
|
10074
10604
|
}
|
|
10075
|
-
const homeDir2 =
|
|
10605
|
+
const homeDir2 = os20.homedir();
|
|
10076
10606
|
const claudeSettings = readJson2(
|
|
10077
|
-
|
|
10607
|
+
path26.join(homeDir2, ".claude", "settings.json")
|
|
10078
10608
|
);
|
|
10079
|
-
const claudeConfig = readJson2(
|
|
10609
|
+
const claudeConfig = readJson2(path26.join(homeDir2, ".claude.json"));
|
|
10080
10610
|
const geminiSettings = readJson2(
|
|
10081
|
-
|
|
10611
|
+
path26.join(homeDir2, ".gemini", "settings.json")
|
|
10082
10612
|
);
|
|
10083
|
-
const cursorConfig = readJson2(
|
|
10613
|
+
const cursorConfig = readJson2(path26.join(homeDir2, ".cursor", "mcp.json"));
|
|
10084
10614
|
const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
|
|
10085
10615
|
if (agentFound) {
|
|
10086
10616
|
console.log("");
|
|
10087
|
-
console.log(
|
|
10617
|
+
console.log(chalk11.bold(" Agent Wiring:"));
|
|
10088
10618
|
console.log("");
|
|
10089
10619
|
if (claudeSettings || claudeConfig) {
|
|
10090
10620
|
const preHook = claudeSettings?.hooks?.PreToolUse?.some(
|
|
@@ -10130,7 +10660,7 @@ function registerStatusCommand(program2) {
|
|
|
10130
10660
|
const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
|
|
10131
10661
|
console.log("");
|
|
10132
10662
|
console.log(
|
|
10133
|
-
|
|
10663
|
+
chalk11.yellow(` \u23F8 PAUSED until ${expiresAt}`) + chalk11.gray(" \u2014 all tool calls allowed")
|
|
10134
10664
|
);
|
|
10135
10665
|
}
|
|
10136
10666
|
console.log("");
|
|
@@ -10139,10 +10669,10 @@ function registerStatusCommand(program2) {
|
|
|
10139
10669
|
|
|
10140
10670
|
// src/cli/commands/init.ts
|
|
10141
10671
|
init_core();
|
|
10142
|
-
import
|
|
10143
|
-
import
|
|
10144
|
-
import
|
|
10145
|
-
import
|
|
10672
|
+
import chalk12 from "chalk";
|
|
10673
|
+
import fs25 from "fs";
|
|
10674
|
+
import path27 from "path";
|
|
10675
|
+
import os21 from "os";
|
|
10146
10676
|
import https2 from "https";
|
|
10147
10677
|
init_shields();
|
|
10148
10678
|
var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
|
|
@@ -10177,7 +10707,7 @@ function fireTelemetryPing(agents) {
|
|
|
10177
10707
|
}
|
|
10178
10708
|
function registerInitCommand(program2) {
|
|
10179
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) => {
|
|
10180
|
-
console.log(
|
|
10710
|
+
console.log(chalk12.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
|
|
10181
10711
|
let chosenMode = options.mode.toLowerCase();
|
|
10182
10712
|
if (!["standard", "strict", "audit"].includes(chosenMode)) {
|
|
10183
10713
|
chosenMode = DEFAULT_CONFIG.settings.mode;
|
|
@@ -10196,37 +10726,37 @@ function registerInitCommand(program2) {
|
|
|
10196
10726
|
const hasNewShields = DEFAULT_SHIELDS.some((s) => !current.includes(s));
|
|
10197
10727
|
if (hasNewShields) writeActiveShields(merged);
|
|
10198
10728
|
} catch (err2) {
|
|
10199
|
-
console.log(
|
|
10729
|
+
console.log(chalk12.yellow(` \u26A0\uFE0F Could not update shields: ${String(err2)}`));
|
|
10200
10730
|
}
|
|
10201
10731
|
}
|
|
10202
10732
|
console.log("");
|
|
10203
10733
|
}
|
|
10204
|
-
const configPath =
|
|
10205
|
-
if (
|
|
10734
|
+
const configPath = path27.join(os21.homedir(), ".node9", "config.json");
|
|
10735
|
+
if (fs25.existsSync(configPath) && !options.force) {
|
|
10206
10736
|
try {
|
|
10207
|
-
const existing = JSON.parse(
|
|
10737
|
+
const existing = JSON.parse(fs25.readFileSync(configPath, "utf-8"));
|
|
10208
10738
|
const settings = existing.settings ?? {};
|
|
10209
10739
|
if (settings.mode !== chosenMode) {
|
|
10210
10740
|
settings.mode = chosenMode;
|
|
10211
10741
|
existing.settings = settings;
|
|
10212
|
-
|
|
10213
|
-
console.log(
|
|
10742
|
+
fs25.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
|
|
10743
|
+
console.log(chalk12.green(`\u2705 Mode updated: ${chosenMode}`));
|
|
10214
10744
|
} else {
|
|
10215
|
-
console.log(
|
|
10745
|
+
console.log(chalk12.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
10216
10746
|
}
|
|
10217
10747
|
} catch {
|
|
10218
|
-
console.log(
|
|
10748
|
+
console.log(chalk12.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
10219
10749
|
}
|
|
10220
10750
|
} else {
|
|
10221
10751
|
const configToSave = {
|
|
10222
10752
|
...DEFAULT_CONFIG,
|
|
10223
10753
|
settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
|
|
10224
10754
|
};
|
|
10225
|
-
const dir =
|
|
10226
|
-
if (!
|
|
10227
|
-
|
|
10228
|
-
console.log(
|
|
10229
|
-
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}`));
|
|
10230
10760
|
}
|
|
10231
10761
|
if (options.skipSetup) return;
|
|
10232
10762
|
console.log("");
|
|
@@ -10236,18 +10766,18 @@ function registerInitCommand(program2) {
|
|
|
10236
10766
|
);
|
|
10237
10767
|
if (found.length === 0) {
|
|
10238
10768
|
console.log(
|
|
10239
|
-
|
|
10769
|
+
chalk12.gray("No AI agents detected. Install Claude Code, Gemini CLI, Cursor, or Codex")
|
|
10240
10770
|
);
|
|
10241
|
-
console.log(
|
|
10771
|
+
console.log(chalk12.gray("then run: node9 addto <claude|gemini|cursor|codex>"));
|
|
10242
10772
|
return;
|
|
10243
10773
|
}
|
|
10244
|
-
console.log(
|
|
10774
|
+
console.log(chalk12.bold("Detected agents:"));
|
|
10245
10775
|
for (const agent of found) {
|
|
10246
|
-
console.log(
|
|
10776
|
+
console.log(chalk12.green(` \u2713 ${agent}`));
|
|
10247
10777
|
}
|
|
10248
10778
|
console.log("");
|
|
10249
10779
|
for (const agent of found) {
|
|
10250
|
-
console.log(
|
|
10780
|
+
console.log(chalk12.bold(`Wiring ${agent}...`));
|
|
10251
10781
|
if (agent === "claude") await setupClaude();
|
|
10252
10782
|
else if (agent === "gemini") await setupGemini();
|
|
10253
10783
|
else if (agent === "cursor") await setupCursor();
|
|
@@ -10264,26 +10794,26 @@ function registerInitCommand(program2) {
|
|
|
10264
10794
|
console.log("");
|
|
10265
10795
|
}
|
|
10266
10796
|
const agentList = found.join(", ");
|
|
10267
|
-
console.log(
|
|
10797
|
+
console.log(chalk12.green.bold(`\u{1F6E1}\uFE0F Node9 is protecting ${agentList}!`));
|
|
10268
10798
|
console.log("");
|
|
10269
|
-
console.log(
|
|
10270
|
-
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"));
|
|
10271
10801
|
console.log("");
|
|
10272
|
-
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"));
|
|
10273
10803
|
console.log(
|
|
10274
|
-
|
|
10804
|
+
chalk12.white(" Team dashboard + full audit trail \u2192 ") + chalk12.cyan.bold("https://node9.ai")
|
|
10275
10805
|
);
|
|
10276
|
-
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"));
|
|
10277
10807
|
});
|
|
10278
10808
|
}
|
|
10279
10809
|
|
|
10280
10810
|
// src/cli/commands/undo.ts
|
|
10281
|
-
import
|
|
10282
|
-
import
|
|
10811
|
+
import path28 from "path";
|
|
10812
|
+
import chalk14 from "chalk";
|
|
10283
10813
|
|
|
10284
10814
|
// src/tui/undo-navigator.ts
|
|
10285
10815
|
import readline2 from "readline";
|
|
10286
|
-
import
|
|
10816
|
+
import chalk13 from "chalk";
|
|
10287
10817
|
var RESET = "\x1B[0m";
|
|
10288
10818
|
var BOLD = "\x1B[1m";
|
|
10289
10819
|
var CLEAR_SCREEN = "\x1B[2J\x1B[H";
|
|
@@ -10301,15 +10831,15 @@ function renderDiff(raw) {
|
|
|
10301
10831
|
);
|
|
10302
10832
|
for (const line of lines) {
|
|
10303
10833
|
if (line.startsWith("+++") || line.startsWith("---")) {
|
|
10304
|
-
process.stdout.write(
|
|
10834
|
+
process.stdout.write(chalk13.bold(line) + "\n");
|
|
10305
10835
|
} else if (line.startsWith("+")) {
|
|
10306
|
-
process.stdout.write(
|
|
10836
|
+
process.stdout.write(chalk13.green(line) + "\n");
|
|
10307
10837
|
} else if (line.startsWith("-")) {
|
|
10308
|
-
process.stdout.write(
|
|
10838
|
+
process.stdout.write(chalk13.red(line) + "\n");
|
|
10309
10839
|
} else if (line.startsWith("@@")) {
|
|
10310
|
-
process.stdout.write(
|
|
10840
|
+
process.stdout.write(chalk13.cyan(line) + "\n");
|
|
10311
10841
|
} else {
|
|
10312
|
-
process.stdout.write(
|
|
10842
|
+
process.stdout.write(chalk13.gray(line) + "\n");
|
|
10313
10843
|
}
|
|
10314
10844
|
}
|
|
10315
10845
|
}
|
|
@@ -10328,23 +10858,23 @@ function render(entries, idx) {
|
|
|
10328
10858
|
const step = idx + 1;
|
|
10329
10859
|
process.stdout.write(CLEAR_SCREEN);
|
|
10330
10860
|
process.stdout.write(
|
|
10331
|
-
|
|
10861
|
+
chalk13.magenta.bold(`\u23EA Node9 Undo`) + chalk13.gray(` \u2500\u2500 step ${step} of ${total}`) + (entry.files?.length ? chalk13.gray(
|
|
10332
10862
|
` \u2500\u2500 ${entry.files.slice(0, 2).join(", ")}${entry.files.length > 2 ? ` +${entry.files.length - 2} more` : ""}`
|
|
10333
10863
|
) : "") + "\n\n"
|
|
10334
10864
|
);
|
|
10335
10865
|
process.stdout.write(
|
|
10336
|
-
` ${BOLD}Tool:${RESET} ${
|
|
10866
|
+
` ${BOLD}Tool:${RESET} ${chalk13.cyan(entry.tool)}` + (entry.argsSummary ? chalk13.gray(" \u2192 " + entry.argsSummary) : "") + "\n"
|
|
10337
10867
|
);
|
|
10338
|
-
process.stdout.write(` ${BOLD}When:${RESET} ${
|
|
10868
|
+
process.stdout.write(` ${BOLD}When:${RESET} ${chalk13.gray(formatAge(entry.timestamp))}
|
|
10339
10869
|
`);
|
|
10340
|
-
process.stdout.write(` ${BOLD}Dir: ${RESET} ${
|
|
10870
|
+
process.stdout.write(` ${BOLD}Dir: ${RESET} ${chalk13.gray(entry.cwd)}
|
|
10341
10871
|
`);
|
|
10342
10872
|
if (entry.files && entry.files.length > 0) {
|
|
10343
|
-
process.stdout.write(` ${BOLD}Files:${RESET} ${
|
|
10873
|
+
process.stdout.write(` ${BOLD}Files:${RESET} ${chalk13.gray(entry.files.join(", "))}
|
|
10344
10874
|
`);
|
|
10345
10875
|
}
|
|
10346
10876
|
if (idx < total - 1 && isSessionBoundary(entries, idx + 1)) {
|
|
10347
|
-
process.stdout.write(
|
|
10877
|
+
process.stdout.write(chalk13.gray("\n \u2500\u2500 session boundary above \u2500\u2500\n"));
|
|
10348
10878
|
}
|
|
10349
10879
|
process.stdout.write("\n");
|
|
10350
10880
|
const diff = entry.diff ?? computeUndoDiff(entry.hash, entry.cwd);
|
|
@@ -10352,12 +10882,12 @@ function render(entries, idx) {
|
|
|
10352
10882
|
renderDiff(diff);
|
|
10353
10883
|
} else {
|
|
10354
10884
|
process.stdout.write(
|
|
10355
|
-
|
|
10885
|
+
chalk13.gray(" (no diff \u2014 working tree may already match this snapshot)\n")
|
|
10356
10886
|
);
|
|
10357
10887
|
}
|
|
10358
10888
|
process.stdout.write("\n");
|
|
10359
10889
|
process.stdout.write(
|
|
10360
|
-
|
|
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"
|
|
10361
10891
|
);
|
|
10362
10892
|
}
|
|
10363
10893
|
async function runUndoNavigator(entries) {
|
|
@@ -10411,19 +10941,19 @@ async function runUndoNavigator(entries) {
|
|
|
10411
10941
|
cleanup();
|
|
10412
10942
|
process.stdout.write(CLEAR_SCREEN);
|
|
10413
10943
|
const entry = display[idx];
|
|
10414
|
-
process.stdout.write(
|
|
10944
|
+
process.stdout.write(chalk13.magenta.bold("\n\u23EA Restoring snapshot...\n\n"));
|
|
10415
10945
|
if (applyUndo(entry.hash, entry.cwd)) {
|
|
10416
|
-
process.stdout.write(
|
|
10946
|
+
process.stdout.write(chalk13.green("\u2705 Reverted successfully.\n\n"));
|
|
10417
10947
|
resolve({ restored: true });
|
|
10418
10948
|
} else {
|
|
10419
|
-
process.stdout.write(
|
|
10949
|
+
process.stdout.write(chalk13.red("\u274C Undo failed.\n\n"));
|
|
10420
10950
|
resolve({ restored: false });
|
|
10421
10951
|
}
|
|
10422
10952
|
} else if (name === "q" || key?.ctrl && name === "c") {
|
|
10423
10953
|
done = true;
|
|
10424
10954
|
cleanup();
|
|
10425
10955
|
process.stdout.write(CLEAR_SCREEN);
|
|
10426
|
-
process.stdout.write(
|
|
10956
|
+
process.stdout.write(chalk13.gray("\nCancelled.\n\n"));
|
|
10427
10957
|
resolve({ restored: false });
|
|
10428
10958
|
}
|
|
10429
10959
|
};
|
|
@@ -10437,7 +10967,7 @@ function findMatchingCwd(startDir, history) {
|
|
|
10437
10967
|
let dir = startDir;
|
|
10438
10968
|
while (true) {
|
|
10439
10969
|
if (cwds.has(dir)) return dir;
|
|
10440
|
-
const parent =
|
|
10970
|
+
const parent = path28.dirname(dir);
|
|
10441
10971
|
if (parent === dir) return null;
|
|
10442
10972
|
dir = parent;
|
|
10443
10973
|
}
|
|
@@ -10459,39 +10989,39 @@ function registerUndoCommand(program2) {
|
|
|
10459
10989
|
if (history.length === 0) {
|
|
10460
10990
|
if (!options.all && allHistory.length > 0) {
|
|
10461
10991
|
console.log(
|
|
10462
|
-
|
|
10992
|
+
chalk14.yellow(
|
|
10463
10993
|
`
|
|
10464
10994
|
\u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
|
|
10465
|
-
Run ${
|
|
10995
|
+
Run ${chalk14.cyan("node9 undo --all")} to see snapshots from all projects.
|
|
10466
10996
|
`
|
|
10467
10997
|
)
|
|
10468
10998
|
);
|
|
10469
10999
|
} else {
|
|
10470
|
-
console.log(
|
|
11000
|
+
console.log(chalk14.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
|
|
10471
11001
|
}
|
|
10472
11002
|
return;
|
|
10473
11003
|
}
|
|
10474
11004
|
if (options.list) {
|
|
10475
|
-
console.log(
|
|
11005
|
+
console.log(chalk14.magenta.bold("\n\u23EA Snapshot History\n"));
|
|
10476
11006
|
console.log(
|
|
10477
|
-
|
|
11007
|
+
chalk14.gray(
|
|
10478
11008
|
` ${"#".padEnd(3)} ${"File / Command".padEnd(30)} ${"Tool".padEnd(8)} ${"When".padEnd(10)} Dir`
|
|
10479
11009
|
)
|
|
10480
11010
|
);
|
|
10481
|
-
console.log(
|
|
11011
|
+
console.log(chalk14.gray(" " + "\u2500".repeat(80)));
|
|
10482
11012
|
const display = [...history].reverse();
|
|
10483
11013
|
let prevTs = null;
|
|
10484
11014
|
for (let i = 0; i < display.length; i++) {
|
|
10485
11015
|
const e = display[i];
|
|
10486
11016
|
const isGap = prevTs !== null && prevTs - e.timestamp > 6e4;
|
|
10487
|
-
if (isGap) console.log(
|
|
11017
|
+
if (isGap) console.log(chalk14.gray(" \u2500\u2500 earlier \u2500\u2500"));
|
|
10488
11018
|
const label = (e.argsSummary || e.files?.[0] || "\u2014").slice(0, 30).padEnd(30);
|
|
10489
11019
|
const tool = e.tool.slice(0, 8).padEnd(8);
|
|
10490
11020
|
const when = formatAge2(e.timestamp).padEnd(10);
|
|
10491
11021
|
const dir = e.cwd.length > 30 ? "\u2026" + e.cwd.slice(-29) : e.cwd;
|
|
10492
11022
|
console.log(
|
|
10493
|
-
|
|
10494
|
-
` ${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)}`
|
|
10495
11025
|
)
|
|
10496
11026
|
);
|
|
10497
11027
|
prevTs = e.timestamp;
|
|
@@ -10504,7 +11034,7 @@ function registerUndoCommand(program2) {
|
|
|
10504
11034
|
const idx = history.length - steps;
|
|
10505
11035
|
if (idx < 0) {
|
|
10506
11036
|
console.log(
|
|
10507
|
-
|
|
11037
|
+
chalk14.yellow(
|
|
10508
11038
|
`
|
|
10509
11039
|
\u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
|
|
10510
11040
|
`
|
|
@@ -10515,47 +11045,47 @@ function registerUndoCommand(program2) {
|
|
|
10515
11045
|
const snapshot = history[idx];
|
|
10516
11046
|
const ageStr = formatAge2(snapshot.timestamp);
|
|
10517
11047
|
console.log(
|
|
10518
|
-
|
|
11048
|
+
chalk14.magenta.bold(`
|
|
10519
11049
|
\u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`)
|
|
10520
11050
|
);
|
|
10521
11051
|
console.log(
|
|
10522
|
-
|
|
10523
|
-
` Tool: ${
|
|
11052
|
+
chalk14.white(
|
|
11053
|
+
` Tool: ${chalk14.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk14.gray(" \u2192 " + snapshot.argsSummary) : ""}`
|
|
10524
11054
|
)
|
|
10525
11055
|
);
|
|
10526
|
-
console.log(
|
|
10527
|
-
console.log(
|
|
11056
|
+
console.log(chalk14.white(` When: ${chalk14.gray(ageStr)}`));
|
|
11057
|
+
console.log(chalk14.white(` Dir: ${chalk14.gray(snapshot.cwd)}`));
|
|
10528
11058
|
if (steps > 1)
|
|
10529
11059
|
console.log(
|
|
10530
|
-
|
|
11060
|
+
chalk14.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
|
|
10531
11061
|
);
|
|
10532
11062
|
console.log("");
|
|
10533
11063
|
const diff = snapshot.diff ?? computeUndoDiff(snapshot.hash, snapshot.cwd);
|
|
10534
11064
|
if (diff) {
|
|
10535
11065
|
const lines = diff.split("\n").filter((l) => !l.startsWith("diff --git") && !l.startsWith("index "));
|
|
10536
11066
|
for (const line of lines) {
|
|
10537
|
-
if (line.startsWith("+++") || line.startsWith("---")) console.log(
|
|
10538
|
-
else if (line.startsWith("+")) console.log(
|
|
10539
|
-
else if (line.startsWith("-")) console.log(
|
|
10540
|
-
else if (line.startsWith("@@")) console.log(
|
|
10541
|
-
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));
|
|
10542
11072
|
}
|
|
10543
11073
|
console.log("");
|
|
10544
11074
|
} else {
|
|
10545
11075
|
console.log(
|
|
10546
|
-
|
|
11076
|
+
chalk14.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
|
|
10547
11077
|
);
|
|
10548
11078
|
}
|
|
10549
11079
|
const { confirm: confirm3 } = await import("@inquirer/prompts");
|
|
10550
11080
|
const proceed = await confirm3({ message: `Revert to this snapshot?`, default: false });
|
|
10551
11081
|
if (proceed) {
|
|
10552
11082
|
if (applyUndo(snapshot.hash, snapshot.cwd)) {
|
|
10553
|
-
console.log(
|
|
11083
|
+
console.log(chalk14.green("\n\u2705 Reverted successfully.\n"));
|
|
10554
11084
|
} else {
|
|
10555
|
-
console.error(
|
|
11085
|
+
console.error(chalk14.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
|
|
10556
11086
|
}
|
|
10557
11087
|
} else {
|
|
10558
|
-
console.log(
|
|
11088
|
+
console.log(chalk14.gray("\nCancelled.\n"));
|
|
10559
11089
|
}
|
|
10560
11090
|
return;
|
|
10561
11091
|
}
|
|
@@ -10565,7 +11095,7 @@ function registerUndoCommand(program2) {
|
|
|
10565
11095
|
|
|
10566
11096
|
// src/cli/commands/watch.ts
|
|
10567
11097
|
init_daemon();
|
|
10568
|
-
import
|
|
11098
|
+
import chalk15 from "chalk";
|
|
10569
11099
|
import { spawn as spawn8, spawnSync as spawnSync5 } from "child_process";
|
|
10570
11100
|
function registerWatchCommand(program2) {
|
|
10571
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) => {
|
|
@@ -10581,7 +11111,7 @@ function registerWatchCommand(program2) {
|
|
|
10581
11111
|
throw new Error("not running");
|
|
10582
11112
|
}
|
|
10583
11113
|
} catch {
|
|
10584
|
-
console.error(
|
|
11114
|
+
console.error(chalk15.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
|
|
10585
11115
|
const child = spawn8(process.execPath, [process.argv[1], "daemon"], {
|
|
10586
11116
|
detached: true,
|
|
10587
11117
|
stdio: "ignore",
|
|
@@ -10603,12 +11133,12 @@ function registerWatchCommand(program2) {
|
|
|
10603
11133
|
}
|
|
10604
11134
|
}
|
|
10605
11135
|
if (!ready) {
|
|
10606
|
-
console.error(
|
|
11136
|
+
console.error(chalk15.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
10607
11137
|
process.exit(1);
|
|
10608
11138
|
}
|
|
10609
11139
|
}
|
|
10610
11140
|
console.error(
|
|
10611
|
-
|
|
11141
|
+
chalk15.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + chalk15.dim(` \u2192 localhost:${port}`) + chalk15.dim(
|
|
10612
11142
|
"\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
|
|
10613
11143
|
)
|
|
10614
11144
|
);
|
|
@@ -10617,7 +11147,7 @@ function registerWatchCommand(program2) {
|
|
|
10617
11147
|
env: { ...process.env, NODE9_WATCH_MODE: "1" }
|
|
10618
11148
|
});
|
|
10619
11149
|
if (result.error) {
|
|
10620
|
-
console.error(
|
|
11150
|
+
console.error(chalk15.red(`\u274C Failed to run command: ${result.error.message}`));
|
|
10621
11151
|
process.exit(1);
|
|
10622
11152
|
}
|
|
10623
11153
|
process.exit(result.status ?? 0);
|
|
@@ -10627,18 +11157,18 @@ function registerWatchCommand(program2) {
|
|
|
10627
11157
|
// src/mcp-gateway/index.ts
|
|
10628
11158
|
init_orchestrator();
|
|
10629
11159
|
import readline3 from "readline";
|
|
10630
|
-
import
|
|
11160
|
+
import chalk16 from "chalk";
|
|
10631
11161
|
import { spawn as spawn9 } from "child_process";
|
|
10632
11162
|
import { execa as execa2 } from "execa";
|
|
10633
11163
|
init_provenance();
|
|
10634
11164
|
|
|
10635
11165
|
// src/mcp-pin.ts
|
|
10636
|
-
import
|
|
10637
|
-
import
|
|
10638
|
-
import
|
|
10639
|
-
import
|
|
11166
|
+
import fs26 from "fs";
|
|
11167
|
+
import path29 from "path";
|
|
11168
|
+
import os22 from "os";
|
|
11169
|
+
import crypto4 from "crypto";
|
|
10640
11170
|
function getPinsFilePath() {
|
|
10641
|
-
return
|
|
11171
|
+
return path29.join(os22.homedir(), ".node9", "mcp-pins.json");
|
|
10642
11172
|
}
|
|
10643
11173
|
function hashToolDefinitions(tools) {
|
|
10644
11174
|
const sorted = [...tools].sort((a, b) => {
|
|
@@ -10647,15 +11177,15 @@ function hashToolDefinitions(tools) {
|
|
|
10647
11177
|
return nameA.localeCompare(nameB);
|
|
10648
11178
|
});
|
|
10649
11179
|
const canonical = JSON.stringify(sorted);
|
|
10650
|
-
return
|
|
11180
|
+
return crypto4.createHash("sha256").update(canonical).digest("hex");
|
|
10651
11181
|
}
|
|
10652
11182
|
function getServerKey(upstreamCommand) {
|
|
10653
|
-
return
|
|
11183
|
+
return crypto4.createHash("sha256").update(upstreamCommand).digest("hex").slice(0, 16);
|
|
10654
11184
|
}
|
|
10655
11185
|
function readMcpPinsSafe() {
|
|
10656
11186
|
const filePath = getPinsFilePath();
|
|
10657
11187
|
try {
|
|
10658
|
-
const raw =
|
|
11188
|
+
const raw = fs26.readFileSync(filePath, "utf-8");
|
|
10659
11189
|
if (!raw.trim()) {
|
|
10660
11190
|
return { ok: false, reason: "corrupt", detail: "empty file" };
|
|
10661
11191
|
}
|
|
@@ -10679,10 +11209,10 @@ function readMcpPins() {
|
|
|
10679
11209
|
}
|
|
10680
11210
|
function writeMcpPins(data) {
|
|
10681
11211
|
const filePath = getPinsFilePath();
|
|
10682
|
-
|
|
10683
|
-
const tmp = `${filePath}.${
|
|
10684
|
-
|
|
10685
|
-
|
|
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);
|
|
10686
11216
|
}
|
|
10687
11217
|
function checkPin(serverKey, currentHash) {
|
|
10688
11218
|
const result = readMcpPinsSafe();
|
|
@@ -10774,13 +11304,13 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
10774
11304
|
const prov = checkProvenance(executable);
|
|
10775
11305
|
if (prov.trustLevel === "suspect") {
|
|
10776
11306
|
console.error(
|
|
10777
|
-
|
|
11307
|
+
chalk16.red(
|
|
10778
11308
|
`\u26A0\uFE0F Node9: Upstream MCP server binary is suspect \u2014 ${prov.reason} (${prov.resolvedPath})`
|
|
10779
11309
|
)
|
|
10780
11310
|
);
|
|
10781
|
-
console.error(
|
|
11311
|
+
console.error(chalk16.red(" Verify this binary is trusted before proceeding."));
|
|
10782
11312
|
}
|
|
10783
|
-
console.error(
|
|
11313
|
+
console.error(chalk16.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
|
|
10784
11314
|
const UPSTREAM_INJECTOR_VARS = /* @__PURE__ */ new Set([
|
|
10785
11315
|
"NODE_OPTIONS",
|
|
10786
11316
|
"NODE_PATH",
|
|
@@ -10883,10 +11413,10 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
10883
11413
|
mcpServer
|
|
10884
11414
|
});
|
|
10885
11415
|
if (!result.approved) {
|
|
10886
|
-
console.error(
|
|
11416
|
+
console.error(chalk16.red(`
|
|
10887
11417
|
\u{1F6D1} Node9 MCP Gateway: Action Blocked`));
|
|
10888
|
-
console.error(
|
|
10889
|
-
console.error(
|
|
11418
|
+
console.error(chalk16.gray(` Tool: ${toolName}`));
|
|
11419
|
+
console.error(chalk16.gray(` Reason: ${result.reason ?? "Security Policy"}
|
|
10890
11420
|
`));
|
|
10891
11421
|
const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
|
|
10892
11422
|
const isHumanDecision = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
|
|
@@ -10965,7 +11495,7 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
10965
11495
|
updatePin(serverKey, upstreamCommand, currentHash, toolNames);
|
|
10966
11496
|
pinState = "validated";
|
|
10967
11497
|
console.error(
|
|
10968
|
-
|
|
11498
|
+
chalk16.green(
|
|
10969
11499
|
`\u{1F512} Node9: Pinned ${toolNames.length} tool definition(s) for this MCP server`
|
|
10970
11500
|
)
|
|
10971
11501
|
);
|
|
@@ -10978,11 +11508,11 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
10978
11508
|
} else if (pinStatus === "corrupt") {
|
|
10979
11509
|
pinState = "quarantined";
|
|
10980
11510
|
console.error(
|
|
10981
|
-
|
|
11511
|
+
chalk16.red("\n\u{1F6A8} Node9: MCP pin file is corrupt or unreadable \u2014 session quarantined!")
|
|
10982
11512
|
);
|
|
10983
|
-
console.error(
|
|
11513
|
+
console.error(chalk16.red(" Tool calls are blocked until the pin file is repaired."));
|
|
10984
11514
|
console.error(
|
|
10985
|
-
|
|
11515
|
+
chalk16.yellow(` Run: node9 mcp pin reset (to clear and re-pin on next connect)
|
|
10986
11516
|
`)
|
|
10987
11517
|
);
|
|
10988
11518
|
const errorResponse = {
|
|
@@ -10999,13 +11529,13 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
10999
11529
|
} else {
|
|
11000
11530
|
pinState = "quarantined";
|
|
11001
11531
|
console.error(
|
|
11002
|
-
|
|
11532
|
+
chalk16.red("\n\u{1F6A8} Node9: MCP tool definitions have changed since last verified!")
|
|
11003
11533
|
);
|
|
11004
11534
|
console.error(
|
|
11005
|
-
|
|
11535
|
+
chalk16.red(" This could indicate a supply chain attack (tool poisoning / rug pull).")
|
|
11006
11536
|
);
|
|
11007
|
-
console.error(
|
|
11008
|
-
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}
|
|
11009
11539
|
`));
|
|
11010
11540
|
const errorResponse = {
|
|
11011
11541
|
jsonrpc: "2.0",
|
|
@@ -11054,9 +11584,9 @@ function registerMcpGatewayCommand(program2) {
|
|
|
11054
11584
|
|
|
11055
11585
|
// src/mcp-server/index.ts
|
|
11056
11586
|
import readline4 from "readline";
|
|
11057
|
-
import
|
|
11058
|
-
import
|
|
11059
|
-
import
|
|
11587
|
+
import fs27 from "fs";
|
|
11588
|
+
import os23 from "os";
|
|
11589
|
+
import path30 from "path";
|
|
11060
11590
|
init_core();
|
|
11061
11591
|
init_daemon();
|
|
11062
11592
|
init_shields();
|
|
@@ -11231,13 +11761,13 @@ function handleStatus() {
|
|
|
11231
11761
|
lines.push(`Active shields: ${activeShields.length > 0 ? activeShields.join(", ") : "none"}`);
|
|
11232
11762
|
lines.push(`Smart rules: ${config.policy.smartRules.length} loaded`);
|
|
11233
11763
|
lines.push(`DLP: ${config.policy.dlp?.enabled !== false ? "enabled" : "disabled"}`);
|
|
11234
|
-
const projectConfig =
|
|
11235
|
-
const globalConfig =
|
|
11764
|
+
const projectConfig = path30.join(process.cwd(), "node9.config.json");
|
|
11765
|
+
const globalConfig = path30.join(os23.homedir(), ".node9", "config.json");
|
|
11236
11766
|
lines.push(
|
|
11237
|
-
`Project config (node9.config.json): ${
|
|
11767
|
+
`Project config (node9.config.json): ${fs27.existsSync(projectConfig) ? "present" : "not found"}`
|
|
11238
11768
|
);
|
|
11239
11769
|
lines.push(
|
|
11240
|
-
`Global config (~/.node9/config.json): ${
|
|
11770
|
+
`Global config (~/.node9/config.json): ${fs27.existsSync(globalConfig) ? "present" : "not found"}`
|
|
11241
11771
|
);
|
|
11242
11772
|
return lines.join("\n");
|
|
11243
11773
|
}
|
|
@@ -11311,21 +11841,21 @@ function handleShieldDisable(args) {
|
|
|
11311
11841
|
writeActiveShields(active.filter((s) => s !== name));
|
|
11312
11842
|
return `Shield "${name}" disabled.`;
|
|
11313
11843
|
}
|
|
11314
|
-
var GLOBAL_CONFIG_PATH2 =
|
|
11844
|
+
var GLOBAL_CONFIG_PATH2 = path30.join(os23.homedir(), ".node9", "config.json");
|
|
11315
11845
|
var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
|
|
11316
11846
|
function readGlobalConfigRaw() {
|
|
11317
11847
|
try {
|
|
11318
|
-
if (
|
|
11319
|
-
return JSON.parse(
|
|
11848
|
+
if (fs27.existsSync(GLOBAL_CONFIG_PATH2)) {
|
|
11849
|
+
return JSON.parse(fs27.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
|
|
11320
11850
|
}
|
|
11321
11851
|
} catch {
|
|
11322
11852
|
}
|
|
11323
11853
|
return {};
|
|
11324
11854
|
}
|
|
11325
11855
|
function writeGlobalConfigRaw(data) {
|
|
11326
|
-
const dir =
|
|
11327
|
-
if (!
|
|
11328
|
-
|
|
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");
|
|
11329
11859
|
}
|
|
11330
11860
|
function handleApproverList() {
|
|
11331
11861
|
const config = getConfig();
|
|
@@ -11368,9 +11898,9 @@ function handleApproverSet(args) {
|
|
|
11368
11898
|
}
|
|
11369
11899
|
function handleAuditGet(args) {
|
|
11370
11900
|
const limit = Math.min(typeof args.limit === "number" ? args.limit : 20, 100);
|
|
11371
|
-
const auditPath =
|
|
11372
|
-
if (!
|
|
11373
|
-
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);
|
|
11374
11904
|
const recent = lines.slice(-limit);
|
|
11375
11905
|
const entries = recent.map((line) => {
|
|
11376
11906
|
try {
|
|
@@ -11558,7 +12088,7 @@ function registerMcpServerCommand(program2) {
|
|
|
11558
12088
|
|
|
11559
12089
|
// src/cli/commands/trust.ts
|
|
11560
12090
|
init_trusted_hosts();
|
|
11561
|
-
import
|
|
12091
|
+
import chalk17 from "chalk";
|
|
11562
12092
|
function isValidHost(host) {
|
|
11563
12093
|
return /^(\*\.)?[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/.test(host);
|
|
11564
12094
|
}
|
|
@@ -11568,51 +12098,51 @@ function registerTrustCommand(program2) {
|
|
|
11568
12098
|
const normalized = normalizeHost(host.trim());
|
|
11569
12099
|
if (!isValidHost(normalized)) {
|
|
11570
12100
|
console.error(
|
|
11571
|
-
|
|
12101
|
+
chalk17.red(`
|
|
11572
12102
|
\u274C Invalid host: "${host}"
|
|
11573
|
-
`) +
|
|
12103
|
+
`) + chalk17.gray(" Use an FQDN like api.mycompany.com or *.mycompany.com\n")
|
|
11574
12104
|
);
|
|
11575
12105
|
process.exit(1);
|
|
11576
12106
|
}
|
|
11577
12107
|
addTrustedHost(normalized);
|
|
11578
|
-
console.log(
|
|
12108
|
+
console.log(chalk17.green(`
|
|
11579
12109
|
\u2705 ${normalized} added to trusted hosts.`));
|
|
11580
12110
|
console.log(
|
|
11581
|
-
|
|
12111
|
+
chalk17.gray(" Pipe-chain blocks to this host: critical \u2192 review, high \u2192 allow\n")
|
|
11582
12112
|
);
|
|
11583
12113
|
});
|
|
11584
12114
|
trustCmd.command("remove <host>").description("Remove a trusted host").action((host) => {
|
|
11585
12115
|
const normalized = normalizeHost(host.trim());
|
|
11586
12116
|
const removed = removeTrustedHost(normalized);
|
|
11587
12117
|
if (!removed) {
|
|
11588
|
-
console.error(
|
|
12118
|
+
console.error(chalk17.yellow(`
|
|
11589
12119
|
\u26A0\uFE0F "${normalized}" is not in the trusted hosts list.
|
|
11590
12120
|
`));
|
|
11591
12121
|
process.exit(1);
|
|
11592
12122
|
}
|
|
11593
|
-
console.log(
|
|
12123
|
+
console.log(chalk17.green(`
|
|
11594
12124
|
\u2705 ${normalized} removed from trusted hosts.
|
|
11595
12125
|
`));
|
|
11596
12126
|
});
|
|
11597
12127
|
trustCmd.command("list").description("Show all trusted hosts").action(() => {
|
|
11598
12128
|
const hosts = readTrustedHosts();
|
|
11599
12129
|
if (hosts.length === 0) {
|
|
11600
|
-
console.log(
|
|
11601
|
-
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")}
|
|
11602
12132
|
`);
|
|
11603
12133
|
return;
|
|
11604
12134
|
}
|
|
11605
|
-
console.log(
|
|
12135
|
+
console.log(chalk17.bold("\n\u{1F513} Trusted Hosts\n"));
|
|
11606
12136
|
for (const entry of hosts) {
|
|
11607
12137
|
const date = new Date(entry.addedAt).toLocaleDateString();
|
|
11608
|
-
console.log(` ${
|
|
12138
|
+
console.log(` ${chalk17.cyan(entry.host.padEnd(40))} ${chalk17.gray(`added ${date}`)}`);
|
|
11609
12139
|
}
|
|
11610
12140
|
console.log("");
|
|
11611
12141
|
});
|
|
11612
12142
|
}
|
|
11613
12143
|
|
|
11614
12144
|
// src/cli/commands/mcp-pin.ts
|
|
11615
|
-
import
|
|
12145
|
+
import chalk18 from "chalk";
|
|
11616
12146
|
function registerMcpPinCommand(program2) {
|
|
11617
12147
|
const pinCmd = program2.command("mcp").description("Manage MCP server tool definition pinning (rug pull defense)");
|
|
11618
12148
|
const pinSubCmd = pinCmd.command("pin").description("Manage pinned MCP server tool definitions");
|
|
@@ -11620,31 +12150,31 @@ function registerMcpPinCommand(program2) {
|
|
|
11620
12150
|
const result = readMcpPinsSafe();
|
|
11621
12151
|
if (!result.ok) {
|
|
11622
12152
|
if (result.reason === "missing") {
|
|
11623
|
-
console.log(
|
|
12153
|
+
console.log(chalk18.gray("\nNo MCP servers are pinned yet."));
|
|
11624
12154
|
console.log(
|
|
11625
|
-
|
|
12155
|
+
chalk18.gray("Pins are created automatically when the MCP gateway first connects.\n")
|
|
11626
12156
|
);
|
|
11627
12157
|
return;
|
|
11628
12158
|
}
|
|
11629
|
-
console.error(
|
|
12159
|
+
console.error(chalk18.red(`
|
|
11630
12160
|
\u274C Pin file is corrupt: ${result.detail}`));
|
|
11631
|
-
console.error(
|
|
12161
|
+
console.error(chalk18.yellow(" Run: node9 mcp pin reset\n"));
|
|
11632
12162
|
process.exit(1);
|
|
11633
12163
|
}
|
|
11634
12164
|
const entries = Object.entries(result.pins.servers);
|
|
11635
12165
|
if (entries.length === 0) {
|
|
11636
|
-
console.log(
|
|
12166
|
+
console.log(chalk18.gray("\nNo MCP servers are pinned yet."));
|
|
11637
12167
|
console.log(
|
|
11638
|
-
|
|
12168
|
+
chalk18.gray("Pins are created automatically when the MCP gateway first connects.\n")
|
|
11639
12169
|
);
|
|
11640
12170
|
return;
|
|
11641
12171
|
}
|
|
11642
|
-
console.log(
|
|
12172
|
+
console.log(chalk18.bold("\n\u{1F512} Pinned MCP Servers\n"));
|
|
11643
12173
|
for (const [key, entry] of entries) {
|
|
11644
|
-
console.log(` ${
|
|
11645
|
-
console.log(` Tools (${entry.toolCount}): ${
|
|
11646
|
-
console.log(` Hash: ${
|
|
11647
|
-
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)}`);
|
|
11648
12178
|
console.log("");
|
|
11649
12179
|
}
|
|
11650
12180
|
});
|
|
@@ -11655,55 +12185,55 @@ function registerMcpPinCommand(program2) {
|
|
|
11655
12185
|
try {
|
|
11656
12186
|
pins = readMcpPins();
|
|
11657
12187
|
} catch {
|
|
11658
|
-
console.error(
|
|
11659
|
-
console.error(
|
|
12188
|
+
console.error(chalk18.red("\n\u274C Pin file is corrupt."));
|
|
12189
|
+
console.error(chalk18.yellow(" Run: node9 mcp pin reset\n"));
|
|
11660
12190
|
process.exit(1);
|
|
11661
12191
|
}
|
|
11662
12192
|
if (!pins.servers[serverKey]) {
|
|
11663
|
-
console.error(
|
|
12193
|
+
console.error(chalk18.red(`
|
|
11664
12194
|
\u274C No pin found for server key "${serverKey}"
|
|
11665
12195
|
`));
|
|
11666
|
-
console.error(`Run ${
|
|
12196
|
+
console.error(`Run ${chalk18.cyan("node9 mcp pin list")} to see pinned servers.
|
|
11667
12197
|
`);
|
|
11668
12198
|
process.exit(1);
|
|
11669
12199
|
}
|
|
11670
12200
|
const label = pins.servers[serverKey].label;
|
|
11671
12201
|
removePin(serverKey);
|
|
11672
|
-
console.log(
|
|
11673
|
-
\u{1F513} Pin removed for ${
|
|
11674
|
-
console.log(
|
|
11675
|
-
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"));
|
|
11676
12206
|
});
|
|
11677
12207
|
pinSubCmd.command("reset").description("Clear all MCP pins (next connection to each server will re-pin)").action(() => {
|
|
11678
12208
|
const result = readMcpPinsSafe();
|
|
11679
12209
|
if (!result.ok && result.reason === "missing") {
|
|
11680
|
-
console.log(
|
|
12210
|
+
console.log(chalk18.gray("\nNo pins to clear.\n"));
|
|
11681
12211
|
return;
|
|
11682
12212
|
}
|
|
11683
12213
|
const count = result.ok ? Object.keys(result.pins.servers).length : "?";
|
|
11684
12214
|
clearAllPins();
|
|
11685
|
-
console.log(
|
|
12215
|
+
console.log(chalk18.green(`
|
|
11686
12216
|
\u{1F513} Cleared ${count} MCP pin(s).`));
|
|
11687
|
-
console.log(
|
|
12217
|
+
console.log(chalk18.gray(" Next connection to each server will re-pin.\n"));
|
|
11688
12218
|
});
|
|
11689
12219
|
}
|
|
11690
12220
|
|
|
11691
12221
|
// src/cli.ts
|
|
11692
12222
|
var { version } = JSON.parse(
|
|
11693
|
-
|
|
12223
|
+
fs30.readFileSync(path33.join(__dirname, "../package.json"), "utf-8")
|
|
11694
12224
|
);
|
|
11695
12225
|
var program = new Command();
|
|
11696
12226
|
program.name("node9").description("The Sudo Command for AI Agents").version(version);
|
|
11697
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) => {
|
|
11698
12228
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
11699
|
-
const credPath =
|
|
11700
|
-
if (!
|
|
11701
|
-
|
|
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 });
|
|
11702
12232
|
const profileName = options.profile || "default";
|
|
11703
12233
|
let existingCreds = {};
|
|
11704
12234
|
try {
|
|
11705
|
-
if (
|
|
11706
|
-
const raw = JSON.parse(
|
|
12235
|
+
if (fs30.existsSync(credPath)) {
|
|
12236
|
+
const raw = JSON.parse(fs30.readFileSync(credPath, "utf-8"));
|
|
11707
12237
|
if (raw.apiKey) {
|
|
11708
12238
|
existingCreds = {
|
|
11709
12239
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
|
|
@@ -11715,13 +12245,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
11715
12245
|
} catch {
|
|
11716
12246
|
}
|
|
11717
12247
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
11718
|
-
|
|
12248
|
+
fs30.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
11719
12249
|
if (profileName === "default") {
|
|
11720
|
-
const configPath =
|
|
12250
|
+
const configPath = path33.join(os26.homedir(), ".node9", "config.json");
|
|
11721
12251
|
let config = {};
|
|
11722
12252
|
try {
|
|
11723
|
-
if (
|
|
11724
|
-
config = JSON.parse(
|
|
12253
|
+
if (fs30.existsSync(configPath))
|
|
12254
|
+
config = JSON.parse(fs30.readFileSync(configPath, "utf-8"));
|
|
11725
12255
|
} catch {
|
|
11726
12256
|
}
|
|
11727
12257
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -11736,19 +12266,19 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
11736
12266
|
approvers.cloud = false;
|
|
11737
12267
|
}
|
|
11738
12268
|
s.approvers = approvers;
|
|
11739
|
-
if (!
|
|
11740
|
-
|
|
11741
|
-
|
|
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 });
|
|
11742
12272
|
}
|
|
11743
12273
|
if (options.profile && profileName !== "default") {
|
|
11744
|
-
console.log(
|
|
11745
|
-
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`));
|
|
11746
12276
|
} else if (options.local) {
|
|
11747
|
-
console.log(
|
|
11748
|
-
console.log(
|
|
12277
|
+
console.log(chalk20.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
12278
|
+
console.log(chalk20.gray(` All decisions stay on this machine.`));
|
|
11749
12279
|
} else {
|
|
11750
|
-
console.log(
|
|
11751
|
-
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.`));
|
|
11752
12282
|
}
|
|
11753
12283
|
});
|
|
11754
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) => {
|
|
@@ -11756,19 +12286,19 @@ program.command("addto").description("Integrate Node9 with an AI agent").addHelp
|
|
|
11756
12286
|
if (target === "claude") return await setupClaude();
|
|
11757
12287
|
if (target === "cursor") return await setupCursor();
|
|
11758
12288
|
if (target === "hud") return setupHud();
|
|
11759
|
-
console.error(
|
|
12289
|
+
console.error(chalk20.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
|
|
11760
12290
|
process.exit(1);
|
|
11761
12291
|
});
|
|
11762
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) => {
|
|
11763
12293
|
if (!target) {
|
|
11764
|
-
console.log(
|
|
11765
|
-
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");
|
|
11766
12296
|
console.log(" Targets:");
|
|
11767
|
-
console.log(" " +
|
|
11768
|
-
console.log(" " +
|
|
11769
|
-
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)");
|
|
11770
12300
|
process.stdout.write(
|
|
11771
|
-
" " +
|
|
12301
|
+
" " + chalk20.green("hud") + " \u2014 Claude Code security statusline\n"
|
|
11772
12302
|
);
|
|
11773
12303
|
console.log("");
|
|
11774
12304
|
return;
|
|
@@ -11778,7 +12308,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
11778
12308
|
if (t === "claude") return await setupClaude();
|
|
11779
12309
|
if (t === "cursor") return await setupCursor();
|
|
11780
12310
|
if (t === "hud") return setupHud();
|
|
11781
|
-
console.error(
|
|
12311
|
+
console.error(chalk20.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
|
|
11782
12312
|
process.exit(1);
|
|
11783
12313
|
});
|
|
11784
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) => {
|
|
@@ -11789,31 +12319,31 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
|
|
|
11789
12319
|
else if (target === "hud") fn = teardownHud;
|
|
11790
12320
|
else {
|
|
11791
12321
|
console.error(
|
|
11792
|
-
|
|
12322
|
+
chalk20.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`)
|
|
11793
12323
|
);
|
|
11794
12324
|
process.exit(1);
|
|
11795
12325
|
}
|
|
11796
|
-
console.log(
|
|
12326
|
+
console.log(chalk20.cyan(`
|
|
11797
12327
|
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
11798
12328
|
`));
|
|
11799
12329
|
try {
|
|
11800
12330
|
fn();
|
|
11801
12331
|
} catch (err2) {
|
|
11802
|
-
console.error(
|
|
12332
|
+
console.error(chalk20.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
11803
12333
|
process.exit(1);
|
|
11804
12334
|
}
|
|
11805
|
-
console.log(
|
|
12335
|
+
console.log(chalk20.gray("\n Restart the agent for changes to take effect."));
|
|
11806
12336
|
});
|
|
11807
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) => {
|
|
11808
|
-
console.log(
|
|
11809
|
-
console.log(
|
|
12338
|
+
console.log(chalk20.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
12339
|
+
console.log(chalk20.bold("Stopping daemon..."));
|
|
11810
12340
|
try {
|
|
11811
12341
|
stopDaemon();
|
|
11812
|
-
console.log(
|
|
12342
|
+
console.log(chalk20.green(" \u2705 Daemon stopped"));
|
|
11813
12343
|
} catch {
|
|
11814
|
-
console.log(
|
|
12344
|
+
console.log(chalk20.blue(" \u2139\uFE0F Daemon was not running"));
|
|
11815
12345
|
}
|
|
11816
|
-
console.log(
|
|
12346
|
+
console.log(chalk20.bold("\nRemoving hooks..."));
|
|
11817
12347
|
let teardownFailed = false;
|
|
11818
12348
|
for (const [label, fn] of [
|
|
11819
12349
|
["Claude", teardownClaude],
|
|
@@ -11825,45 +12355,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
11825
12355
|
} catch (err2) {
|
|
11826
12356
|
teardownFailed = true;
|
|
11827
12357
|
console.error(
|
|
11828
|
-
|
|
12358
|
+
chalk20.red(
|
|
11829
12359
|
` \u26A0\uFE0F Failed to remove ${label} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
11830
12360
|
)
|
|
11831
12361
|
);
|
|
11832
12362
|
}
|
|
11833
12363
|
}
|
|
11834
12364
|
if (options.purge) {
|
|
11835
|
-
const node9Dir =
|
|
11836
|
-
if (
|
|
12365
|
+
const node9Dir = path33.join(os26.homedir(), ".node9");
|
|
12366
|
+
if (fs30.existsSync(node9Dir)) {
|
|
11837
12367
|
const confirmed = await confirm2({
|
|
11838
12368
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
11839
12369
|
default: false
|
|
11840
12370
|
});
|
|
11841
12371
|
if (confirmed) {
|
|
11842
|
-
|
|
11843
|
-
if (
|
|
12372
|
+
fs30.rmSync(node9Dir, { recursive: true });
|
|
12373
|
+
if (fs30.existsSync(node9Dir)) {
|
|
11844
12374
|
console.error(
|
|
11845
|
-
|
|
12375
|
+
chalk20.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
11846
12376
|
);
|
|
11847
12377
|
} else {
|
|
11848
|
-
console.log(
|
|
12378
|
+
console.log(chalk20.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
11849
12379
|
}
|
|
11850
12380
|
} else {
|
|
11851
|
-
console.log(
|
|
12381
|
+
console.log(chalk20.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
11852
12382
|
}
|
|
11853
12383
|
} else {
|
|
11854
|
-
console.log(
|
|
12384
|
+
console.log(chalk20.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
11855
12385
|
}
|
|
11856
12386
|
} else {
|
|
11857
12387
|
console.log(
|
|
11858
|
-
|
|
12388
|
+
chalk20.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
11859
12389
|
);
|
|
11860
12390
|
}
|
|
11861
12391
|
if (teardownFailed) {
|
|
11862
|
-
console.error(
|
|
12392
|
+
console.error(chalk20.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
11863
12393
|
process.exit(1);
|
|
11864
12394
|
}
|
|
11865
|
-
console.log(
|
|
11866
|
-
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"));
|
|
11867
12397
|
});
|
|
11868
12398
|
registerDoctorCommand(program, version);
|
|
11869
12399
|
program.command("explain").description(
|
|
@@ -11876,7 +12406,7 @@ program.command("explain").description(
|
|
|
11876
12406
|
try {
|
|
11877
12407
|
args = JSON.parse(trimmed);
|
|
11878
12408
|
} catch {
|
|
11879
|
-
console.error(
|
|
12409
|
+
console.error(chalk20.red(`
|
|
11880
12410
|
\u274C Invalid JSON: ${trimmed}
|
|
11881
12411
|
`));
|
|
11882
12412
|
process.exit(1);
|
|
@@ -11887,60 +12417,61 @@ program.command("explain").description(
|
|
|
11887
12417
|
}
|
|
11888
12418
|
const result = await explainPolicy(tool, args);
|
|
11889
12419
|
console.log("");
|
|
11890
|
-
console.log(
|
|
12420
|
+
console.log(chalk20.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
11891
12421
|
console.log("");
|
|
11892
|
-
console.log(` ${
|
|
12422
|
+
console.log(` ${chalk20.bold("Tool:")} ${chalk20.white(result.tool)}`);
|
|
11893
12423
|
if (argsRaw) {
|
|
11894
12424
|
const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
11895
|
-
console.log(` ${
|
|
12425
|
+
console.log(` ${chalk20.bold("Input:")} ${chalk20.gray(preview)}`);
|
|
11896
12426
|
}
|
|
11897
12427
|
console.log("");
|
|
11898
|
-
console.log(
|
|
12428
|
+
console.log(chalk20.bold("Config Sources (Waterfall):"));
|
|
11899
12429
|
for (const tier of result.waterfall) {
|
|
11900
|
-
const
|
|
12430
|
+
const num2 = chalk20.gray(` ${tier.tier}.`);
|
|
11901
12431
|
const label = tier.label.padEnd(16);
|
|
11902
12432
|
let statusStr;
|
|
11903
12433
|
if (tier.tier === 1) {
|
|
11904
|
-
statusStr =
|
|
12434
|
+
statusStr = chalk20.gray(tier.note ?? "");
|
|
11905
12435
|
} else if (tier.status === "active") {
|
|
11906
|
-
const loc = tier.path ?
|
|
11907
|
-
const note = tier.note ?
|
|
11908
|
-
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 : "");
|
|
11909
12439
|
} else {
|
|
11910
|
-
statusStr =
|
|
12440
|
+
statusStr = chalk20.gray("\u25CB " + (tier.note ?? "not found"));
|
|
11911
12441
|
}
|
|
11912
|
-
console.log(`${
|
|
12442
|
+
console.log(`${num2} ${chalk20.white(label)} ${statusStr}`);
|
|
11913
12443
|
}
|
|
11914
12444
|
console.log("");
|
|
11915
|
-
console.log(
|
|
12445
|
+
console.log(chalk20.bold("Policy Evaluation:"));
|
|
11916
12446
|
for (const step of result.steps) {
|
|
11917
12447
|
const isFinal = step.isFinal;
|
|
11918
12448
|
let icon;
|
|
11919
|
-
if (step.outcome === "allow") icon =
|
|
11920
|
-
else if (step.outcome === "review") icon =
|
|
11921
|
-
else if (step.outcome === "skip") icon =
|
|
11922
|
-
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 ");
|
|
11923
12453
|
const name = step.name.padEnd(18);
|
|
11924
|
-
const nameStr = isFinal ?
|
|
11925
|
-
const detail = isFinal ?
|
|
11926
|
-
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") : "";
|
|
11927
12457
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
11928
12458
|
}
|
|
11929
12459
|
console.log("");
|
|
11930
12460
|
if (result.decision === "allow") {
|
|
11931
|
-
console.log(
|
|
12461
|
+
console.log(chalk20.green.bold(" Decision: \u2705 ALLOW") + chalk20.gray(" \u2014 no approval needed"));
|
|
11932
12462
|
} else {
|
|
11933
12463
|
console.log(
|
|
11934
|
-
|
|
12464
|
+
chalk20.red.bold(" Decision: \u{1F534} REVIEW") + chalk20.gray(" \u2014 human approval required")
|
|
11935
12465
|
);
|
|
11936
12466
|
if (result.blockedByLabel) {
|
|
11937
|
-
console.log(
|
|
12467
|
+
console.log(chalk20.gray(` Reason: ${result.blockedByLabel}`));
|
|
11938
12468
|
}
|
|
11939
12469
|
}
|
|
11940
12470
|
console.log("");
|
|
11941
12471
|
});
|
|
11942
12472
|
registerInitCommand(program);
|
|
11943
12473
|
registerAuditCommand(program);
|
|
12474
|
+
registerReportCommand(program);
|
|
11944
12475
|
registerStatusCommand(program);
|
|
11945
12476
|
registerDaemonCommand(program);
|
|
11946
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) => {
|
|
@@ -11948,7 +12479,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
|
|
|
11948
12479
|
try {
|
|
11949
12480
|
await startTail2(options);
|
|
11950
12481
|
} catch (err2) {
|
|
11951
|
-
console.error(
|
|
12482
|
+
console.error(chalk20.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
11952
12483
|
process.exit(1);
|
|
11953
12484
|
}
|
|
11954
12485
|
});
|
|
@@ -11980,14 +12511,14 @@ Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
|
|
|
11980
12511
|
Run "node9 addto claude" to register it as the statusLine.`
|
|
11981
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) => {
|
|
11982
12513
|
if (subcommand === "debug") {
|
|
11983
|
-
const flagFile =
|
|
12514
|
+
const flagFile = path33.join(os26.homedir(), ".node9", "hud-debug");
|
|
11984
12515
|
if (state === "on") {
|
|
11985
|
-
|
|
11986
|
-
|
|
12516
|
+
fs30.mkdirSync(path33.dirname(flagFile), { recursive: true });
|
|
12517
|
+
fs30.writeFileSync(flagFile, "");
|
|
11987
12518
|
console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
|
|
11988
12519
|
console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
|
|
11989
12520
|
} else if (state === "off") {
|
|
11990
|
-
if (
|
|
12521
|
+
if (fs30.existsSync(flagFile)) fs30.unlinkSync(flagFile);
|
|
11991
12522
|
console.log("HUD debug logging disabled.");
|
|
11992
12523
|
} else {
|
|
11993
12524
|
console.error("Usage: node9 hud debug on|off");
|
|
@@ -12002,7 +12533,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
12002
12533
|
const ms = parseDuration(options.duration);
|
|
12003
12534
|
if (ms === null) {
|
|
12004
12535
|
console.error(
|
|
12005
|
-
|
|
12536
|
+
chalk20.red(`
|
|
12006
12537
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
12007
12538
|
`)
|
|
12008
12539
|
);
|
|
@@ -12010,20 +12541,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
12010
12541
|
}
|
|
12011
12542
|
pauseNode9(ms, options.duration);
|
|
12012
12543
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
12013
|
-
console.log(
|
|
12544
|
+
console.log(chalk20.yellow(`
|
|
12014
12545
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
12015
|
-
console.log(
|
|
12016
|
-
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.
|
|
12017
12548
|
`));
|
|
12018
12549
|
});
|
|
12019
12550
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
12020
12551
|
const { paused } = checkPause();
|
|
12021
12552
|
if (!paused) {
|
|
12022
|
-
console.log(
|
|
12553
|
+
console.log(chalk20.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
12023
12554
|
return;
|
|
12024
12555
|
}
|
|
12025
12556
|
resumeNode9();
|
|
12026
|
-
console.log(
|
|
12557
|
+
console.log(chalk20.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
12027
12558
|
});
|
|
12028
12559
|
var HOOK_BASED_AGENTS = {
|
|
12029
12560
|
claude: "claude",
|
|
@@ -12036,15 +12567,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
12036
12567
|
if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
|
|
12037
12568
|
const target = HOOK_BASED_AGENTS[firstArg2];
|
|
12038
12569
|
console.error(
|
|
12039
|
-
|
|
12570
|
+
chalk20.yellow(`
|
|
12040
12571
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
12041
12572
|
);
|
|
12042
|
-
console.error(
|
|
12573
|
+
console.error(chalk20.white(`
|
|
12043
12574
|
"${target}" uses its own hook system. Use:`));
|
|
12044
12575
|
console.error(
|
|
12045
|
-
|
|
12576
|
+
chalk20.green(` node9 addto ${target} `) + chalk20.gray("# one-time setup")
|
|
12046
12577
|
);
|
|
12047
|
-
console.error(
|
|
12578
|
+
console.error(chalk20.green(` ${target} `) + chalk20.gray("# run normally"));
|
|
12048
12579
|
process.exit(1);
|
|
12049
12580
|
}
|
|
12050
12581
|
const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
|
|
@@ -12061,7 +12592,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
12061
12592
|
}
|
|
12062
12593
|
);
|
|
12063
12594
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
12064
|
-
console.error(
|
|
12595
|
+
console.error(chalk20.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
12065
12596
|
const daemonReady = await autoStartDaemonAndWait();
|
|
12066
12597
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
12067
12598
|
}
|
|
@@ -12074,12 +12605,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
12074
12605
|
}
|
|
12075
12606
|
if (!result.approved) {
|
|
12076
12607
|
console.error(
|
|
12077
|
-
|
|
12608
|
+
chalk20.red(`
|
|
12078
12609
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
12079
12610
|
);
|
|
12080
12611
|
process.exit(1);
|
|
12081
12612
|
}
|
|
12082
|
-
console.error(
|
|
12613
|
+
console.error(chalk20.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
12083
12614
|
await runProxy(fullCommand);
|
|
12084
12615
|
} else {
|
|
12085
12616
|
program.help();
|
|
@@ -12094,9 +12625,9 @@ if (process.argv[2] !== "daemon") {
|
|
|
12094
12625
|
const isCheckHook = process.argv[2] === "check";
|
|
12095
12626
|
if (isCheckHook) {
|
|
12096
12627
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
12097
|
-
const logPath =
|
|
12628
|
+
const logPath = path33.join(os26.homedir(), ".node9", "hook-debug.log");
|
|
12098
12629
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
12099
|
-
|
|
12630
|
+
fs30.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
12100
12631
|
`);
|
|
12101
12632
|
}
|
|
12102
12633
|
process.exit(0);
|