@node9/proxy 1.4.0 → 1.5.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 +36 -7
- package/dist/cli.js +1110 -374
- package/dist/cli.mjs +1105 -368
- package/dist/index.js +72 -21
- package/dist/index.mjs +72 -21
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -114,8 +114,8 @@ function sanitizeConfig(raw) {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
const lines = result.error.issues.map((issue) => {
|
|
117
|
-
const
|
|
118
|
-
return ` \u2022 ${
|
|
117
|
+
const path27 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
118
|
+
return ` \u2022 ${path27}: ${issue.message}`;
|
|
119
119
|
});
|
|
120
120
|
return {
|
|
121
121
|
sanitized,
|
|
@@ -1623,17 +1623,44 @@ function readTrustedHosts() {
|
|
|
1623
1623
|
return [];
|
|
1624
1624
|
}
|
|
1625
1625
|
}
|
|
1626
|
+
function getFileMtime() {
|
|
1627
|
+
try {
|
|
1628
|
+
return import_fs6.default.statSync(getTrustedHostsPath()).mtimeMs;
|
|
1629
|
+
} catch {
|
|
1630
|
+
return 0;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
function getCachedHosts() {
|
|
1634
|
+
const now = Date.now();
|
|
1635
|
+
if (_cache && now < _cache.expiry) {
|
|
1636
|
+
const mtime = getFileMtime();
|
|
1637
|
+
if (mtime === _cache.mtime) return _cache.hosts;
|
|
1638
|
+
}
|
|
1639
|
+
const hosts = readTrustedHosts();
|
|
1640
|
+
_cache = { hosts, expiry: now + CACHE_TTL_MS, mtime: getFileMtime() };
|
|
1641
|
+
return hosts;
|
|
1642
|
+
}
|
|
1626
1643
|
function writeTrustedHosts(hosts) {
|
|
1627
1644
|
const filePath = getTrustedHostsPath();
|
|
1628
1645
|
import_fs6.default.mkdirSync(import_path7.default.dirname(filePath), { recursive: true });
|
|
1629
1646
|
const tmp = filePath + ".node9-tmp";
|
|
1630
|
-
import_fs6.default.writeFileSync(tmp, JSON.stringify({ hosts }, null, 2));
|
|
1647
|
+
import_fs6.default.writeFileSync(tmp, JSON.stringify({ hosts }, null, 2), { mode: 384 });
|
|
1631
1648
|
import_fs6.default.renameSync(tmp, filePath);
|
|
1649
|
+
_cache = { hosts, expiry: Date.now() + CACHE_TTL_MS, mtime: getFileMtime() };
|
|
1632
1650
|
}
|
|
1633
1651
|
function addTrustedHost(host) {
|
|
1652
|
+
const normalized = normalizeHost(host);
|
|
1653
|
+
if (normalized.startsWith("*.")) {
|
|
1654
|
+
const base = normalized.slice(2);
|
|
1655
|
+
if (!base.includes(".")) {
|
|
1656
|
+
throw new Error(
|
|
1657
|
+
`Wildcard pattern '${normalized}' is too broad \u2014 the base domain must have at least one dot (e.g. '*.mycompany.com', not '*.com').`
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1634
1661
|
const hosts = readTrustedHosts();
|
|
1635
|
-
if (hosts.some((h) => h.host ===
|
|
1636
|
-
hosts.push({ host, addedAt: Date.now(), addedBy: "user" });
|
|
1662
|
+
if (hosts.some((h) => h.host === normalized)) return;
|
|
1663
|
+
hosts.push({ host: normalized, addedAt: Date.now(), addedBy: "user" });
|
|
1637
1664
|
writeTrustedHosts(hosts);
|
|
1638
1665
|
}
|
|
1639
1666
|
function removeTrustedHost(host) {
|
|
@@ -1648,22 +1675,24 @@ function normalizeHost(raw) {
|
|
|
1648
1675
|
}
|
|
1649
1676
|
function isTrustedHost(host) {
|
|
1650
1677
|
const normalized = normalizeHost(host);
|
|
1651
|
-
return
|
|
1678
|
+
return getCachedHosts().some((entry) => {
|
|
1652
1679
|
const entryHost = entry.host.toLowerCase();
|
|
1653
1680
|
if (entryHost.startsWith("*.")) {
|
|
1654
1681
|
const domain = entryHost.slice(2);
|
|
1655
|
-
return normalized
|
|
1682
|
+
return normalized.endsWith("." + domain);
|
|
1656
1683
|
}
|
|
1657
1684
|
return normalized === entryHost;
|
|
1658
1685
|
});
|
|
1659
1686
|
}
|
|
1660
|
-
var import_fs6, import_path7, import_os5;
|
|
1687
|
+
var import_fs6, import_path7, import_os5, _cache, CACHE_TTL_MS;
|
|
1661
1688
|
var init_trusted_hosts = __esm({
|
|
1662
1689
|
"src/auth/trusted-hosts.ts"() {
|
|
1663
1690
|
"use strict";
|
|
1664
1691
|
import_fs6 = __toESM(require("fs"));
|
|
1665
1692
|
import_path7 = __toESM(require("path"));
|
|
1666
1693
|
import_os5 = __toESM(require("os"));
|
|
1694
|
+
_cache = null;
|
|
1695
|
+
CACHE_TTL_MS = 5e3;
|
|
1667
1696
|
}
|
|
1668
1697
|
});
|
|
1669
1698
|
|
|
@@ -1681,9 +1710,9 @@ function matchesPattern(text, patterns) {
|
|
|
1681
1710
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1682
1711
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1683
1712
|
}
|
|
1684
|
-
function getNestedValue(obj,
|
|
1713
|
+
function getNestedValue(obj, path27) {
|
|
1685
1714
|
if (!obj || typeof obj !== "object") return null;
|
|
1686
|
-
return
|
|
1715
|
+
return path27.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1687
1716
|
}
|
|
1688
1717
|
function shouldSnapshot(toolName, args, config) {
|
|
1689
1718
|
if (!config.settings.enableUndo) return false;
|
|
@@ -1869,7 +1898,12 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1869
1898
|
};
|
|
1870
1899
|
}
|
|
1871
1900
|
if (allTrusted) {
|
|
1872
|
-
return {
|
|
1901
|
+
return {
|
|
1902
|
+
decision: "allow",
|
|
1903
|
+
blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
|
|
1904
|
+
reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
|
|
1905
|
+
tier: 3
|
|
1906
|
+
};
|
|
1873
1907
|
}
|
|
1874
1908
|
return {
|
|
1875
1909
|
decision: "review",
|
|
@@ -2411,8 +2445,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
2411
2445
|
signal: ctrl.signal
|
|
2412
2446
|
});
|
|
2413
2447
|
if (!res.ok) throw new Error("Daemon fail");
|
|
2414
|
-
const { id } = await res.json();
|
|
2415
|
-
return id;
|
|
2448
|
+
const { id, allowCount } = await res.json();
|
|
2449
|
+
return { id, allowCount: allowCount ?? 1 };
|
|
2416
2450
|
} finally {
|
|
2417
2451
|
clearTimeout(timer);
|
|
2418
2452
|
}
|
|
@@ -2451,15 +2485,15 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
|
|
|
2451
2485
|
signal: AbortSignal.timeout(3e3)
|
|
2452
2486
|
});
|
|
2453
2487
|
if (!res.ok) throw new Error("Daemon unreachable");
|
|
2454
|
-
const { id } = await res.json();
|
|
2455
|
-
return id;
|
|
2488
|
+
const { id, allowCount } = await res.json();
|
|
2489
|
+
return { id, allowCount: allowCount ?? 1 };
|
|
2456
2490
|
}
|
|
2457
|
-
async function resolveViaDaemon(id, decision, internalToken) {
|
|
2491
|
+
async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
2458
2492
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2459
2493
|
await fetch(`${base}/resolve/${id}`, {
|
|
2460
2494
|
method: "POST",
|
|
2461
2495
|
headers: { "Content-Type": "application/json", "X-Node9-Internal": internalToken },
|
|
2462
|
-
body: JSON.stringify({ decision }),
|
|
2496
|
+
body: JSON.stringify({ decision, ...source && { source } }),
|
|
2463
2497
|
signal: AbortSignal.timeout(3e3)
|
|
2464
2498
|
});
|
|
2465
2499
|
}
|
|
@@ -2666,20 +2700,24 @@ ${smartTruncate(str, 500)}`
|
|
|
2666
2700
|
function escapePango(text) {
|
|
2667
2701
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2668
2702
|
}
|
|
2669
|
-
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
2703
|
+
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
|
|
2670
2704
|
const lines = [];
|
|
2671
2705
|
if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
|
|
2672
2706
|
lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
|
|
2673
2707
|
lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
|
|
2674
2708
|
lines.push("");
|
|
2675
2709
|
lines.push(formattedArgs);
|
|
2710
|
+
if (allowCount >= 3) {
|
|
2711
|
+
lines.push("");
|
|
2712
|
+
lines.push(`\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule`);
|
|
2713
|
+
}
|
|
2676
2714
|
if (!locked) {
|
|
2677
2715
|
lines.push("");
|
|
2678
2716
|
lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
|
|
2679
2717
|
}
|
|
2680
2718
|
return lines.join("\n");
|
|
2681
2719
|
}
|
|
2682
|
-
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
2720
|
+
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
|
|
2683
2721
|
const lines = [];
|
|
2684
2722
|
if (locked) {
|
|
2685
2723
|
lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
|
|
@@ -2691,6 +2729,12 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
2691
2729
|
lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
|
|
2692
2730
|
lines.push("");
|
|
2693
2731
|
lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
|
|
2732
|
+
if (allowCount >= 3) {
|
|
2733
|
+
lines.push("");
|
|
2734
|
+
lines.push(
|
|
2735
|
+
`<span foreground="#f0c040">\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</span>`
|
|
2736
|
+
);
|
|
2737
|
+
}
|
|
2694
2738
|
if (!locked) {
|
|
2695
2739
|
lines.push("");
|
|
2696
2740
|
lines.push(
|
|
@@ -2699,12 +2743,19 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
2699
2743
|
}
|
|
2700
2744
|
return lines.join("\n");
|
|
2701
2745
|
}
|
|
2702
|
-
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord) {
|
|
2746
|
+
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1) {
|
|
2703
2747
|
if (isTestEnv()) return "deny";
|
|
2704
2748
|
const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
|
|
2705
2749
|
const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
|
|
2706
2750
|
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
|
|
2707
|
-
const message = buildPlainMessage(
|
|
2751
|
+
const message = buildPlainMessage(
|
|
2752
|
+
toolName,
|
|
2753
|
+
formattedArgs,
|
|
2754
|
+
agent,
|
|
2755
|
+
explainableLabel,
|
|
2756
|
+
locked,
|
|
2757
|
+
allowCount
|
|
2758
|
+
);
|
|
2708
2759
|
return new Promise((resolve) => {
|
|
2709
2760
|
let childProcess = null;
|
|
2710
2761
|
const onAbort = () => {
|
|
@@ -2736,7 +2787,8 @@ end run`;
|
|
|
2736
2787
|
formattedArgs,
|
|
2737
2788
|
agent,
|
|
2738
2789
|
explainableLabel,
|
|
2739
|
-
locked
|
|
2790
|
+
locked,
|
|
2791
|
+
allowCount
|
|
2740
2792
|
);
|
|
2741
2793
|
const argsList = [
|
|
2742
2794
|
locked ? "--info" : "--question",
|
|
@@ -3099,13 +3151,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3099
3151
|
let viewerId = null;
|
|
3100
3152
|
const internalToken = getInternalToken();
|
|
3101
3153
|
let daemonEntryId = null;
|
|
3154
|
+
let daemonAllowCount = 1;
|
|
3102
3155
|
if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
|
|
3103
3156
|
if (cloudEnforced && cloudRequestId) {
|
|
3104
|
-
|
|
3157
|
+
const viewer = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
|
|
3158
|
+
viewerId = viewer?.id ?? null;
|
|
3105
3159
|
daemonEntryId = viewerId;
|
|
3160
|
+
if (viewer) daemonAllowCount = viewer.allowCount;
|
|
3106
3161
|
} else {
|
|
3107
3162
|
try {
|
|
3108
|
-
|
|
3163
|
+
const entry = await registerDaemonEntry(
|
|
3109
3164
|
toolName,
|
|
3110
3165
|
args,
|
|
3111
3166
|
meta,
|
|
@@ -3113,6 +3168,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3113
3168
|
options?.activityId,
|
|
3114
3169
|
options?.cwd
|
|
3115
3170
|
);
|
|
3171
|
+
daemonEntryId = entry.id;
|
|
3172
|
+
daemonAllowCount = entry.allowCount;
|
|
3116
3173
|
} catch {
|
|
3117
3174
|
}
|
|
3118
3175
|
}
|
|
@@ -3148,7 +3205,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3148
3205
|
false,
|
|
3149
3206
|
signal,
|
|
3150
3207
|
policyMatchedField,
|
|
3151
|
-
policyMatchedWord
|
|
3208
|
+
policyMatchedWord,
|
|
3209
|
+
daemonAllowCount
|
|
3152
3210
|
);
|
|
3153
3211
|
if (decision === "always_allow") {
|
|
3154
3212
|
writeTrustSession(toolName, 36e5);
|
|
@@ -3206,10 +3264,13 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
3206
3264
|
if (!resolved) {
|
|
3207
3265
|
resolved = true;
|
|
3208
3266
|
abortController.abort();
|
|
3209
|
-
if (
|
|
3210
|
-
resolveViaDaemon(
|
|
3211
|
-
|
|
3212
|
-
|
|
3267
|
+
if (daemonEntryId && internalToken) {
|
|
3268
|
+
resolveViaDaemon(
|
|
3269
|
+
daemonEntryId,
|
|
3270
|
+
res.approved ? "allow" : "deny",
|
|
3271
|
+
internalToken,
|
|
3272
|
+
res.decisionSource
|
|
3273
|
+
).catch(() => null);
|
|
3213
3274
|
}
|
|
3214
3275
|
resolve(res);
|
|
3215
3276
|
}
|
|
@@ -3566,6 +3627,15 @@ var init_ui = __esm({
|
|
|
3566
3627
|
padding: 5px 10px;
|
|
3567
3628
|
margin-bottom: 14px;
|
|
3568
3629
|
}
|
|
3630
|
+
.insight-hint {
|
|
3631
|
+
font-size: 12px;
|
|
3632
|
+
color: #f0c040;
|
|
3633
|
+
background: rgba(240, 192, 64, 0.08);
|
|
3634
|
+
border: 1px solid rgba(240, 192, 64, 0.25);
|
|
3635
|
+
border-radius: 6px;
|
|
3636
|
+
padding: 6px 10px;
|
|
3637
|
+
margin-bottom: 12px;
|
|
3638
|
+
}
|
|
3569
3639
|
pre {
|
|
3570
3640
|
background: #0d1117;
|
|
3571
3641
|
padding: 14px 16px;
|
|
@@ -4038,6 +4108,78 @@ var init_ui = __esm({
|
|
|
4038
4108
|
color: var(--danger);
|
|
4039
4109
|
}
|
|
4040
4110
|
|
|
4111
|
+
/* \u2500\u2500 Suggestion cards \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 */
|
|
4112
|
+
.suggestion-card {
|
|
4113
|
+
background: rgba(82, 130, 255, 0.06);
|
|
4114
|
+
border: 1px solid rgba(82, 130, 255, 0.25);
|
|
4115
|
+
border-radius: 8px;
|
|
4116
|
+
padding: 10px 12px;
|
|
4117
|
+
margin-bottom: 8px;
|
|
4118
|
+
}
|
|
4119
|
+
.suggestion-card:last-child {
|
|
4120
|
+
margin-bottom: 0;
|
|
4121
|
+
}
|
|
4122
|
+
.suggestion-header {
|
|
4123
|
+
display: flex;
|
|
4124
|
+
align-items: center;
|
|
4125
|
+
gap: 8px;
|
|
4126
|
+
margin-bottom: 6px;
|
|
4127
|
+
}
|
|
4128
|
+
.suggestion-tool {
|
|
4129
|
+
font-family: 'Fira Code', monospace;
|
|
4130
|
+
font-size: 11px;
|
|
4131
|
+
color: var(--text-bright);
|
|
4132
|
+
flex: 1;
|
|
4133
|
+
word-break: break-all;
|
|
4134
|
+
}
|
|
4135
|
+
.suggestion-count {
|
|
4136
|
+
font-size: 10px;
|
|
4137
|
+
color: var(--muted);
|
|
4138
|
+
white-space: nowrap;
|
|
4139
|
+
}
|
|
4140
|
+
.suggestion-rule {
|
|
4141
|
+
font-family: 'Fira Code', monospace;
|
|
4142
|
+
font-size: 10px;
|
|
4143
|
+
color: #79c0ff;
|
|
4144
|
+
background: rgba(0, 0, 0, 0.25);
|
|
4145
|
+
border-radius: 4px;
|
|
4146
|
+
padding: 4px 8px;
|
|
4147
|
+
margin-bottom: 8px;
|
|
4148
|
+
word-break: break-all;
|
|
4149
|
+
white-space: pre-wrap;
|
|
4150
|
+
}
|
|
4151
|
+
.suggestion-actions {
|
|
4152
|
+
display: flex;
|
|
4153
|
+
gap: 6px;
|
|
4154
|
+
}
|
|
4155
|
+
.btn-apply {
|
|
4156
|
+
background: rgba(52, 125, 57, 0.2);
|
|
4157
|
+
border: 1px solid rgba(87, 171, 90, 0.4);
|
|
4158
|
+
color: #57ab5a;
|
|
4159
|
+
padding: 4px 10px;
|
|
4160
|
+
font-size: 11px;
|
|
4161
|
+
border-radius: 5px;
|
|
4162
|
+
font-family: inherit;
|
|
4163
|
+
cursor: pointer;
|
|
4164
|
+
}
|
|
4165
|
+
.btn-apply:hover {
|
|
4166
|
+
background: rgba(52, 125, 57, 0.35);
|
|
4167
|
+
}
|
|
4168
|
+
.btn-dismiss-suggestion {
|
|
4169
|
+
background: transparent;
|
|
4170
|
+
border: 1px solid var(--border);
|
|
4171
|
+
color: var(--muted);
|
|
4172
|
+
padding: 4px 10px;
|
|
4173
|
+
font-size: 11px;
|
|
4174
|
+
border-radius: 5px;
|
|
4175
|
+
font-family: inherit;
|
|
4176
|
+
cursor: pointer;
|
|
4177
|
+
}
|
|
4178
|
+
.btn-dismiss-suggestion:hover {
|
|
4179
|
+
border-color: var(--danger);
|
|
4180
|
+
color: var(--danger);
|
|
4181
|
+
}
|
|
4182
|
+
|
|
4041
4183
|
.modal-overlay {
|
|
4042
4184
|
display: none;
|
|
4043
4185
|
position: fixed;
|
|
@@ -4219,6 +4361,11 @@ var init_ui = __esm({
|
|
|
4219
4361
|
<div class="panel-title">\u{1F4CB} Persistent Decisions</div>
|
|
4220
4362
|
<div id="decisionsList"><span class="decisions-empty">None yet.</span></div>
|
|
4221
4363
|
</div>
|
|
4364
|
+
|
|
4365
|
+
<div class="panel" id="suggestionsPanel" style="display: none">
|
|
4366
|
+
<div class="panel-title">\u{1F4A1} Smart Rule Suggestions</div>
|
|
4367
|
+
<div id="suggestionsList"></div>
|
|
4368
|
+
</div>
|
|
4222
4369
|
</div>
|
|
4223
4370
|
</div>
|
|
4224
4371
|
</div>
|
|
@@ -4408,6 +4555,7 @@ var init_ui = __esm({
|
|
|
4408
4555
|
</div>
|
|
4409
4556
|
<div class="tool-chip">\${esc(req.toolName)}</div>
|
|
4410
4557
|
\${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Cloud approval \u2014 view only</div>' : ''}
|
|
4558
|
+
\${req.allowCount >= 3 ? \`<div class="insight-hint">\u{1F4A1} Approved \${req.allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</div>\` : ''}
|
|
4411
4559
|
\${renderPayload(req)}
|
|
4412
4560
|
<div class="actions" id="act-\${req.id}">
|
|
4413
4561
|
<button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>\u2705 Allow this Action</button>
|
|
@@ -4474,6 +4622,14 @@ var init_ui = __esm({
|
|
|
4474
4622
|
ev.addEventListener('shields-status', (e) => {
|
|
4475
4623
|
renderShields(JSON.parse(e.data).shields);
|
|
4476
4624
|
});
|
|
4625
|
+
ev.addEventListener('suggestion:new', (e) => {
|
|
4626
|
+
const s = JSON.parse(e.data);
|
|
4627
|
+
addSuggestionCard(s);
|
|
4628
|
+
});
|
|
4629
|
+
ev.addEventListener('suggestion:resolved', (e) => {
|
|
4630
|
+
const { id } = JSON.parse(e.data);
|
|
4631
|
+
removeSuggestionCard(id);
|
|
4632
|
+
});
|
|
4477
4633
|
|
|
4478
4634
|
// \u2500\u2500 Flight Recorder \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
|
|
4479
4635
|
ev.addEventListener('activity', (e) => {
|
|
@@ -4723,6 +4879,74 @@ var init_ui = __esm({
|
|
|
4723
4879
|
.then((r) => r.json())
|
|
4724
4880
|
.then(renderDecisions)
|
|
4725
4881
|
.catch(() => {});
|
|
4882
|
+
|
|
4883
|
+
// \u2500\u2500 Smart Rule Suggestions \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
|
|
4884
|
+
function rulePreview(suggestion) {
|
|
4885
|
+
const r = suggestion.suggestedRule;
|
|
4886
|
+
if (r.type === 'ignoredTool') return \`ignoredTool: "\${r.toolName}"\`;
|
|
4887
|
+
const cond = r.rule.conditions?.[0];
|
|
4888
|
+
const condStr = cond ? \` where \${cond.field} \${cond.op} "\${cond.value}"\` : '';
|
|
4889
|
+
return \`allow \${r.rule.tool}\${condStr}\`;
|
|
4890
|
+
}
|
|
4891
|
+
|
|
4892
|
+
function addSuggestionCard(s) {
|
|
4893
|
+
const panel = document.getElementById('suggestionsPanel');
|
|
4894
|
+
const list = document.getElementById('suggestionsList');
|
|
4895
|
+
panel.style.display = '';
|
|
4896
|
+
|
|
4897
|
+
const card = document.createElement('div');
|
|
4898
|
+
card.className = 'suggestion-card';
|
|
4899
|
+
card.id = 'sg-' + s.id;
|
|
4900
|
+
card.innerHTML = \`
|
|
4901
|
+
<div class="suggestion-header">
|
|
4902
|
+
<span class="suggestion-tool">\${esc(s.toolName)}</span>
|
|
4903
|
+
<span class="suggestion-count">allowed \${s.allowCount}\xD7</span>
|
|
4904
|
+
</div>
|
|
4905
|
+
<div class="suggestion-rule">\${esc(rulePreview(s))}</div>
|
|
4906
|
+
<div class="suggestion-actions">
|
|
4907
|
+
<button class="btn-apply" onclick="applySuggestion('\${esc(s.id)}')">Apply rule</button>
|
|
4908
|
+
<button class="btn-dismiss-suggestion" onclick="dismissSuggestion('\${esc(s.id)}')">Dismiss</button>
|
|
4909
|
+
</div>
|
|
4910
|
+
\`;
|
|
4911
|
+
list.appendChild(card);
|
|
4912
|
+
}
|
|
4913
|
+
|
|
4914
|
+
function removeSuggestionCard(id) {
|
|
4915
|
+
document.getElementById('sg-' + id)?.remove();
|
|
4916
|
+
const list = document.getElementById('suggestionsList');
|
|
4917
|
+
if (!list.querySelector('.suggestion-card')) {
|
|
4918
|
+
document.getElementById('suggestionsPanel').style.display = 'none';
|
|
4919
|
+
}
|
|
4920
|
+
}
|
|
4921
|
+
|
|
4922
|
+
function applySuggestion(id) {
|
|
4923
|
+
fetch('/suggestions/' + id + '/apply', {
|
|
4924
|
+
method: 'POST',
|
|
4925
|
+
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
4926
|
+
body: JSON.stringify({}),
|
|
4927
|
+
})
|
|
4928
|
+
.then((r) => {
|
|
4929
|
+
if (r.ok) removeSuggestionCard(id);
|
|
4930
|
+
})
|
|
4931
|
+
.catch(() => {});
|
|
4932
|
+
}
|
|
4933
|
+
|
|
4934
|
+
function dismissSuggestion(id) {
|
|
4935
|
+
fetch('/suggestions/' + id + '/dismiss', {
|
|
4936
|
+
method: 'POST',
|
|
4937
|
+
headers: { 'X-Node9-Token': CSRF_TOKEN },
|
|
4938
|
+
})
|
|
4939
|
+
.then((r) => {
|
|
4940
|
+
if (r.ok) removeSuggestionCard(id);
|
|
4941
|
+
})
|
|
4942
|
+
.catch(() => {});
|
|
4943
|
+
}
|
|
4944
|
+
|
|
4945
|
+
// Load any suggestions that survived a page reload (daemon still running)
|
|
4946
|
+
fetch('/suggestions')
|
|
4947
|
+
.then((r) => r.json())
|
|
4948
|
+
.then((list) => list.filter((s) => s.status === 'pending').forEach(addSuggestionCard))
|
|
4949
|
+
.catch(() => {});
|
|
4726
4950
|
</script>
|
|
4727
4951
|
</body>
|
|
4728
4952
|
</html>
|
|
@@ -4740,7 +4964,117 @@ var init_ui2 = __esm({
|
|
|
4740
4964
|
}
|
|
4741
4965
|
});
|
|
4742
4966
|
|
|
4967
|
+
// src/daemon/suggestion-tracker.ts
|
|
4968
|
+
function extractPath(args) {
|
|
4969
|
+
if (!args || typeof args !== "object") return null;
|
|
4970
|
+
const a = args;
|
|
4971
|
+
for (const key of ["path", "file_path", "filename", "filepath", "dest", "destination"]) {
|
|
4972
|
+
if (typeof a[key] === "string" && a[key]) return a[key];
|
|
4973
|
+
}
|
|
4974
|
+
return null;
|
|
4975
|
+
}
|
|
4976
|
+
function commonPathPrefix(paths) {
|
|
4977
|
+
if (paths.length < 2) return null;
|
|
4978
|
+
const dirParts = paths.map((p) => {
|
|
4979
|
+
const lastSlash = p.lastIndexOf("/");
|
|
4980
|
+
return lastSlash > 0 ? p.slice(0, lastSlash + 1) : "/";
|
|
4981
|
+
});
|
|
4982
|
+
const first = dirParts[0].split("/");
|
|
4983
|
+
const common = [];
|
|
4984
|
+
for (let i = 0; i < first.length; i++) {
|
|
4985
|
+
if (dirParts.every((d) => d.split("/")[i] === first[i])) {
|
|
4986
|
+
common.push(first[i]);
|
|
4987
|
+
} else {
|
|
4988
|
+
break;
|
|
4989
|
+
}
|
|
4990
|
+
}
|
|
4991
|
+
const prefix = common.join("/").replace(/\/?$/, "/");
|
|
4992
|
+
return prefix.length > 1 ? prefix : null;
|
|
4993
|
+
}
|
|
4994
|
+
var import_crypto3, SuggestionTracker;
|
|
4995
|
+
var init_suggestion_tracker = __esm({
|
|
4996
|
+
"src/daemon/suggestion-tracker.ts"() {
|
|
4997
|
+
"use strict";
|
|
4998
|
+
import_crypto3 = require("crypto");
|
|
4999
|
+
SuggestionTracker = class {
|
|
5000
|
+
events = /* @__PURE__ */ new Map();
|
|
5001
|
+
threshold;
|
|
5002
|
+
constructor(threshold = 3) {
|
|
5003
|
+
this.threshold = threshold;
|
|
5004
|
+
}
|
|
5005
|
+
/**
|
|
5006
|
+
* Record a human-allowed review for a tool.
|
|
5007
|
+
* Returns a Suggestion when the threshold is reached, null otherwise.
|
|
5008
|
+
*/
|
|
5009
|
+
recordAllow(toolName, args) {
|
|
5010
|
+
const events = this.events.get(toolName) ?? [];
|
|
5011
|
+
events.push({ args, ts: Date.now() });
|
|
5012
|
+
this.events.set(toolName, events);
|
|
5013
|
+
if (events.length >= this.threshold) {
|
|
5014
|
+
this.events.delete(toolName);
|
|
5015
|
+
return this.generateSuggestion(toolName, events);
|
|
5016
|
+
}
|
|
5017
|
+
return null;
|
|
5018
|
+
}
|
|
5019
|
+
/**
|
|
5020
|
+
* Reset the counter for a tool (e.g. when the user clicks Deny —
|
|
5021
|
+
* don't suggest allowing something they just blocked).
|
|
5022
|
+
*/
|
|
5023
|
+
resetTool(toolName) {
|
|
5024
|
+
this.events.delete(toolName);
|
|
5025
|
+
}
|
|
5026
|
+
/** Current allow count for a tool (for tests). */
|
|
5027
|
+
getCount(toolName) {
|
|
5028
|
+
return this.events.get(toolName)?.length ?? 0;
|
|
5029
|
+
}
|
|
5030
|
+
generateSuggestion(toolName, events) {
|
|
5031
|
+
const paths = events.map((e) => extractPath(e.args)).filter((p) => typeof p === "string" && p.length > 0);
|
|
5032
|
+
const prefix = commonPathPrefix(paths);
|
|
5033
|
+
const suggestedRule = prefix ? {
|
|
5034
|
+
type: "smartRule",
|
|
5035
|
+
rule: {
|
|
5036
|
+
name: `allow-${toolName}-${prefix.replace(/[^a-z0-9]/gi, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}`,
|
|
5037
|
+
tool: toolName,
|
|
5038
|
+
conditions: [{ field: "path", op: "matchesGlob", value: `${prefix}**` }],
|
|
5039
|
+
verdict: "allow",
|
|
5040
|
+
reason: `Auto-suggested: ${toolName} allowed ${events.length}\xD7 in ${prefix}`
|
|
5041
|
+
}
|
|
5042
|
+
} : { type: "ignoredTool", toolName };
|
|
5043
|
+
return {
|
|
5044
|
+
id: (0, import_crypto3.randomUUID)(),
|
|
5045
|
+
toolName,
|
|
5046
|
+
allowCount: events.length,
|
|
5047
|
+
suggestedRule,
|
|
5048
|
+
status: "pending",
|
|
5049
|
+
createdAt: Date.now(),
|
|
5050
|
+
exampleArgs: events.slice(0, 3).map((e) => e.args)
|
|
5051
|
+
};
|
|
5052
|
+
}
|
|
5053
|
+
};
|
|
5054
|
+
}
|
|
5055
|
+
});
|
|
5056
|
+
|
|
4743
5057
|
// src/daemon/state.ts
|
|
5058
|
+
function loadInsightCounts() {
|
|
5059
|
+
try {
|
|
5060
|
+
if (!import_fs12.default.existsSync(INSIGHT_COUNTS_FILE)) return;
|
|
5061
|
+
const data = JSON.parse(import_fs12.default.readFileSync(INSIGHT_COUNTS_FILE, "utf-8"));
|
|
5062
|
+
for (const [tool, count] of Object.entries(data)) {
|
|
5063
|
+
if (typeof count === "number" && count > 0) insightCounts.set(tool, count);
|
|
5064
|
+
}
|
|
5065
|
+
} catch {
|
|
5066
|
+
}
|
|
5067
|
+
}
|
|
5068
|
+
function saveInsightCounts() {
|
|
5069
|
+
try {
|
|
5070
|
+
const data = {};
|
|
5071
|
+
insightCounts.forEach((count, tool) => {
|
|
5072
|
+
data[tool] = count;
|
|
5073
|
+
});
|
|
5074
|
+
atomicWriteSync2(INSIGHT_COUNTS_FILE, JSON.stringify(data, null, 2), { mode: 384 });
|
|
5075
|
+
} catch {
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
4744
5078
|
function getAbandonTimer() {
|
|
4745
5079
|
return _abandonTimer;
|
|
4746
5080
|
}
|
|
@@ -4765,9 +5099,25 @@ function markRejectionHandlerRegistered() {
|
|
|
4765
5099
|
function atomicWriteSync2(filePath, data, options) {
|
|
4766
5100
|
const dir = import_path15.default.dirname(filePath);
|
|
4767
5101
|
if (!import_fs12.default.existsSync(dir)) import_fs12.default.mkdirSync(dir, { recursive: true });
|
|
4768
|
-
const tmpPath = `${filePath}.${(0,
|
|
4769
|
-
|
|
4770
|
-
|
|
5102
|
+
const tmpPath = `${filePath}.${(0, import_crypto4.randomUUID)()}.tmp`;
|
|
5103
|
+
try {
|
|
5104
|
+
import_fs12.default.writeFileSync(tmpPath, data, options);
|
|
5105
|
+
} catch (err) {
|
|
5106
|
+
try {
|
|
5107
|
+
import_fs12.default.unlinkSync(tmpPath);
|
|
5108
|
+
} catch {
|
|
5109
|
+
}
|
|
5110
|
+
throw err;
|
|
5111
|
+
}
|
|
5112
|
+
try {
|
|
5113
|
+
import_fs12.default.renameSync(tmpPath, filePath);
|
|
5114
|
+
} catch (err) {
|
|
5115
|
+
try {
|
|
5116
|
+
import_fs12.default.unlinkSync(tmpPath);
|
|
5117
|
+
} catch {
|
|
5118
|
+
}
|
|
5119
|
+
throw err;
|
|
5120
|
+
}
|
|
4771
5121
|
}
|
|
4772
5122
|
function redactArgs(value) {
|
|
4773
5123
|
if (!value || typeof value !== "object") return value;
|
|
@@ -4965,7 +5315,7 @@ function startActivitySocket() {
|
|
|
4965
5315
|
}
|
|
4966
5316
|
});
|
|
4967
5317
|
}
|
|
4968
|
-
var import_net2, import_fs12, import_path15, import_os12, import_child_process3,
|
|
5318
|
+
var import_net2, import_fs12, import_path15, import_os12, import_child_process3, import_crypto4, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE;
|
|
4969
5319
|
var init_state2 = __esm({
|
|
4970
5320
|
"src/daemon/state.ts"() {
|
|
4971
5321
|
"use strict";
|
|
@@ -4974,8 +5324,9 @@ var init_state2 = __esm({
|
|
|
4974
5324
|
import_path15 = __toESM(require("path"));
|
|
4975
5325
|
import_os12 = __toESM(require("os"));
|
|
4976
5326
|
import_child_process3 = require("child_process");
|
|
4977
|
-
|
|
5327
|
+
import_crypto4 = require("crypto");
|
|
4978
5328
|
init_daemon();
|
|
5329
|
+
init_suggestion_tracker();
|
|
4979
5330
|
homeDir = import_os12.default.homedir();
|
|
4980
5331
|
DAEMON_PID_FILE = import_path15.default.join(homeDir, ".node9", "daemon.pid");
|
|
4981
5332
|
DECISIONS_FILE = import_path15.default.join(homeDir, ".node9", "decisions.json");
|
|
@@ -4983,8 +5334,12 @@ var init_state2 = __esm({
|
|
|
4983
5334
|
TRUST_FILE2 = import_path15.default.join(homeDir, ".node9", "trust.json");
|
|
4984
5335
|
GLOBAL_CONFIG_FILE = import_path15.default.join(homeDir, ".node9", "config.json");
|
|
4985
5336
|
CREDENTIALS_FILE = import_path15.default.join(homeDir, ".node9", "credentials.json");
|
|
5337
|
+
INSIGHT_COUNTS_FILE = import_path15.default.join(homeDir, ".node9", "insight-counts.json");
|
|
4986
5338
|
pending = /* @__PURE__ */ new Map();
|
|
4987
5339
|
sseClients = /* @__PURE__ */ new Set();
|
|
5340
|
+
suggestionTracker = new SuggestionTracker(3);
|
|
5341
|
+
suggestions = /* @__PURE__ */ new Map();
|
|
5342
|
+
insightCounts = /* @__PURE__ */ new Map();
|
|
4988
5343
|
_abandonTimer = null;
|
|
4989
5344
|
_hadBrowserClient = false;
|
|
4990
5345
|
_daemonServer = null;
|
|
@@ -5003,10 +5358,68 @@ var init_state2 = __esm({
|
|
|
5003
5358
|
}
|
|
5004
5359
|
});
|
|
5005
5360
|
|
|
5361
|
+
// src/config/patch.ts
|
|
5362
|
+
function patchConfig(configPath, patch) {
|
|
5363
|
+
let config = {};
|
|
5364
|
+
try {
|
|
5365
|
+
if (import_fs13.default.existsSync(configPath)) {
|
|
5366
|
+
config = JSON.parse(import_fs13.default.readFileSync(configPath, "utf8"));
|
|
5367
|
+
}
|
|
5368
|
+
} catch {
|
|
5369
|
+
throw new Error(`Cannot read config at ${configPath} \u2014 file may be corrupted`);
|
|
5370
|
+
}
|
|
5371
|
+
if (!config.policy || typeof config.policy !== "object") config.policy = {};
|
|
5372
|
+
const policy = config.policy;
|
|
5373
|
+
if (patch.type === "smartRule") {
|
|
5374
|
+
if (!Array.isArray(policy.smartRules)) policy.smartRules = [];
|
|
5375
|
+
const rules = policy.smartRules;
|
|
5376
|
+
if (patch.rule.name && rules.some((r) => r.name === patch.rule.name)) return;
|
|
5377
|
+
rules.push(patch.rule);
|
|
5378
|
+
} else {
|
|
5379
|
+
if (!Array.isArray(policy.ignoredTools)) policy.ignoredTools = [];
|
|
5380
|
+
const ignored = policy.ignoredTools;
|
|
5381
|
+
if (!ignored.includes(patch.toolName)) {
|
|
5382
|
+
ignored.push(patch.toolName);
|
|
5383
|
+
}
|
|
5384
|
+
}
|
|
5385
|
+
const dir = import_path16.default.dirname(configPath);
|
|
5386
|
+
import_fs13.default.mkdirSync(dir, { recursive: true });
|
|
5387
|
+
const tmp = configPath + ".node9-tmp";
|
|
5388
|
+
try {
|
|
5389
|
+
import_fs13.default.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
|
|
5390
|
+
} catch (err) {
|
|
5391
|
+
try {
|
|
5392
|
+
import_fs13.default.unlinkSync(tmp);
|
|
5393
|
+
} catch {
|
|
5394
|
+
}
|
|
5395
|
+
throw err;
|
|
5396
|
+
}
|
|
5397
|
+
try {
|
|
5398
|
+
import_fs13.default.renameSync(tmp, configPath);
|
|
5399
|
+
} catch (err) {
|
|
5400
|
+
try {
|
|
5401
|
+
import_fs13.default.unlinkSync(tmp);
|
|
5402
|
+
} catch {
|
|
5403
|
+
}
|
|
5404
|
+
throw err;
|
|
5405
|
+
}
|
|
5406
|
+
}
|
|
5407
|
+
var import_fs13, import_path16, import_os13, GLOBAL_CONFIG_PATH;
|
|
5408
|
+
var init_patch = __esm({
|
|
5409
|
+
"src/config/patch.ts"() {
|
|
5410
|
+
"use strict";
|
|
5411
|
+
import_fs13 = __toESM(require("fs"));
|
|
5412
|
+
import_path16 = __toESM(require("path"));
|
|
5413
|
+
import_os13 = __toESM(require("os"));
|
|
5414
|
+
GLOBAL_CONFIG_PATH = import_path16.default.join(import_os13.default.homedir(), ".node9", "config.json");
|
|
5415
|
+
}
|
|
5416
|
+
});
|
|
5417
|
+
|
|
5006
5418
|
// src/daemon/server.ts
|
|
5007
5419
|
function startDaemon() {
|
|
5008
|
-
|
|
5009
|
-
const
|
|
5420
|
+
loadInsightCounts();
|
|
5421
|
+
const csrfToken = (0, import_crypto5.randomUUID)();
|
|
5422
|
+
const internalToken = (0, import_crypto5.randomUUID)();
|
|
5010
5423
|
const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
|
|
5011
5424
|
const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
|
|
5012
5425
|
const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
|
|
@@ -5019,7 +5432,7 @@ function startDaemon() {
|
|
|
5019
5432
|
idleTimer = setTimeout(() => {
|
|
5020
5433
|
if (autoStarted) {
|
|
5021
5434
|
try {
|
|
5022
|
-
|
|
5435
|
+
import_fs14.default.unlinkSync(DAEMON_PID_FILE);
|
|
5023
5436
|
} catch {
|
|
5024
5437
|
}
|
|
5025
5438
|
}
|
|
@@ -5028,8 +5441,14 @@ function startDaemon() {
|
|
|
5028
5441
|
idleTimer.unref();
|
|
5029
5442
|
}
|
|
5030
5443
|
resetIdleTimer();
|
|
5444
|
+
const allowedHosts = /* @__PURE__ */ new Set([`127.0.0.1:${DAEMON_PORT}`, `localhost:${DAEMON_PORT}`]);
|
|
5031
5445
|
const server = import_http.default.createServer(async (req, res) => {
|
|
5032
|
-
const
|
|
5446
|
+
const host = req.headers.host ?? "";
|
|
5447
|
+
if (!allowedHosts.has(host)) {
|
|
5448
|
+
res.writeHead(421, { "Content-Type": "text/plain" });
|
|
5449
|
+
return res.end("Misdirected Request");
|
|
5450
|
+
}
|
|
5451
|
+
const reqUrl = new URL(req.url || "/", `http://${host}`);
|
|
5033
5452
|
const { pathname } = reqUrl;
|
|
5034
5453
|
if (req.method === "GET" && pathname === "/") {
|
|
5035
5454
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
@@ -5062,7 +5481,8 @@ data: ${JSON.stringify({
|
|
|
5062
5481
|
slackDelegated: e.slackDelegated,
|
|
5063
5482
|
timestamp: e.timestamp,
|
|
5064
5483
|
agent: e.agent,
|
|
5065
|
-
mcpServer: e.mcpServer
|
|
5484
|
+
mcpServer: e.mcpServer,
|
|
5485
|
+
allowCount: (insightCounts.get(e.toolName) ?? 0) + 1
|
|
5066
5486
|
})),
|
|
5067
5487
|
orgName: getOrgName(),
|
|
5068
5488
|
autoDenyMs: getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS
|
|
@@ -5104,6 +5524,12 @@ data: ${JSON.stringify(item.data)}
|
|
|
5104
5524
|
}
|
|
5105
5525
|
});
|
|
5106
5526
|
}
|
|
5527
|
+
if (req.method === "POST" && pathname === "/browser-opened") {
|
|
5528
|
+
if (req.headers["x-node9-internal"] !== internalToken) return res.writeHead(403).end();
|
|
5529
|
+
browserOpened = true;
|
|
5530
|
+
res.writeHead(200).end();
|
|
5531
|
+
return;
|
|
5532
|
+
}
|
|
5107
5533
|
if (req.method === "POST" && pathname === "/check") {
|
|
5108
5534
|
try {
|
|
5109
5535
|
resetIdleTimer();
|
|
@@ -5121,7 +5547,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5121
5547
|
activityId,
|
|
5122
5548
|
cwd
|
|
5123
5549
|
} = JSON.parse(body);
|
|
5124
|
-
const id = fromCLI && typeof activityId === "string" && activityId || (0,
|
|
5550
|
+
const id = fromCLI && typeof activityId === "string" && activityId || (0, import_crypto5.randomUUID)();
|
|
5125
5551
|
const entry = {
|
|
5126
5552
|
id,
|
|
5127
5553
|
toolName,
|
|
@@ -5147,7 +5573,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5147
5573
|
e.earlyReason = "No response \u2014 auto-denied after timeout";
|
|
5148
5574
|
}
|
|
5149
5575
|
pending.delete(id);
|
|
5150
|
-
broadcast("remove", { id });
|
|
5576
|
+
broadcast("remove", { id, decision: "deny" });
|
|
5151
5577
|
}
|
|
5152
5578
|
}, getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS)
|
|
5153
5579
|
};
|
|
@@ -5161,7 +5587,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5161
5587
|
status: "pending"
|
|
5162
5588
|
});
|
|
5163
5589
|
}
|
|
5164
|
-
const projectCwd = typeof cwd === "string" &&
|
|
5590
|
+
const projectCwd = typeof cwd === "string" && import_path17.default.isAbsolute(cwd) ? cwd : void 0;
|
|
5165
5591
|
const projectConfig = getConfig(projectCwd);
|
|
5166
5592
|
const browserEnabled = projectConfig.settings.approvers?.browser !== false;
|
|
5167
5593
|
const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
|
|
@@ -5174,7 +5600,10 @@ data: ${JSON.stringify(item.data)}
|
|
|
5174
5600
|
slackDelegated: entry.slackDelegated,
|
|
5175
5601
|
agent: entry.agent,
|
|
5176
5602
|
mcpServer: entry.mcpServer,
|
|
5177
|
-
interactive: terminalEnabled
|
|
5603
|
+
interactive: terminalEnabled,
|
|
5604
|
+
// allowCount = what this count will be if the user allows.
|
|
5605
|
+
// Terminal uses this to show the 💡 insight line on the Nth consecutive approval.
|
|
5606
|
+
allowCount: (insightCounts.get(toolName) ?? 0) + 1
|
|
5178
5607
|
});
|
|
5179
5608
|
const browserAlreadyOpened = process.env.NODE9_BROWSER_OPENED === "1";
|
|
5180
5609
|
if (browserEnabled && !browserOpened && !browserAlreadyOpened) {
|
|
@@ -5183,7 +5612,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5183
5612
|
}
|
|
5184
5613
|
}
|
|
5185
5614
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5186
|
-
res.end(JSON.stringify({ id }));
|
|
5615
|
+
res.end(JSON.stringify({ id, allowCount: (insightCounts.get(toolName) ?? 0) + 1 }));
|
|
5187
5616
|
if (slackDelegated) return;
|
|
5188
5617
|
authorizeHeadless(
|
|
5189
5618
|
toolName,
|
|
@@ -5210,7 +5639,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5210
5639
|
if (e.waiter) {
|
|
5211
5640
|
e.waiter(decision, result.reason);
|
|
5212
5641
|
pending.delete(id);
|
|
5213
|
-
broadcast("remove", { id });
|
|
5642
|
+
broadcast("remove", { id, decision });
|
|
5214
5643
|
} else {
|
|
5215
5644
|
e.earlyDecision = decision;
|
|
5216
5645
|
e.earlyReason = result.reason;
|
|
@@ -5226,7 +5655,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5226
5655
|
e.earlyReason = reason;
|
|
5227
5656
|
}
|
|
5228
5657
|
pending.delete(id);
|
|
5229
|
-
broadcast("remove", { id });
|
|
5658
|
+
broadcast("remove", { id, decision: "deny" });
|
|
5230
5659
|
});
|
|
5231
5660
|
return;
|
|
5232
5661
|
} catch {
|
|
@@ -5257,12 +5686,14 @@ data: ${JSON.stringify(item.data)}
|
|
|
5257
5686
|
res.end(JSON.stringify(body));
|
|
5258
5687
|
};
|
|
5259
5688
|
req.on("close", () => {
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5689
|
+
setTimeout(() => {
|
|
5690
|
+
const e = pending.get(id);
|
|
5691
|
+
if (e && e.waiter && e.earlyDecision === null) {
|
|
5692
|
+
clearTimeout(e.timer);
|
|
5693
|
+
pending.delete(id);
|
|
5694
|
+
broadcast("remove", { id });
|
|
5695
|
+
}
|
|
5696
|
+
}, 200);
|
|
5266
5697
|
});
|
|
5267
5698
|
return;
|
|
5268
5699
|
}
|
|
@@ -5291,10 +5722,10 @@ data: ${JSON.stringify(item.data)}
|
|
|
5291
5722
|
if (entry.waiter) {
|
|
5292
5723
|
entry.waiter("allow");
|
|
5293
5724
|
pending.delete(id);
|
|
5294
|
-
broadcast("remove", { id });
|
|
5725
|
+
broadcast("remove", { id, decision: "allow" });
|
|
5295
5726
|
} else {
|
|
5296
5727
|
entry.earlyDecision = "allow";
|
|
5297
|
-
broadcast("remove", { id });
|
|
5728
|
+
broadcast("remove", { id, decision: "allow" });
|
|
5298
5729
|
entry.timer = setTimeout(() => pending.delete(id), 3e4);
|
|
5299
5730
|
}
|
|
5300
5731
|
res.writeHead(200);
|
|
@@ -5308,16 +5739,29 @@ data: ${JSON.stringify(item.data)}
|
|
|
5308
5739
|
decision: resolvedDecision
|
|
5309
5740
|
});
|
|
5310
5741
|
clearTimeout(entry.timer);
|
|
5742
|
+
if (resolvedDecision === "allow" && !persist) {
|
|
5743
|
+
insightCounts.set(entry.toolName, (insightCounts.get(entry.toolName) ?? 0) + 1);
|
|
5744
|
+
saveInsightCounts();
|
|
5745
|
+
const suggestion = suggestionTracker.recordAllow(entry.toolName, entry.args);
|
|
5746
|
+
if (suggestion) {
|
|
5747
|
+
suggestions.set(suggestion.id, suggestion);
|
|
5748
|
+
broadcast("suggestion:new", suggestion);
|
|
5749
|
+
}
|
|
5750
|
+
} else if (resolvedDecision === "deny") {
|
|
5751
|
+
insightCounts.delete(entry.toolName);
|
|
5752
|
+
saveInsightCounts();
|
|
5753
|
+
suggestionTracker.resetTool(entry.toolName);
|
|
5754
|
+
}
|
|
5311
5755
|
const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
|
|
5312
5756
|
if (source && VALID_SOURCES.has(source)) entry.decisionSource = source;
|
|
5313
5757
|
if (entry.waiter) {
|
|
5314
5758
|
entry.waiter(resolvedDecision, reason);
|
|
5315
5759
|
pending.delete(id);
|
|
5316
|
-
broadcast("remove", { id });
|
|
5760
|
+
broadcast("remove", { id, decision: resolvedDecision });
|
|
5317
5761
|
} else {
|
|
5318
5762
|
entry.earlyDecision = resolvedDecision;
|
|
5319
5763
|
entry.earlyReason = reason;
|
|
5320
|
-
broadcast("remove", { id });
|
|
5764
|
+
broadcast("remove", { id, decision: resolvedDecision });
|
|
5321
5765
|
entry.timer = setTimeout(() => pending.delete(id), 3e4);
|
|
5322
5766
|
}
|
|
5323
5767
|
res.writeHead(200);
|
|
@@ -5405,13 +5849,38 @@ data: ${JSON.stringify(item.data)}
|
|
|
5405
5849
|
const id = pathname.split("/").pop();
|
|
5406
5850
|
const entry = pending.get(id);
|
|
5407
5851
|
if (!entry) return res.writeHead(404).end();
|
|
5408
|
-
const { decision } = JSON.parse(await readBody(req));
|
|
5409
|
-
|
|
5852
|
+
const { decision, source } = JSON.parse(await readBody(req));
|
|
5853
|
+
const resolvedResolveDecision = decision === "allow" ? "allow" : "deny";
|
|
5854
|
+
appendAuditLog({
|
|
5855
|
+
toolName: entry.toolName,
|
|
5856
|
+
args: entry.args,
|
|
5857
|
+
decision: resolvedResolveDecision
|
|
5858
|
+
});
|
|
5410
5859
|
clearTimeout(entry.timer);
|
|
5411
|
-
if (
|
|
5412
|
-
|
|
5860
|
+
if (resolvedResolveDecision === "allow") {
|
|
5861
|
+
insightCounts.set(entry.toolName, (insightCounts.get(entry.toolName) ?? 0) + 1);
|
|
5862
|
+
saveInsightCounts();
|
|
5863
|
+
} else {
|
|
5864
|
+
insightCounts.delete(entry.toolName);
|
|
5865
|
+
saveInsightCounts();
|
|
5866
|
+
}
|
|
5867
|
+
if (!entry.slackDelegated) {
|
|
5868
|
+
if (resolvedResolveDecision === "allow") {
|
|
5869
|
+
const suggestion = suggestionTracker.recordAllow(entry.toolName, entry.args);
|
|
5870
|
+
if (suggestion) {
|
|
5871
|
+
suggestions.set(suggestion.id, suggestion);
|
|
5872
|
+
broadcast("suggestion:new", suggestion);
|
|
5873
|
+
}
|
|
5874
|
+
} else {
|
|
5875
|
+
suggestionTracker.resetTool(entry.toolName);
|
|
5876
|
+
}
|
|
5877
|
+
}
|
|
5878
|
+
const VALID_RESOLVE_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
|
|
5879
|
+
if (source && VALID_RESOLVE_SOURCES.has(source)) entry.decisionSource = source;
|
|
5880
|
+
if (entry.waiter) entry.waiter(resolvedResolveDecision);
|
|
5881
|
+
else entry.earlyDecision = resolvedResolveDecision;
|
|
5413
5882
|
pending.delete(id);
|
|
5414
|
-
broadcast("remove", { id });
|
|
5883
|
+
broadcast("remove", { id, decision: resolvedResolveDecision });
|
|
5415
5884
|
res.writeHead(200);
|
|
5416
5885
|
return res.end(JSON.stringify({ ok: true }));
|
|
5417
5886
|
} catch {
|
|
@@ -5459,20 +5928,79 @@ data: ${JSON.stringify(item.data)}
|
|
|
5459
5928
|
res.writeHead(400).end();
|
|
5460
5929
|
}
|
|
5461
5930
|
}
|
|
5931
|
+
if (req.method === "GET" && pathname === "/suggestions") {
|
|
5932
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5933
|
+
return res.end(JSON.stringify([...suggestions.values()]));
|
|
5934
|
+
}
|
|
5935
|
+
if (req.method === "POST" && pathname.startsWith("/suggestions/") && pathname.endsWith("/apply")) {
|
|
5936
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
5937
|
+
try {
|
|
5938
|
+
const body = await readBody(req);
|
|
5939
|
+
const data = body ? JSON.parse(body) : {};
|
|
5940
|
+
const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
|
|
5941
|
+
const node9Dir = import_path17.default.dirname(GLOBAL_CONFIG_PATH);
|
|
5942
|
+
if (!import_path17.default.resolve(configPath).startsWith(node9Dir + import_path17.default.sep)) {
|
|
5943
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
5944
|
+
return res.end(
|
|
5945
|
+
JSON.stringify({ error: "configPath must be within the node9 config directory" })
|
|
5946
|
+
);
|
|
5947
|
+
}
|
|
5948
|
+
const id = pathname.split("/")[2];
|
|
5949
|
+
const suggestion = suggestions.get(id);
|
|
5950
|
+
if (!suggestion) return res.writeHead(404).end();
|
|
5951
|
+
let patch;
|
|
5952
|
+
if (data.rule !== void 0) {
|
|
5953
|
+
const parsed = SmartRuleSchema.safeParse(data.rule);
|
|
5954
|
+
if (!parsed.success) {
|
|
5955
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
5956
|
+
return res.end(JSON.stringify({ error: parsed.error.message }));
|
|
5957
|
+
}
|
|
5958
|
+
patch = { type: "smartRule", rule: parsed.data };
|
|
5959
|
+
} else {
|
|
5960
|
+
patch = suggestion.suggestedRule;
|
|
5961
|
+
}
|
|
5962
|
+
patchConfig(configPath, patch);
|
|
5963
|
+
_resetConfigCache();
|
|
5964
|
+
insightCounts.delete(suggestion.toolName);
|
|
5965
|
+
saveInsightCounts();
|
|
5966
|
+
suggestion.status = "applied";
|
|
5967
|
+
broadcast("suggestion:resolved", { id, status: "applied" });
|
|
5968
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5969
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
5970
|
+
} catch (err) {
|
|
5971
|
+
console.error(import_chalk2.default.red("[node9 daemon] POST /suggestions/:id/apply failed:"), err);
|
|
5972
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
5973
|
+
return res.end(JSON.stringify({ error: String(err) }));
|
|
5974
|
+
}
|
|
5975
|
+
}
|
|
5976
|
+
if (req.method === "POST" && pathname.startsWith("/suggestions/") && pathname.endsWith("/dismiss")) {
|
|
5977
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
5978
|
+
try {
|
|
5979
|
+
const id = pathname.split("/")[2];
|
|
5980
|
+
const suggestion = suggestions.get(id);
|
|
5981
|
+
if (!suggestion) return res.writeHead(404).end();
|
|
5982
|
+
suggestion.status = "dismissed";
|
|
5983
|
+
broadcast("suggestion:resolved", { id, status: "dismissed" });
|
|
5984
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5985
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
5986
|
+
} catch {
|
|
5987
|
+
res.writeHead(400).end();
|
|
5988
|
+
}
|
|
5989
|
+
}
|
|
5462
5990
|
res.writeHead(404).end();
|
|
5463
5991
|
});
|
|
5464
5992
|
setDaemonServer(server);
|
|
5465
5993
|
server.on("error", (e) => {
|
|
5466
5994
|
if (e.code === "EADDRINUSE") {
|
|
5467
5995
|
try {
|
|
5468
|
-
if (
|
|
5469
|
-
const { pid } = JSON.parse(
|
|
5996
|
+
if (import_fs14.default.existsSync(DAEMON_PID_FILE)) {
|
|
5997
|
+
const { pid } = JSON.parse(import_fs14.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
5470
5998
|
process.kill(pid, 0);
|
|
5471
5999
|
return process.exit(0);
|
|
5472
6000
|
}
|
|
5473
6001
|
} catch {
|
|
5474
6002
|
try {
|
|
5475
|
-
|
|
6003
|
+
import_fs14.default.unlinkSync(DAEMON_PID_FILE);
|
|
5476
6004
|
} catch {
|
|
5477
6005
|
}
|
|
5478
6006
|
server.listen(DAEMON_PORT, DAEMON_HOST);
|
|
@@ -5531,43 +6059,45 @@ data: ${JSON.stringify(item.data)}
|
|
|
5531
6059
|
}
|
|
5532
6060
|
startActivitySocket();
|
|
5533
6061
|
}
|
|
5534
|
-
var import_http,
|
|
6062
|
+
var import_http, import_fs14, import_path17, import_crypto5, import_child_process4, import_chalk2;
|
|
5535
6063
|
var init_server = __esm({
|
|
5536
6064
|
"src/daemon/server.ts"() {
|
|
5537
6065
|
"use strict";
|
|
5538
6066
|
import_http = __toESM(require("http"));
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
6067
|
+
import_fs14 = __toESM(require("fs"));
|
|
6068
|
+
import_path17 = __toESM(require("path"));
|
|
6069
|
+
import_crypto5 = require("crypto");
|
|
5542
6070
|
import_child_process4 = require("child_process");
|
|
5543
6071
|
import_chalk2 = __toESM(require("chalk"));
|
|
5544
6072
|
init_core();
|
|
5545
6073
|
init_shields();
|
|
5546
6074
|
init_ui2();
|
|
5547
6075
|
init_state2();
|
|
6076
|
+
init_patch();
|
|
6077
|
+
init_config_schema();
|
|
5548
6078
|
}
|
|
5549
6079
|
});
|
|
5550
6080
|
|
|
5551
6081
|
// src/daemon/index.ts
|
|
5552
6082
|
function stopDaemon() {
|
|
5553
|
-
if (!
|
|
6083
|
+
if (!import_fs15.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk3.default.yellow("Not running."));
|
|
5554
6084
|
try {
|
|
5555
|
-
const { pid } = JSON.parse(
|
|
6085
|
+
const { pid } = JSON.parse(import_fs15.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
5556
6086
|
process.kill(pid, "SIGTERM");
|
|
5557
6087
|
console.log(import_chalk3.default.green("\u2705 Stopped."));
|
|
5558
6088
|
} catch {
|
|
5559
6089
|
console.log(import_chalk3.default.gray("Cleaned up stale PID file."));
|
|
5560
6090
|
} finally {
|
|
5561
6091
|
try {
|
|
5562
|
-
|
|
6092
|
+
import_fs15.default.unlinkSync(DAEMON_PID_FILE);
|
|
5563
6093
|
} catch {
|
|
5564
6094
|
}
|
|
5565
6095
|
}
|
|
5566
6096
|
}
|
|
5567
6097
|
function daemonStatus() {
|
|
5568
|
-
if (
|
|
6098
|
+
if (import_fs15.default.existsSync(DAEMON_PID_FILE)) {
|
|
5569
6099
|
try {
|
|
5570
|
-
const { pid } = JSON.parse(
|
|
6100
|
+
const { pid } = JSON.parse(import_fs15.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
5571
6101
|
process.kill(pid, 0);
|
|
5572
6102
|
console.log(import_chalk3.default.green("Node9 daemon: running"));
|
|
5573
6103
|
return;
|
|
@@ -5586,11 +6116,11 @@ function daemonStatus() {
|
|
|
5586
6116
|
console.log(import_chalk3.default.yellow("Node9 daemon: not running"));
|
|
5587
6117
|
}
|
|
5588
6118
|
}
|
|
5589
|
-
var
|
|
6119
|
+
var import_fs15, import_chalk3, import_child_process5;
|
|
5590
6120
|
var init_daemon2 = __esm({
|
|
5591
6121
|
"src/daemon/index.ts"() {
|
|
5592
6122
|
"use strict";
|
|
5593
|
-
|
|
6123
|
+
import_fs15 = __toESM(require("fs"));
|
|
5594
6124
|
import_chalk3 = __toESM(require("chalk"));
|
|
5595
6125
|
import_child_process5 = require("child_process");
|
|
5596
6126
|
init_server();
|
|
@@ -5617,17 +6147,17 @@ function formatBase(activity) {
|
|
|
5617
6147
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
5618
6148
|
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
|
|
5619
6149
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
5620
|
-
return `${
|
|
6150
|
+
return `${import_chalk16.default.gray(time)} ${icon} ${import_chalk16.default.white.bold(toolName)} ${import_chalk16.default.dim(argsPreview)}`;
|
|
5621
6151
|
}
|
|
5622
6152
|
function renderResult(activity, result) {
|
|
5623
6153
|
const base = formatBase(activity);
|
|
5624
6154
|
let status;
|
|
5625
6155
|
if (result.status === "allow") {
|
|
5626
|
-
status =
|
|
6156
|
+
status = import_chalk16.default.green("\u2713 ALLOW");
|
|
5627
6157
|
} else if (result.status === "dlp") {
|
|
5628
|
-
status =
|
|
6158
|
+
status = import_chalk16.default.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
5629
6159
|
} else {
|
|
5630
|
-
status =
|
|
6160
|
+
status = import_chalk16.default.red("\u2717 BLOCK");
|
|
5631
6161
|
}
|
|
5632
6162
|
if (process.stdout.isTTY) {
|
|
5633
6163
|
import_readline3.default.clearLine(process.stdout, 0);
|
|
@@ -5637,16 +6167,16 @@ function renderResult(activity, result) {
|
|
|
5637
6167
|
}
|
|
5638
6168
|
function renderPending(activity) {
|
|
5639
6169
|
if (!process.stdout.isTTY) return;
|
|
5640
|
-
process.stdout.write(`${formatBase(activity)} ${
|
|
6170
|
+
process.stdout.write(`${formatBase(activity)} ${import_chalk16.default.yellow("\u25CF \u2026")}\r`);
|
|
5641
6171
|
}
|
|
5642
6172
|
async function ensureDaemon() {
|
|
5643
6173
|
let pidPort = null;
|
|
5644
|
-
if (
|
|
6174
|
+
if (import_fs23.default.existsSync(PID_FILE)) {
|
|
5645
6175
|
try {
|
|
5646
|
-
const { port } = JSON.parse(
|
|
6176
|
+
const { port } = JSON.parse(import_fs23.default.readFileSync(PID_FILE, "utf-8"));
|
|
5647
6177
|
pidPort = port;
|
|
5648
6178
|
} catch {
|
|
5649
|
-
console.error(
|
|
6179
|
+
console.error(import_chalk16.default.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
5650
6180
|
}
|
|
5651
6181
|
}
|
|
5652
6182
|
const checkPort = pidPort ?? DAEMON_PORT;
|
|
@@ -5657,7 +6187,7 @@ async function ensureDaemon() {
|
|
|
5657
6187
|
if (res.ok) return checkPort;
|
|
5658
6188
|
} catch {
|
|
5659
6189
|
}
|
|
5660
|
-
console.log(
|
|
6190
|
+
console.log(import_chalk16.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
5661
6191
|
const child = (0, import_child_process13.spawn)(process.execPath, [process.argv[1], "daemon"], {
|
|
5662
6192
|
detached: true,
|
|
5663
6193
|
stdio: "ignore",
|
|
@@ -5674,12 +6204,15 @@ async function ensureDaemon() {
|
|
|
5674
6204
|
} catch {
|
|
5675
6205
|
}
|
|
5676
6206
|
}
|
|
5677
|
-
console.error(
|
|
6207
|
+
console.error(import_chalk16.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
5678
6208
|
process.exit(1);
|
|
5679
6209
|
}
|
|
5680
|
-
function postDecisionHttp(id, decision, csrfToken, port) {
|
|
6210
|
+
function postDecisionHttp(id, decision, csrfToken, port, opts) {
|
|
5681
6211
|
return new Promise((resolve, reject) => {
|
|
5682
|
-
const
|
|
6212
|
+
const bodyObj = { decision, source: "terminal" };
|
|
6213
|
+
if (opts?.persist) bodyObj.persist = true;
|
|
6214
|
+
if (opts?.trustDuration) bodyObj.trustDuration = opts.trustDuration;
|
|
6215
|
+
const body = JSON.stringify(bodyObj);
|
|
5683
6216
|
const req = import_http2.default.request(
|
|
5684
6217
|
{
|
|
5685
6218
|
hostname: "127.0.0.1",
|
|
@@ -5702,22 +6235,30 @@ function postDecisionHttp(id, decision, csrfToken, port) {
|
|
|
5702
6235
|
req.end(body);
|
|
5703
6236
|
});
|
|
5704
6237
|
}
|
|
5705
|
-
function buildCardLines(req) {
|
|
6238
|
+
function buildCardLines(req, localCount = 0) {
|
|
5706
6239
|
const argsStr = JSON.stringify(req.args ?? {}).replace(/\s+/g, " ");
|
|
5707
6240
|
const argsPreview = argsStr.length > 60 ? argsStr.slice(0, 60) + "\u2026" : argsStr;
|
|
5708
6241
|
const tierLabel = req.riskMetadata?.tier != null ? req.riskMetadata.tier <= 2 ? `${YELLOW}\u26A0 Tier ${req.riskMetadata.tier}` : `${RED}\u{1F6D1} Tier ${req.riskMetadata.tier}` : `${YELLOW}\u26A0 Review`;
|
|
5709
6242
|
const blockedBy = req.riskMetadata?.blockedByLabel ?? "Policy rule";
|
|
5710
|
-
|
|
6243
|
+
const lines = [
|
|
5711
6244
|
``,
|
|
5712
6245
|
`${BOLD}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET}`,
|
|
5713
6246
|
`${CYAN}\u2551${RESET} Tool: ${BOLD}${req.toolName}${RESET}`,
|
|
5714
6247
|
`${CYAN}\u2551${RESET} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET}`,
|
|
5715
|
-
`${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}
|
|
6248
|
+
`${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}`
|
|
6249
|
+
];
|
|
6250
|
+
if (localCount >= 2) {
|
|
6251
|
+
lines.push(
|
|
6252
|
+
`${CYAN}\u2551${RESET} ${YELLOW}\u{1F4A1}${RESET} Approved ${localCount}\xD7 before \u2014 ${BOLD}[a]${RESET}${YELLOW} creates a permanent rule${RESET}`
|
|
6253
|
+
);
|
|
6254
|
+
}
|
|
6255
|
+
lines.push(
|
|
5716
6256
|
`${CYAN}\u255A${RESET}`,
|
|
5717
6257
|
``,
|
|
5718
|
-
` ${BOLD}${GREEN}[
|
|
6258
|
+
` ${BOLD}${GREEN}[\u21B5/y]${RESET} Allow ${BOLD}${RED}[n]${RESET} Deny ${BOLD}${YELLOW}[a]${RESET} Always Allow ${BOLD}${CYAN}[t]${RESET} Trust 30m`,
|
|
5719
6259
|
``
|
|
5720
|
-
|
|
6260
|
+
);
|
|
6261
|
+
return lines;
|
|
5721
6262
|
}
|
|
5722
6263
|
async function startTail(options = {}) {
|
|
5723
6264
|
const port = await ensureDaemon();
|
|
@@ -5745,7 +6286,7 @@ async function startTail(options = {}) {
|
|
|
5745
6286
|
req2.end();
|
|
5746
6287
|
});
|
|
5747
6288
|
if (result.ok) {
|
|
5748
|
-
console.log(
|
|
6289
|
+
console.log(import_chalk16.default.green("\u2713 Flight Recorder buffer cleared."));
|
|
5749
6290
|
} else if (result.code === "ECONNREFUSED") {
|
|
5750
6291
|
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
5751
6292
|
} else if (result.code === "ETIMEDOUT") {
|
|
@@ -5762,6 +6303,7 @@ async function startTail(options = {}) {
|
|
|
5762
6303
|
let cardActive = false;
|
|
5763
6304
|
let cardLineCount = 0;
|
|
5764
6305
|
let cancelActiveCard = null;
|
|
6306
|
+
const localAllowCounts = /* @__PURE__ */ new Map();
|
|
5765
6307
|
const canApprove = process.stdout.isTTY && process.stdin.isTTY;
|
|
5766
6308
|
if (canApprove) import_readline3.default.emitKeypressEvents(process.stdin);
|
|
5767
6309
|
function clearCard() {
|
|
@@ -5772,7 +6314,10 @@ async function startTail(options = {}) {
|
|
|
5772
6314
|
}
|
|
5773
6315
|
function printCard(req2) {
|
|
5774
6316
|
process.stdout.write(HIDE_CURSOR + SAVE_CURSOR);
|
|
5775
|
-
const
|
|
6317
|
+
const daemonPrior = req2.allowCount !== void 0 ? req2.allowCount - 1 : 0;
|
|
6318
|
+
const localPrior = localAllowCounts.get(req2.toolName) ?? 0;
|
|
6319
|
+
const priorCount = Math.max(daemonPrior, localPrior);
|
|
6320
|
+
const lines = buildCardLines(req2, priorCount);
|
|
5776
6321
|
for (const line of lines) process.stdout.write(line + "\n");
|
|
5777
6322
|
cardLineCount = lines.length;
|
|
5778
6323
|
}
|
|
@@ -5800,34 +6345,70 @@ async function startTail(options = {}) {
|
|
|
5800
6345
|
process.stdin.pause();
|
|
5801
6346
|
cancelActiveCard = null;
|
|
5802
6347
|
};
|
|
5803
|
-
const settle = (
|
|
6348
|
+
const settle = (action) => {
|
|
5804
6349
|
if (settled) return;
|
|
5805
6350
|
settled = true;
|
|
5806
6351
|
cleanup();
|
|
5807
|
-
|
|
6352
|
+
process.stdout.write(RESTORE_CURSOR + ERASE_DOWN);
|
|
6353
|
+
const stampedLines = buildCardLines(
|
|
6354
|
+
req2,
|
|
6355
|
+
Math.max(
|
|
6356
|
+
req2.allowCount !== void 0 ? req2.allowCount - 1 : 0,
|
|
6357
|
+
localAllowCounts.get(req2.toolName) ?? 0
|
|
6358
|
+
)
|
|
6359
|
+
);
|
|
6360
|
+
const decisionStamp = action === "always-allow" ? import_chalk16.default.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? import_chalk16.default.cyan("\u23F1 TRUST 30m") : action === "allow" ? import_chalk16.default.green("\u2713 ALLOWED") : import_chalk16.default.red("\u2717 DENIED");
|
|
6361
|
+
stampedLines.push(` ${BOLD}\u2192${RESET} ${decisionStamp} ${GRAY}(terminal)${RESET}`, ``);
|
|
6362
|
+
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
5808
6363
|
process.stdout.write(SHOW_CURSOR);
|
|
5809
|
-
|
|
6364
|
+
cardLineCount = 0;
|
|
6365
|
+
if (action === "allow" || action === "always-allow" || action === "trust") {
|
|
6366
|
+
localAllowCounts.set(req2.toolName, (localAllowCounts.get(req2.toolName) ?? 0) + 1);
|
|
6367
|
+
} else if (action === "deny") {
|
|
6368
|
+
localAllowCounts.delete(req2.toolName);
|
|
6369
|
+
}
|
|
6370
|
+
let httpDecision;
|
|
6371
|
+
let httpOpts;
|
|
6372
|
+
if (action === "always-allow") {
|
|
6373
|
+
httpDecision = "allow";
|
|
6374
|
+
httpOpts = { persist: true };
|
|
6375
|
+
} else if (action === "trust") {
|
|
6376
|
+
httpDecision = "trust";
|
|
6377
|
+
httpOpts = { trustDuration: "30m" };
|
|
6378
|
+
} else {
|
|
6379
|
+
httpDecision = action;
|
|
6380
|
+
}
|
|
6381
|
+
postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err) => {
|
|
5810
6382
|
try {
|
|
5811
|
-
|
|
5812
|
-
|
|
6383
|
+
import_fs23.default.appendFileSync(
|
|
6384
|
+
import_path25.default.join(import_os21.default.homedir(), ".node9", "hook-debug.log"),
|
|
5813
6385
|
`[tail] POST /decision failed: ${String(err)}
|
|
5814
6386
|
`
|
|
5815
6387
|
);
|
|
5816
6388
|
} catch {
|
|
5817
6389
|
}
|
|
5818
6390
|
});
|
|
5819
|
-
const decisionLabel = decision === "allow" ? import_chalk15.default.green("\u2713 ALLOWED (terminal)") : import_chalk15.default.red("\u2717 DENIED (terminal)");
|
|
5820
|
-
console.log(`${import_chalk15.default.cyan("\u25C6")} ${import_chalk15.default.bold(req2.toolName.padEnd(16))} ${decisionLabel}`);
|
|
5821
6391
|
approvalQueue.shift();
|
|
5822
6392
|
cardActive = false;
|
|
5823
6393
|
showNextCard();
|
|
5824
6394
|
};
|
|
5825
|
-
cancelActiveCard = () => {
|
|
6395
|
+
cancelActiveCard = (externalDecision) => {
|
|
5826
6396
|
if (settled) return;
|
|
5827
6397
|
settled = true;
|
|
5828
6398
|
cleanup();
|
|
5829
|
-
|
|
6399
|
+
process.stdout.write(RESTORE_CURSOR + ERASE_DOWN);
|
|
6400
|
+
const priorCount = Math.max(
|
|
6401
|
+
req2.allowCount !== void 0 ? req2.allowCount - 1 : 0,
|
|
6402
|
+
localAllowCounts.get(req2.toolName) ?? 0
|
|
6403
|
+
);
|
|
6404
|
+
const stampedLines = buildCardLines(req2, priorCount);
|
|
6405
|
+
if (externalDecision) {
|
|
6406
|
+
const source = externalDecision === "allow" ? import_chalk16.default.green("\u2713 ALLOWED") : import_chalk16.default.red("\u2717 DENIED");
|
|
6407
|
+
stampedLines.push(` ${BOLD}\u2192${RESET} ${source} ${GRAY}(external)${RESET}`, ``);
|
|
6408
|
+
}
|
|
6409
|
+
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
5830
6410
|
process.stdout.write(SHOW_CURSOR);
|
|
6411
|
+
cardLineCount = 0;
|
|
5831
6412
|
approvalQueue.shift();
|
|
5832
6413
|
cardActive = false;
|
|
5833
6414
|
showNextCard();
|
|
@@ -5835,10 +6416,14 @@ async function startTail(options = {}) {
|
|
|
5835
6416
|
process.stdin.resume();
|
|
5836
6417
|
onKeypress = (_str, key) => {
|
|
5837
6418
|
const name = key?.name ?? "";
|
|
5838
|
-
if (name === "
|
|
6419
|
+
if (name === "y" || name === "return") {
|
|
5839
6420
|
settle("allow");
|
|
5840
|
-
} else if (name === "
|
|
6421
|
+
} else if (name === "n" || name === "d" || key?.ctrl && name === "c") {
|
|
5841
6422
|
settle("deny");
|
|
6423
|
+
} else if (name === "a") {
|
|
6424
|
+
settle("always-allow");
|
|
6425
|
+
} else if (name === "t") {
|
|
6426
|
+
settle("trust");
|
|
5842
6427
|
}
|
|
5843
6428
|
};
|
|
5844
6429
|
process.stdin.on("keypress", onKeypress);
|
|
@@ -5851,19 +6436,27 @@ async function startTail(options = {}) {
|
|
|
5851
6436
|
else if (process.platform === "win32")
|
|
5852
6437
|
(0, import_child_process13.execSync)(`cmd /c start "" "${dashboardUrl}"`, { stdio: "ignore" });
|
|
5853
6438
|
else (0, import_child_process13.execSync)(`xdg-open "${dashboardUrl}"`, { stdio: "ignore" });
|
|
6439
|
+
const intToken = getInternalToken();
|
|
6440
|
+
fetch(`http://127.0.0.1:${port}/browser-opened`, {
|
|
6441
|
+
method: "POST",
|
|
6442
|
+
headers: intToken ? { "X-Node9-Internal": intToken } : {}
|
|
6443
|
+
}).catch(() => {
|
|
6444
|
+
});
|
|
5854
6445
|
}
|
|
5855
6446
|
} catch {
|
|
5856
6447
|
}
|
|
5857
|
-
console.log(
|
|
5858
|
-
\u{1F6F0}\uFE0F Node9 tail `) +
|
|
6448
|
+
console.log(import_chalk16.default.cyan.bold(`
|
|
6449
|
+
\u{1F6F0}\uFE0F Node9 tail `) + import_chalk16.default.dim(`\u2192 ${dashboardUrl}`));
|
|
5859
6450
|
if (canApprove) {
|
|
5860
|
-
console.log(
|
|
6451
|
+
console.log(
|
|
6452
|
+
import_chalk16.default.dim("Interactive approvals: [\u21B5/y] Allow [n] Deny [a] Always Allow [t] Trust 30m")
|
|
6453
|
+
);
|
|
5861
6454
|
}
|
|
5862
6455
|
if (options.history) {
|
|
5863
|
-
console.log(
|
|
6456
|
+
console.log(import_chalk16.default.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
|
|
5864
6457
|
} else {
|
|
5865
6458
|
console.log(
|
|
5866
|
-
|
|
6459
|
+
import_chalk16.default.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
|
|
5867
6460
|
);
|
|
5868
6461
|
}
|
|
5869
6462
|
process.on("SIGINT", () => {
|
|
@@ -5873,13 +6466,13 @@ async function startTail(options = {}) {
|
|
|
5873
6466
|
import_readline3.default.clearLine(process.stdout, 0);
|
|
5874
6467
|
import_readline3.default.cursorTo(process.stdout, 0);
|
|
5875
6468
|
}
|
|
5876
|
-
console.log(
|
|
6469
|
+
console.log(import_chalk16.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
5877
6470
|
process.exit(0);
|
|
5878
6471
|
});
|
|
5879
6472
|
const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
|
|
5880
6473
|
const req = import_http2.default.get(sseUrl, (res) => {
|
|
5881
6474
|
if (res.statusCode !== 200) {
|
|
5882
|
-
console.error(
|
|
6475
|
+
console.error(import_chalk16.default.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
5883
6476
|
process.exit(1);
|
|
5884
6477
|
}
|
|
5885
6478
|
let currentEvent = "";
|
|
@@ -5909,7 +6502,7 @@ async function startTail(options = {}) {
|
|
|
5909
6502
|
import_readline3.default.clearLine(process.stdout, 0);
|
|
5910
6503
|
import_readline3.default.cursorTo(process.stdout, 0);
|
|
5911
6504
|
}
|
|
5912
|
-
console.log(
|
|
6505
|
+
console.log(import_chalk16.default.red("\n\u274C Daemon disconnected."));
|
|
5913
6506
|
process.exit(1);
|
|
5914
6507
|
});
|
|
5915
6508
|
});
|
|
@@ -5950,11 +6543,17 @@ async function startTail(options = {}) {
|
|
|
5950
6543
|
}
|
|
5951
6544
|
if (event === "remove") {
|
|
5952
6545
|
try {
|
|
5953
|
-
const { id } = JSON.parse(rawData);
|
|
6546
|
+
const { id, decision } = JSON.parse(rawData);
|
|
5954
6547
|
const idx = approvalQueue.findIndex((r) => r.id === id);
|
|
5955
6548
|
if (idx !== -1) {
|
|
5956
6549
|
if (idx === 0 && cardActive && cancelActiveCard) {
|
|
5957
|
-
|
|
6550
|
+
const toolName = approvalQueue[0].toolName;
|
|
6551
|
+
if (decision === "allow") {
|
|
6552
|
+
localAllowCounts.set(toolName, (localAllowCounts.get(toolName) ?? 0) + 1);
|
|
6553
|
+
} else if (decision === "deny") {
|
|
6554
|
+
localAllowCounts.delete(toolName);
|
|
6555
|
+
}
|
|
6556
|
+
cancelActiveCard(decision);
|
|
5958
6557
|
} else {
|
|
5959
6558
|
approvalQueue.splice(idx, 1);
|
|
5960
6559
|
}
|
|
@@ -5989,25 +6588,26 @@ async function startTail(options = {}) {
|
|
|
5989
6588
|
}
|
|
5990
6589
|
req.on("error", (err) => {
|
|
5991
6590
|
const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
|
|
5992
|
-
console.error(
|
|
6591
|
+
console.error(import_chalk16.default.red(`
|
|
5993
6592
|
\u274C ${msg}`));
|
|
5994
6593
|
process.exit(1);
|
|
5995
6594
|
});
|
|
5996
6595
|
}
|
|
5997
|
-
var import_http2,
|
|
6596
|
+
var import_http2, import_chalk16, import_fs23, import_os21, import_path25, import_readline3, import_child_process13, PID_FILE, ICONS, RESET, BOLD, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, SAVE_CURSOR, RESTORE_CURSOR;
|
|
5998
6597
|
var init_tail = __esm({
|
|
5999
6598
|
"src/tui/tail.ts"() {
|
|
6000
6599
|
"use strict";
|
|
6001
6600
|
import_http2 = __toESM(require("http"));
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6601
|
+
import_chalk16 = __toESM(require("chalk"));
|
|
6602
|
+
import_fs23 = __toESM(require("fs"));
|
|
6603
|
+
import_os21 = __toESM(require("os"));
|
|
6604
|
+
import_path25 = __toESM(require("path"));
|
|
6006
6605
|
import_readline3 = __toESM(require("readline"));
|
|
6007
6606
|
import_child_process13 = require("child_process");
|
|
6008
6607
|
init_daemon2();
|
|
6608
|
+
init_daemon();
|
|
6009
6609
|
init_core();
|
|
6010
|
-
PID_FILE =
|
|
6610
|
+
PID_FILE = import_path25.default.join(import_os21.default.homedir(), ".node9", "daemon.pid");
|
|
6011
6611
|
ICONS = {
|
|
6012
6612
|
bash: "\u{1F4BB}",
|
|
6013
6613
|
shell: "\u{1F4BB}",
|
|
@@ -6355,6 +6955,25 @@ async function setupGemini() {
|
|
|
6355
6955
|
printDaemonTip();
|
|
6356
6956
|
}
|
|
6357
6957
|
}
|
|
6958
|
+
function detectAgents(homeDir2 = import_os11.default.homedir()) {
|
|
6959
|
+
const exists = (p) => {
|
|
6960
|
+
try {
|
|
6961
|
+
return import_fs11.default.existsSync(p);
|
|
6962
|
+
} catch (err) {
|
|
6963
|
+
const code = err.code;
|
|
6964
|
+
if (code !== "ENOENT") {
|
|
6965
|
+
process.stderr.write(`[node9] detectAgents: cannot access ${p}: ${code ?? String(err)}
|
|
6966
|
+
`);
|
|
6967
|
+
}
|
|
6968
|
+
return false;
|
|
6969
|
+
}
|
|
6970
|
+
};
|
|
6971
|
+
return {
|
|
6972
|
+
claude: exists(import_path14.default.join(homeDir2, ".claude")) || exists(import_path14.default.join(homeDir2, ".claude.json")),
|
|
6973
|
+
gemini: exists(import_path14.default.join(homeDir2, ".gemini")),
|
|
6974
|
+
cursor: exists(import_path14.default.join(homeDir2, ".cursor"))
|
|
6975
|
+
};
|
|
6976
|
+
}
|
|
6358
6977
|
async function setupCursor() {
|
|
6359
6978
|
const homeDir2 = import_os11.default.homedir();
|
|
6360
6979
|
const mcpPath = import_path14.default.join(homeDir2, ".cursor", "mcp.json");
|
|
@@ -6413,10 +7032,10 @@ async function setupCursor() {
|
|
|
6413
7032
|
|
|
6414
7033
|
// src/cli.ts
|
|
6415
7034
|
init_daemon2();
|
|
6416
|
-
var
|
|
6417
|
-
var
|
|
6418
|
-
var
|
|
6419
|
-
var
|
|
7035
|
+
var import_chalk17 = __toESM(require("chalk"));
|
|
7036
|
+
var import_fs24 = __toESM(require("fs"));
|
|
7037
|
+
var import_path26 = __toESM(require("path"));
|
|
7038
|
+
var import_os22 = __toESM(require("os"));
|
|
6420
7039
|
var import_prompts3 = require("@inquirer/prompts");
|
|
6421
7040
|
|
|
6422
7041
|
// src/utils/duration.ts
|
|
@@ -6637,9 +7256,9 @@ async function autoStartDaemonAndWait() {
|
|
|
6637
7256
|
|
|
6638
7257
|
// src/cli/commands/check.ts
|
|
6639
7258
|
var import_chalk5 = __toESM(require("chalk"));
|
|
6640
|
-
var
|
|
6641
|
-
var
|
|
6642
|
-
var
|
|
7259
|
+
var import_fs17 = __toESM(require("fs"));
|
|
7260
|
+
var import_path19 = __toESM(require("path"));
|
|
7261
|
+
var import_os15 = __toESM(require("os"));
|
|
6643
7262
|
init_orchestrator();
|
|
6644
7263
|
init_daemon();
|
|
6645
7264
|
init_config();
|
|
@@ -6647,26 +7266,26 @@ init_policy();
|
|
|
6647
7266
|
|
|
6648
7267
|
// src/undo.ts
|
|
6649
7268
|
var import_child_process8 = require("child_process");
|
|
6650
|
-
var
|
|
6651
|
-
var
|
|
6652
|
-
var
|
|
6653
|
-
var
|
|
6654
|
-
var SNAPSHOT_STACK_PATH =
|
|
6655
|
-
var UNDO_LATEST_PATH =
|
|
7269
|
+
var import_crypto6 = __toESM(require("crypto"));
|
|
7270
|
+
var import_fs16 = __toESM(require("fs"));
|
|
7271
|
+
var import_path18 = __toESM(require("path"));
|
|
7272
|
+
var import_os14 = __toESM(require("os"));
|
|
7273
|
+
var SNAPSHOT_STACK_PATH = import_path18.default.join(import_os14.default.homedir(), ".node9", "snapshots.json");
|
|
7274
|
+
var UNDO_LATEST_PATH = import_path18.default.join(import_os14.default.homedir(), ".node9", "undo_latest.txt");
|
|
6656
7275
|
var MAX_SNAPSHOTS = 10;
|
|
6657
7276
|
var GIT_TIMEOUT = 15e3;
|
|
6658
7277
|
function readStack() {
|
|
6659
7278
|
try {
|
|
6660
|
-
if (
|
|
6661
|
-
return JSON.parse(
|
|
7279
|
+
if (import_fs16.default.existsSync(SNAPSHOT_STACK_PATH))
|
|
7280
|
+
return JSON.parse(import_fs16.default.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
|
|
6662
7281
|
} catch {
|
|
6663
7282
|
}
|
|
6664
7283
|
return [];
|
|
6665
7284
|
}
|
|
6666
7285
|
function writeStack(stack) {
|
|
6667
|
-
const dir =
|
|
6668
|
-
if (!
|
|
6669
|
-
|
|
7286
|
+
const dir = import_path18.default.dirname(SNAPSHOT_STACK_PATH);
|
|
7287
|
+
if (!import_fs16.default.existsSync(dir)) import_fs16.default.mkdirSync(dir, { recursive: true });
|
|
7288
|
+
import_fs16.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
6670
7289
|
}
|
|
6671
7290
|
function buildArgsSummary(tool, args) {
|
|
6672
7291
|
if (!args || typeof args !== "object") return "";
|
|
@@ -6682,7 +7301,7 @@ function buildArgsSummary(tool, args) {
|
|
|
6682
7301
|
function normalizeCwdForHash(cwd) {
|
|
6683
7302
|
let normalized;
|
|
6684
7303
|
try {
|
|
6685
|
-
normalized =
|
|
7304
|
+
normalized = import_fs16.default.realpathSync(cwd);
|
|
6686
7305
|
} catch {
|
|
6687
7306
|
normalized = cwd;
|
|
6688
7307
|
}
|
|
@@ -6691,17 +7310,17 @@ function normalizeCwdForHash(cwd) {
|
|
|
6691
7310
|
return normalized;
|
|
6692
7311
|
}
|
|
6693
7312
|
function getShadowRepoDir(cwd) {
|
|
6694
|
-
const hash =
|
|
6695
|
-
return
|
|
7313
|
+
const hash = import_crypto6.default.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
|
|
7314
|
+
return import_path18.default.join(import_os14.default.homedir(), ".node9", "snapshots", hash);
|
|
6696
7315
|
}
|
|
6697
7316
|
function cleanOrphanedIndexFiles(shadowDir) {
|
|
6698
7317
|
try {
|
|
6699
7318
|
const cutoff = Date.now() - 6e4;
|
|
6700
|
-
for (const f of
|
|
7319
|
+
for (const f of import_fs16.default.readdirSync(shadowDir)) {
|
|
6701
7320
|
if (f.startsWith("index_")) {
|
|
6702
|
-
const fp =
|
|
7321
|
+
const fp = import_path18.default.join(shadowDir, f);
|
|
6703
7322
|
try {
|
|
6704
|
-
if (
|
|
7323
|
+
if (import_fs16.default.statSync(fp).mtimeMs < cutoff) import_fs16.default.unlinkSync(fp);
|
|
6705
7324
|
} catch {
|
|
6706
7325
|
}
|
|
6707
7326
|
}
|
|
@@ -6713,7 +7332,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
|
|
|
6713
7332
|
const hardcoded = [".git", ".node9"];
|
|
6714
7333
|
const lines = [...hardcoded, ...ignorePaths].join("\n");
|
|
6715
7334
|
try {
|
|
6716
|
-
|
|
7335
|
+
import_fs16.default.writeFileSync(import_path18.default.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
|
|
6717
7336
|
} catch {
|
|
6718
7337
|
}
|
|
6719
7338
|
}
|
|
@@ -6726,25 +7345,25 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
6726
7345
|
timeout: 3e3
|
|
6727
7346
|
});
|
|
6728
7347
|
if (check.status === 0) {
|
|
6729
|
-
const ptPath =
|
|
7348
|
+
const ptPath = import_path18.default.join(shadowDir, "project-path.txt");
|
|
6730
7349
|
try {
|
|
6731
|
-
const stored =
|
|
7350
|
+
const stored = import_fs16.default.readFileSync(ptPath, "utf8").trim();
|
|
6732
7351
|
if (stored === normalizedCwd) return true;
|
|
6733
7352
|
if (process.env.NODE9_DEBUG === "1")
|
|
6734
7353
|
console.error(
|
|
6735
7354
|
`[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
|
|
6736
7355
|
);
|
|
6737
|
-
|
|
7356
|
+
import_fs16.default.rmSync(shadowDir, { recursive: true, force: true });
|
|
6738
7357
|
} catch {
|
|
6739
7358
|
try {
|
|
6740
|
-
|
|
7359
|
+
import_fs16.default.writeFileSync(ptPath, normalizedCwd, "utf8");
|
|
6741
7360
|
} catch {
|
|
6742
7361
|
}
|
|
6743
7362
|
return true;
|
|
6744
7363
|
}
|
|
6745
7364
|
}
|
|
6746
7365
|
try {
|
|
6747
|
-
|
|
7366
|
+
import_fs16.default.mkdirSync(shadowDir, { recursive: true });
|
|
6748
7367
|
} catch {
|
|
6749
7368
|
}
|
|
6750
7369
|
const init = (0, import_child_process8.spawnSync)("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
|
|
@@ -6753,7 +7372,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
6753
7372
|
console.error("[Node9] git init --bare failed:", init.stderr?.toString());
|
|
6754
7373
|
return false;
|
|
6755
7374
|
}
|
|
6756
|
-
const configFile =
|
|
7375
|
+
const configFile = import_path18.default.join(shadowDir, "config");
|
|
6757
7376
|
(0, import_child_process8.spawnSync)("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
|
|
6758
7377
|
timeout: 3e3
|
|
6759
7378
|
});
|
|
@@ -6761,7 +7380,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
6761
7380
|
timeout: 3e3
|
|
6762
7381
|
});
|
|
6763
7382
|
try {
|
|
6764
|
-
|
|
7383
|
+
import_fs16.default.writeFileSync(import_path18.default.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
|
|
6765
7384
|
} catch {
|
|
6766
7385
|
}
|
|
6767
7386
|
return true;
|
|
@@ -6784,7 +7403,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
6784
7403
|
const shadowDir = getShadowRepoDir(cwd);
|
|
6785
7404
|
if (!ensureShadowRepo(shadowDir, cwd)) return null;
|
|
6786
7405
|
writeShadowExcludes(shadowDir, ignorePaths);
|
|
6787
|
-
indexFile =
|
|
7406
|
+
indexFile = import_path18.default.join(shadowDir, `index_${process.pid}_${Date.now()}`);
|
|
6788
7407
|
const shadowEnv = {
|
|
6789
7408
|
...process.env,
|
|
6790
7409
|
GIT_DIR: shadowDir,
|
|
@@ -6813,7 +7432,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
6813
7432
|
const shouldGc = stack.length % 5 === 0;
|
|
6814
7433
|
if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
|
|
6815
7434
|
writeStack(stack);
|
|
6816
|
-
|
|
7435
|
+
import_fs16.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
|
|
6817
7436
|
if (shouldGc) {
|
|
6818
7437
|
(0, import_child_process8.spawn)("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
|
|
6819
7438
|
}
|
|
@@ -6824,7 +7443,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
6824
7443
|
} finally {
|
|
6825
7444
|
if (indexFile) {
|
|
6826
7445
|
try {
|
|
6827
|
-
|
|
7446
|
+
import_fs16.default.unlinkSync(indexFile);
|
|
6828
7447
|
} catch {
|
|
6829
7448
|
}
|
|
6830
7449
|
}
|
|
@@ -6893,9 +7512,9 @@ function applyUndo(hash, cwd) {
|
|
|
6893
7512
|
timeout: GIT_TIMEOUT
|
|
6894
7513
|
}).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
6895
7514
|
for (const file of [...tracked, ...untracked]) {
|
|
6896
|
-
const fullPath =
|
|
6897
|
-
if (!snapshotFiles.has(file) &&
|
|
6898
|
-
|
|
7515
|
+
const fullPath = import_path18.default.join(dir, file);
|
|
7516
|
+
if (!snapshotFiles.has(file) && import_fs16.default.existsSync(fullPath)) {
|
|
7517
|
+
import_fs16.default.unlinkSync(fullPath);
|
|
6899
7518
|
}
|
|
6900
7519
|
}
|
|
6901
7520
|
return true;
|
|
@@ -6919,9 +7538,9 @@ function registerCheckCommand(program2) {
|
|
|
6919
7538
|
} catch (err) {
|
|
6920
7539
|
const tempConfig = getConfig();
|
|
6921
7540
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
6922
|
-
const logPath =
|
|
7541
|
+
const logPath = import_path19.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
|
|
6923
7542
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
6924
|
-
|
|
7543
|
+
import_fs17.default.appendFileSync(
|
|
6925
7544
|
logPath,
|
|
6926
7545
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
6927
7546
|
RAW: ${raw}
|
|
@@ -6932,10 +7551,10 @@ RAW: ${raw}
|
|
|
6932
7551
|
}
|
|
6933
7552
|
const config = getConfig(payload.cwd || void 0);
|
|
6934
7553
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
6935
|
-
const logPath =
|
|
6936
|
-
if (!
|
|
6937
|
-
|
|
6938
|
-
|
|
7554
|
+
const logPath = import_path19.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
|
|
7555
|
+
if (!import_fs17.default.existsSync(import_path19.default.dirname(logPath)))
|
|
7556
|
+
import_fs17.default.mkdirSync(import_path19.default.dirname(logPath), { recursive: true });
|
|
7557
|
+
import_fs17.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
6939
7558
|
`);
|
|
6940
7559
|
}
|
|
6941
7560
|
const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
|
|
@@ -6948,8 +7567,8 @@ RAW: ${raw}
|
|
|
6948
7567
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
6949
7568
|
let ttyFd = null;
|
|
6950
7569
|
try {
|
|
6951
|
-
ttyFd =
|
|
6952
|
-
const writeTty = (line) =>
|
|
7570
|
+
ttyFd = import_fs17.default.openSync("/dev/tty", "w");
|
|
7571
|
+
const writeTty = (line) => import_fs17.default.writeSync(ttyFd, line + "\n");
|
|
6953
7572
|
if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
|
|
6954
7573
|
writeTty(import_chalk5.default.bgRed.white.bold(`
|
|
6955
7574
|
\u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
@@ -6965,7 +7584,7 @@ RAW: ${raw}
|
|
|
6965
7584
|
} finally {
|
|
6966
7585
|
if (ttyFd !== null)
|
|
6967
7586
|
try {
|
|
6968
|
-
|
|
7587
|
+
import_fs17.default.closeSync(ttyFd);
|
|
6969
7588
|
} catch {
|
|
6970
7589
|
}
|
|
6971
7590
|
}
|
|
@@ -6996,7 +7615,7 @@ RAW: ${raw}
|
|
|
6996
7615
|
if (shouldSnapshot(toolName, toolInput, config)) {
|
|
6997
7616
|
await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
|
|
6998
7617
|
}
|
|
6999
|
-
const safeCwdForAuth = typeof payload.cwd === "string" &&
|
|
7618
|
+
const safeCwdForAuth = typeof payload.cwd === "string" && import_path19.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
7000
7619
|
const result = await authorizeHeadless(toolName, toolInput, meta, {
|
|
7001
7620
|
cwd: safeCwdForAuth
|
|
7002
7621
|
});
|
|
@@ -7008,12 +7627,12 @@ RAW: ${raw}
|
|
|
7008
7627
|
}
|
|
7009
7628
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
7010
7629
|
try {
|
|
7011
|
-
const tty =
|
|
7012
|
-
|
|
7630
|
+
const tty = import_fs17.default.openSync("/dev/tty", "w");
|
|
7631
|
+
import_fs17.default.writeSync(
|
|
7013
7632
|
tty,
|
|
7014
7633
|
import_chalk5.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
|
|
7015
7634
|
);
|
|
7016
|
-
|
|
7635
|
+
import_fs17.default.closeSync(tty);
|
|
7017
7636
|
} catch {
|
|
7018
7637
|
}
|
|
7019
7638
|
const daemonReady = await autoStartDaemonAndWait();
|
|
@@ -7040,9 +7659,9 @@ RAW: ${raw}
|
|
|
7040
7659
|
});
|
|
7041
7660
|
} catch (err) {
|
|
7042
7661
|
if (process.env.NODE9_DEBUG === "1") {
|
|
7043
|
-
const logPath =
|
|
7662
|
+
const logPath = import_path19.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
|
|
7044
7663
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
7045
|
-
|
|
7664
|
+
import_fs17.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
7046
7665
|
`);
|
|
7047
7666
|
}
|
|
7048
7667
|
process.exit(0);
|
|
@@ -7076,9 +7695,9 @@ RAW: ${raw}
|
|
|
7076
7695
|
}
|
|
7077
7696
|
|
|
7078
7697
|
// src/cli/commands/log.ts
|
|
7079
|
-
var
|
|
7080
|
-
var
|
|
7081
|
-
var
|
|
7698
|
+
var import_fs18 = __toESM(require("fs"));
|
|
7699
|
+
var import_path20 = __toESM(require("path"));
|
|
7700
|
+
var import_os16 = __toESM(require("os"));
|
|
7082
7701
|
init_audit();
|
|
7083
7702
|
init_config();
|
|
7084
7703
|
init_policy();
|
|
@@ -7100,11 +7719,11 @@ function registerLogCommand(program2) {
|
|
|
7100
7719
|
decision: "allowed",
|
|
7101
7720
|
source: "post-hook"
|
|
7102
7721
|
};
|
|
7103
|
-
const logPath =
|
|
7104
|
-
if (!
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
const safeCwd = typeof payload.cwd === "string" &&
|
|
7722
|
+
const logPath = import_path20.default.join(import_os16.default.homedir(), ".node9", "audit.log");
|
|
7723
|
+
if (!import_fs18.default.existsSync(import_path20.default.dirname(logPath)))
|
|
7724
|
+
import_fs18.default.mkdirSync(import_path20.default.dirname(logPath), { recursive: true });
|
|
7725
|
+
import_fs18.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
7726
|
+
const safeCwd = typeof payload.cwd === "string" && import_path20.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
7108
7727
|
const config = getConfig(safeCwd);
|
|
7109
7728
|
if (shouldSnapshot(tool, {}, config)) {
|
|
7110
7729
|
await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
|
|
@@ -7113,9 +7732,9 @@ function registerLogCommand(program2) {
|
|
|
7113
7732
|
const msg = err instanceof Error ? err.message : String(err);
|
|
7114
7733
|
process.stderr.write(`[Node9] audit log error: ${msg}
|
|
7115
7734
|
`);
|
|
7116
|
-
const debugPath =
|
|
7735
|
+
const debugPath = import_path20.default.join(import_os16.default.homedir(), ".node9", "hook-debug.log");
|
|
7117
7736
|
try {
|
|
7118
|
-
|
|
7737
|
+
import_fs18.default.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
|
|
7119
7738
|
`);
|
|
7120
7739
|
} catch {
|
|
7121
7740
|
}
|
|
@@ -7419,14 +8038,14 @@ function registerConfigShowCommand(program2) {
|
|
|
7419
8038
|
|
|
7420
8039
|
// src/cli/commands/doctor.ts
|
|
7421
8040
|
var import_chalk7 = __toESM(require("chalk"));
|
|
7422
|
-
var
|
|
7423
|
-
var
|
|
7424
|
-
var
|
|
8041
|
+
var import_fs19 = __toESM(require("fs"));
|
|
8042
|
+
var import_path21 = __toESM(require("path"));
|
|
8043
|
+
var import_os17 = __toESM(require("os"));
|
|
7425
8044
|
var import_child_process9 = require("child_process");
|
|
7426
8045
|
init_daemon();
|
|
7427
8046
|
function registerDoctorCommand(program2, version2) {
|
|
7428
8047
|
program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
7429
|
-
const homeDir2 =
|
|
8048
|
+
const homeDir2 = import_os17.default.homedir();
|
|
7430
8049
|
let failures = 0;
|
|
7431
8050
|
function pass(msg) {
|
|
7432
8051
|
console.log(import_chalk7.default.green(" \u2705 ") + msg);
|
|
@@ -7475,10 +8094,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7475
8094
|
);
|
|
7476
8095
|
}
|
|
7477
8096
|
section("Configuration");
|
|
7478
|
-
const globalConfigPath =
|
|
7479
|
-
if (
|
|
8097
|
+
const globalConfigPath = import_path21.default.join(homeDir2, ".node9", "config.json");
|
|
8098
|
+
if (import_fs19.default.existsSync(globalConfigPath)) {
|
|
7480
8099
|
try {
|
|
7481
|
-
JSON.parse(
|
|
8100
|
+
JSON.parse(import_fs19.default.readFileSync(globalConfigPath, "utf-8"));
|
|
7482
8101
|
pass("~/.node9/config.json found and valid");
|
|
7483
8102
|
} catch {
|
|
7484
8103
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -7486,10 +8105,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7486
8105
|
} else {
|
|
7487
8106
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
7488
8107
|
}
|
|
7489
|
-
const projectConfigPath =
|
|
7490
|
-
if (
|
|
8108
|
+
const projectConfigPath = import_path21.default.join(process.cwd(), "node9.config.json");
|
|
8109
|
+
if (import_fs19.default.existsSync(projectConfigPath)) {
|
|
7491
8110
|
try {
|
|
7492
|
-
JSON.parse(
|
|
8111
|
+
JSON.parse(import_fs19.default.readFileSync(projectConfigPath, "utf-8"));
|
|
7493
8112
|
pass("node9.config.json found and valid (project)");
|
|
7494
8113
|
} catch {
|
|
7495
8114
|
fail(
|
|
@@ -7498,8 +8117,8 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7498
8117
|
);
|
|
7499
8118
|
}
|
|
7500
8119
|
}
|
|
7501
|
-
const credsPath =
|
|
7502
|
-
if (
|
|
8120
|
+
const credsPath = import_path21.default.join(homeDir2, ".node9", "credentials.json");
|
|
8121
|
+
if (import_fs19.default.existsSync(credsPath)) {
|
|
7503
8122
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
7504
8123
|
} else {
|
|
7505
8124
|
warn(
|
|
@@ -7508,10 +8127,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7508
8127
|
);
|
|
7509
8128
|
}
|
|
7510
8129
|
section("Agent Hooks");
|
|
7511
|
-
const claudeSettingsPath =
|
|
7512
|
-
if (
|
|
8130
|
+
const claudeSettingsPath = import_path21.default.join(homeDir2, ".claude", "settings.json");
|
|
8131
|
+
if (import_fs19.default.existsSync(claudeSettingsPath)) {
|
|
7513
8132
|
try {
|
|
7514
|
-
const cs = JSON.parse(
|
|
8133
|
+
const cs = JSON.parse(import_fs19.default.readFileSync(claudeSettingsPath, "utf-8"));
|
|
7515
8134
|
const hasHook = cs.hooks?.PreToolUse?.some(
|
|
7516
8135
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
7517
8136
|
);
|
|
@@ -7527,10 +8146,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7527
8146
|
} else {
|
|
7528
8147
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
7529
8148
|
}
|
|
7530
|
-
const geminiSettingsPath =
|
|
7531
|
-
if (
|
|
8149
|
+
const geminiSettingsPath = import_path21.default.join(homeDir2, ".gemini", "settings.json");
|
|
8150
|
+
if (import_fs19.default.existsSync(geminiSettingsPath)) {
|
|
7532
8151
|
try {
|
|
7533
|
-
const gs = JSON.parse(
|
|
8152
|
+
const gs = JSON.parse(import_fs19.default.readFileSync(geminiSettingsPath, "utf-8"));
|
|
7534
8153
|
const hasHook = gs.hooks?.BeforeTool?.some(
|
|
7535
8154
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
7536
8155
|
);
|
|
@@ -7546,10 +8165,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7546
8165
|
} else {
|
|
7547
8166
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
7548
8167
|
}
|
|
7549
|
-
const cursorHooksPath =
|
|
7550
|
-
if (
|
|
8168
|
+
const cursorHooksPath = import_path21.default.join(homeDir2, ".cursor", "hooks.json");
|
|
8169
|
+
if (import_fs19.default.existsSync(cursorHooksPath)) {
|
|
7551
8170
|
try {
|
|
7552
|
-
const cur = JSON.parse(
|
|
8171
|
+
const cur = JSON.parse(import_fs19.default.readFileSync(cursorHooksPath, "utf-8"));
|
|
7553
8172
|
const hasHook = cur.hooks?.preToolUse?.some(
|
|
7554
8173
|
(h) => h.command?.includes("node9") || h.command?.includes("cli.js")
|
|
7555
8174
|
);
|
|
@@ -7587,9 +8206,9 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7587
8206
|
|
|
7588
8207
|
// src/cli/commands/audit.ts
|
|
7589
8208
|
var import_chalk8 = __toESM(require("chalk"));
|
|
7590
|
-
var
|
|
7591
|
-
var
|
|
7592
|
-
var
|
|
8209
|
+
var import_fs20 = __toESM(require("fs"));
|
|
8210
|
+
var import_path22 = __toESM(require("path"));
|
|
8211
|
+
var import_os18 = __toESM(require("os"));
|
|
7593
8212
|
function formatRelativeTime(timestamp) {
|
|
7594
8213
|
const diff = Date.now() - new Date(timestamp).getTime();
|
|
7595
8214
|
const sec = Math.floor(diff / 1e3);
|
|
@@ -7602,14 +8221,14 @@ function formatRelativeTime(timestamp) {
|
|
|
7602
8221
|
}
|
|
7603
8222
|
function registerAuditCommand(program2) {
|
|
7604
8223
|
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) => {
|
|
7605
|
-
const logPath =
|
|
7606
|
-
if (!
|
|
8224
|
+
const logPath = import_path22.default.join(import_os18.default.homedir(), ".node9", "audit.log");
|
|
8225
|
+
if (!import_fs20.default.existsSync(logPath)) {
|
|
7607
8226
|
console.log(
|
|
7608
8227
|
import_chalk8.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
7609
8228
|
);
|
|
7610
8229
|
return;
|
|
7611
8230
|
}
|
|
7612
|
-
const raw =
|
|
8231
|
+
const raw = import_fs20.default.readFileSync(logPath, "utf-8");
|
|
7613
8232
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
7614
8233
|
let entries = lines.flatMap((line) => {
|
|
7615
8234
|
try {
|
|
@@ -7727,11 +8346,44 @@ function registerDaemonCommand(program2) {
|
|
|
7727
8346
|
|
|
7728
8347
|
// src/cli/commands/status.ts
|
|
7729
8348
|
var import_chalk10 = __toESM(require("chalk"));
|
|
7730
|
-
var
|
|
7731
|
-
var
|
|
7732
|
-
var
|
|
8349
|
+
var import_fs21 = __toESM(require("fs"));
|
|
8350
|
+
var import_path23 = __toESM(require("path"));
|
|
8351
|
+
var import_os19 = __toESM(require("os"));
|
|
7733
8352
|
init_core();
|
|
7734
8353
|
init_daemon();
|
|
8354
|
+
function readJson2(filePath) {
|
|
8355
|
+
try {
|
|
8356
|
+
if (import_fs21.default.existsSync(filePath)) return JSON.parse(import_fs21.default.readFileSync(filePath, "utf-8"));
|
|
8357
|
+
} catch {
|
|
8358
|
+
}
|
|
8359
|
+
return null;
|
|
8360
|
+
}
|
|
8361
|
+
function isNode9Hook2(cmd) {
|
|
8362
|
+
if (!cmd) return false;
|
|
8363
|
+
return /(?:^|[\s/\\])node9 (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js (?:check|log)/.test(cmd);
|
|
8364
|
+
}
|
|
8365
|
+
function wrappedMcpServers(servers) {
|
|
8366
|
+
if (!servers) return [];
|
|
8367
|
+
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(" ")}`);
|
|
8368
|
+
}
|
|
8369
|
+
function printAgentSection(label, hookPairs, wrapped) {
|
|
8370
|
+
console.log(import_chalk10.default.bold(` ${label}`));
|
|
8371
|
+
for (const { name, present } of hookPairs) {
|
|
8372
|
+
if (present) {
|
|
8373
|
+
console.log(import_chalk10.default.green(` \u2713 ${name}`));
|
|
8374
|
+
} else {
|
|
8375
|
+
console.log(import_chalk10.default.red(` \u2717 ${name}`) + import_chalk10.default.gray(" (not wired)"));
|
|
8376
|
+
}
|
|
8377
|
+
}
|
|
8378
|
+
if (wrapped.length > 0) {
|
|
8379
|
+
console.log(import_chalk10.default.cyan(` MCP proxied:`));
|
|
8380
|
+
for (const entry of wrapped) {
|
|
8381
|
+
console.log(import_chalk10.default.gray(` \u2022 ${entry}`));
|
|
8382
|
+
}
|
|
8383
|
+
} else {
|
|
8384
|
+
console.log(import_chalk10.default.gray(` MCP proxied: none`));
|
|
8385
|
+
}
|
|
8386
|
+
}
|
|
7735
8387
|
function registerStatusCommand(program2) {
|
|
7736
8388
|
program2.command("status").description("Show current Node9 mode, policy source, and persistent decisions").action(() => {
|
|
7737
8389
|
const creds = getCredentials();
|
|
@@ -7766,19 +8418,72 @@ function registerStatusCommand(program2) {
|
|
|
7766
8418
|
console.log("");
|
|
7767
8419
|
const modeLabel = settings.mode === "audit" ? import_chalk10.default.blue("audit") : settings.mode === "strict" ? import_chalk10.default.red("strict") : import_chalk10.default.white("standard");
|
|
7768
8420
|
console.log(` Mode: ${modeLabel}`);
|
|
7769
|
-
const projectConfig =
|
|
7770
|
-
const globalConfig =
|
|
8421
|
+
const projectConfig = import_path23.default.join(process.cwd(), "node9.config.json");
|
|
8422
|
+
const globalConfig = import_path23.default.join(import_os19.default.homedir(), ".node9", "config.json");
|
|
7771
8423
|
console.log(
|
|
7772
|
-
` Local: ${
|
|
8424
|
+
` Local: ${import_fs21.default.existsSync(projectConfig) ? import_chalk10.default.green("Active (node9.config.json)") : import_chalk10.default.gray("Not present")}`
|
|
7773
8425
|
);
|
|
7774
8426
|
console.log(
|
|
7775
|
-
` Global: ${
|
|
8427
|
+
` Global: ${import_fs21.default.existsSync(globalConfig) ? import_chalk10.default.green("Active (~/.node9/config.json)") : import_chalk10.default.gray("Not present")}`
|
|
7776
8428
|
);
|
|
7777
8429
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
7778
8430
|
console.log(
|
|
7779
8431
|
` Sandbox: ${import_chalk10.default.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
7780
8432
|
);
|
|
7781
8433
|
}
|
|
8434
|
+
const homeDir2 = import_os19.default.homedir();
|
|
8435
|
+
const claudeSettings = readJson2(
|
|
8436
|
+
import_path23.default.join(homeDir2, ".claude", "settings.json")
|
|
8437
|
+
);
|
|
8438
|
+
const claudeConfig = readJson2(import_path23.default.join(homeDir2, ".claude.json"));
|
|
8439
|
+
const geminiSettings = readJson2(
|
|
8440
|
+
import_path23.default.join(homeDir2, ".gemini", "settings.json")
|
|
8441
|
+
);
|
|
8442
|
+
const cursorConfig = readJson2(import_path23.default.join(homeDir2, ".cursor", "mcp.json"));
|
|
8443
|
+
const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
|
|
8444
|
+
if (agentFound) {
|
|
8445
|
+
console.log("");
|
|
8446
|
+
console.log(import_chalk10.default.bold(" Agent Wiring:"));
|
|
8447
|
+
console.log("");
|
|
8448
|
+
if (claudeSettings || claudeConfig) {
|
|
8449
|
+
const preHook = claudeSettings?.hooks?.PreToolUse?.some(
|
|
8450
|
+
(m) => m.hooks.some((h) => isNode9Hook2(h.command))
|
|
8451
|
+
) ?? false;
|
|
8452
|
+
const postHook = claudeSettings?.hooks?.PostToolUse?.some(
|
|
8453
|
+
(m) => m.hooks.some((h) => isNode9Hook2(h.command))
|
|
8454
|
+
) ?? false;
|
|
8455
|
+
printAgentSection(
|
|
8456
|
+
"Claude Code",
|
|
8457
|
+
[
|
|
8458
|
+
{ name: "PreToolUse (node9 check)", present: preHook },
|
|
8459
|
+
{ name: "PostToolUse (node9 log)", present: postHook }
|
|
8460
|
+
],
|
|
8461
|
+
wrappedMcpServers(claudeConfig?.mcpServers)
|
|
8462
|
+
);
|
|
8463
|
+
console.log("");
|
|
8464
|
+
}
|
|
8465
|
+
if (geminiSettings) {
|
|
8466
|
+
const beforeHook = geminiSettings.hooks?.BeforeTool?.some(
|
|
8467
|
+
(m) => m.hooks.some((h) => isNode9Hook2(h.command))
|
|
8468
|
+
) ?? false;
|
|
8469
|
+
const afterHook = geminiSettings.hooks?.AfterTool?.some(
|
|
8470
|
+
(m) => m.hooks.some((h) => isNode9Hook2(h.command))
|
|
8471
|
+
) ?? false;
|
|
8472
|
+
printAgentSection(
|
|
8473
|
+
"Gemini CLI",
|
|
8474
|
+
[
|
|
8475
|
+
{ name: "BeforeTool (node9 check)", present: beforeHook },
|
|
8476
|
+
{ name: "AfterTool (node9 log)", present: afterHook }
|
|
8477
|
+
],
|
|
8478
|
+
wrappedMcpServers(geminiSettings.mcpServers)
|
|
8479
|
+
);
|
|
8480
|
+
console.log("");
|
|
8481
|
+
}
|
|
8482
|
+
if (cursorConfig) {
|
|
8483
|
+
printAgentSection("Cursor", [], wrappedMcpServers(cursorConfig.mcpServers));
|
|
8484
|
+
console.log("");
|
|
8485
|
+
}
|
|
8486
|
+
}
|
|
7782
8487
|
const pauseState = checkPause();
|
|
7783
8488
|
if (pauseState.paused) {
|
|
7784
8489
|
const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
|
|
@@ -7791,8 +8496,63 @@ function registerStatusCommand(program2) {
|
|
|
7791
8496
|
});
|
|
7792
8497
|
}
|
|
7793
8498
|
|
|
7794
|
-
// src/cli/commands/
|
|
8499
|
+
// src/cli/commands/init.ts
|
|
7795
8500
|
var import_chalk11 = __toESM(require("chalk"));
|
|
8501
|
+
var import_fs22 = __toESM(require("fs"));
|
|
8502
|
+
var import_path24 = __toESM(require("path"));
|
|
8503
|
+
var import_os20 = __toESM(require("os"));
|
|
8504
|
+
init_core();
|
|
8505
|
+
function registerInitCommand(program2) {
|
|
8506
|
+
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) => {
|
|
8507
|
+
console.log(import_chalk11.default.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
|
|
8508
|
+
const configPath = import_path24.default.join(import_os20.default.homedir(), ".node9", "config.json");
|
|
8509
|
+
if (import_fs22.default.existsSync(configPath) && !options.force) {
|
|
8510
|
+
console.log(import_chalk11.default.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
8511
|
+
} else {
|
|
8512
|
+
const requestedMode = options.mode.toLowerCase();
|
|
8513
|
+
const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
|
|
8514
|
+
const configToSave = {
|
|
8515
|
+
...DEFAULT_CONFIG,
|
|
8516
|
+
settings: { ...DEFAULT_CONFIG.settings, mode: safeMode }
|
|
8517
|
+
};
|
|
8518
|
+
const dir = import_path24.default.dirname(configPath);
|
|
8519
|
+
if (!import_fs22.default.existsSync(dir)) import_fs22.default.mkdirSync(dir, { recursive: true });
|
|
8520
|
+
import_fs22.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
8521
|
+
console.log(import_chalk11.default.green(`\u2705 Config created: ${configPath}`));
|
|
8522
|
+
console.log(import_chalk11.default.gray(` Mode: ${safeMode}`));
|
|
8523
|
+
}
|
|
8524
|
+
if (options.skipSetup) return;
|
|
8525
|
+
console.log("");
|
|
8526
|
+
const detected = detectAgents();
|
|
8527
|
+
const found = Object.keys(detected).filter(
|
|
8528
|
+
(k) => detected[k]
|
|
8529
|
+
);
|
|
8530
|
+
if (found.length === 0) {
|
|
8531
|
+
console.log(
|
|
8532
|
+
import_chalk11.default.gray("No AI agents detected. Install Claude Code, Gemini CLI, or Cursor")
|
|
8533
|
+
);
|
|
8534
|
+
console.log(import_chalk11.default.gray("then run: node9 addto <claude|gemini|cursor>"));
|
|
8535
|
+
return;
|
|
8536
|
+
}
|
|
8537
|
+
console.log(import_chalk11.default.bold("Detected agents:"));
|
|
8538
|
+
for (const agent of found) {
|
|
8539
|
+
console.log(import_chalk11.default.green(` \u2713 ${agent}`));
|
|
8540
|
+
}
|
|
8541
|
+
console.log("");
|
|
8542
|
+
for (const agent of found) {
|
|
8543
|
+
console.log(import_chalk11.default.bold(`Wiring ${agent}...`));
|
|
8544
|
+
if (agent === "claude") await setupClaude();
|
|
8545
|
+
else if (agent === "gemini") await setupGemini();
|
|
8546
|
+
else if (agent === "cursor") await setupCursor();
|
|
8547
|
+
console.log("");
|
|
8548
|
+
}
|
|
8549
|
+
console.log(import_chalk11.default.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
|
|
8550
|
+
console.log(import_chalk11.default.gray(" Run: node9 daemon start"));
|
|
8551
|
+
});
|
|
8552
|
+
}
|
|
8553
|
+
|
|
8554
|
+
// src/cli/commands/undo.ts
|
|
8555
|
+
var import_chalk12 = __toESM(require("chalk"));
|
|
7796
8556
|
var import_prompts2 = require("@inquirer/prompts");
|
|
7797
8557
|
function registerUndoCommand(program2) {
|
|
7798
8558
|
program2.command("undo").description(
|
|
@@ -7804,22 +8564,22 @@ function registerUndoCommand(program2) {
|
|
|
7804
8564
|
if (history.length === 0) {
|
|
7805
8565
|
if (!options.all && allHistory.length > 0) {
|
|
7806
8566
|
console.log(
|
|
7807
|
-
|
|
8567
|
+
import_chalk12.default.yellow(
|
|
7808
8568
|
`
|
|
7809
8569
|
\u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
|
|
7810
|
-
Run ${
|
|
8570
|
+
Run ${import_chalk12.default.cyan("node9 undo --all")} to see snapshots from all projects.
|
|
7811
8571
|
`
|
|
7812
8572
|
)
|
|
7813
8573
|
);
|
|
7814
8574
|
} else {
|
|
7815
|
-
console.log(
|
|
8575
|
+
console.log(import_chalk12.default.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
|
|
7816
8576
|
}
|
|
7817
8577
|
return;
|
|
7818
8578
|
}
|
|
7819
8579
|
const idx = history.length - steps;
|
|
7820
8580
|
if (idx < 0) {
|
|
7821
8581
|
console.log(
|
|
7822
|
-
|
|
8582
|
+
import_chalk12.default.yellow(
|
|
7823
8583
|
`
|
|
7824
8584
|
\u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
|
|
7825
8585
|
`
|
|
@@ -7831,19 +8591,19 @@ function registerUndoCommand(program2) {
|
|
|
7831
8591
|
const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
|
|
7832
8592
|
const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
|
|
7833
8593
|
console.log(
|
|
7834
|
-
|
|
8594
|
+
import_chalk12.default.magenta.bold(`
|
|
7835
8595
|
\u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`)
|
|
7836
8596
|
);
|
|
7837
8597
|
console.log(
|
|
7838
|
-
|
|
7839
|
-
` Tool: ${
|
|
8598
|
+
import_chalk12.default.white(
|
|
8599
|
+
` Tool: ${import_chalk12.default.cyan(snapshot.tool)}${snapshot.argsSummary ? import_chalk12.default.gray(" \u2192 " + snapshot.argsSummary) : ""}`
|
|
7840
8600
|
)
|
|
7841
8601
|
);
|
|
7842
|
-
console.log(
|
|
7843
|
-
console.log(
|
|
8602
|
+
console.log(import_chalk12.default.white(` When: ${import_chalk12.default.gray(ageStr)}`));
|
|
8603
|
+
console.log(import_chalk12.default.white(` Dir: ${import_chalk12.default.gray(snapshot.cwd)}`));
|
|
7844
8604
|
if (steps > 1)
|
|
7845
8605
|
console.log(
|
|
7846
|
-
|
|
8606
|
+
import_chalk12.default.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
|
|
7847
8607
|
);
|
|
7848
8608
|
console.log("");
|
|
7849
8609
|
const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
|
|
@@ -7851,21 +8611,21 @@ function registerUndoCommand(program2) {
|
|
|
7851
8611
|
const lines = diff.split("\n");
|
|
7852
8612
|
for (const line of lines) {
|
|
7853
8613
|
if (line.startsWith("+++") || line.startsWith("---")) {
|
|
7854
|
-
console.log(
|
|
8614
|
+
console.log(import_chalk12.default.bold(line));
|
|
7855
8615
|
} else if (line.startsWith("+")) {
|
|
7856
|
-
console.log(
|
|
8616
|
+
console.log(import_chalk12.default.green(line));
|
|
7857
8617
|
} else if (line.startsWith("-")) {
|
|
7858
|
-
console.log(
|
|
8618
|
+
console.log(import_chalk12.default.red(line));
|
|
7859
8619
|
} else if (line.startsWith("@@")) {
|
|
7860
|
-
console.log(
|
|
8620
|
+
console.log(import_chalk12.default.cyan(line));
|
|
7861
8621
|
} else {
|
|
7862
|
-
console.log(
|
|
8622
|
+
console.log(import_chalk12.default.gray(line));
|
|
7863
8623
|
}
|
|
7864
8624
|
}
|
|
7865
8625
|
console.log("");
|
|
7866
8626
|
} else {
|
|
7867
8627
|
console.log(
|
|
7868
|
-
|
|
8628
|
+
import_chalk12.default.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
|
|
7869
8629
|
);
|
|
7870
8630
|
}
|
|
7871
8631
|
const proceed = await (0, import_prompts2.confirm)({
|
|
@@ -7874,18 +8634,18 @@ function registerUndoCommand(program2) {
|
|
|
7874
8634
|
});
|
|
7875
8635
|
if (proceed) {
|
|
7876
8636
|
if (applyUndo(snapshot.hash, snapshot.cwd)) {
|
|
7877
|
-
console.log(
|
|
8637
|
+
console.log(import_chalk12.default.green("\n\u2705 Reverted successfully.\n"));
|
|
7878
8638
|
} else {
|
|
7879
|
-
console.error(
|
|
8639
|
+
console.error(import_chalk12.default.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
|
|
7880
8640
|
}
|
|
7881
8641
|
} else {
|
|
7882
|
-
console.log(
|
|
8642
|
+
console.log(import_chalk12.default.gray("\nCancelled.\n"));
|
|
7883
8643
|
}
|
|
7884
8644
|
});
|
|
7885
8645
|
}
|
|
7886
8646
|
|
|
7887
8647
|
// src/cli/commands/watch.ts
|
|
7888
|
-
var
|
|
8648
|
+
var import_chalk13 = __toESM(require("chalk"));
|
|
7889
8649
|
var import_child_process11 = require("child_process");
|
|
7890
8650
|
init_daemon();
|
|
7891
8651
|
function registerWatchCommand(program2) {
|
|
@@ -7902,7 +8662,7 @@ function registerWatchCommand(program2) {
|
|
|
7902
8662
|
throw new Error("not running");
|
|
7903
8663
|
}
|
|
7904
8664
|
} catch {
|
|
7905
|
-
console.error(
|
|
8665
|
+
console.error(import_chalk13.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
|
|
7906
8666
|
const child = (0, import_child_process11.spawn)(process.execPath, [process.argv[1], "daemon"], {
|
|
7907
8667
|
detached: true,
|
|
7908
8668
|
stdio: "ignore",
|
|
@@ -7924,12 +8684,12 @@ function registerWatchCommand(program2) {
|
|
|
7924
8684
|
}
|
|
7925
8685
|
}
|
|
7926
8686
|
if (!ready) {
|
|
7927
|
-
console.error(
|
|
8687
|
+
console.error(import_chalk13.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
7928
8688
|
process.exit(1);
|
|
7929
8689
|
}
|
|
7930
8690
|
}
|
|
7931
8691
|
console.error(
|
|
7932
|
-
|
|
8692
|
+
import_chalk13.default.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + import_chalk13.default.dim(` \u2192 localhost:${port}`) + import_chalk13.default.dim(
|
|
7933
8693
|
"\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
|
|
7934
8694
|
)
|
|
7935
8695
|
);
|
|
@@ -7938,7 +8698,7 @@ function registerWatchCommand(program2) {
|
|
|
7938
8698
|
env: { ...process.env, NODE9_WATCH_MODE: "1" }
|
|
7939
8699
|
});
|
|
7940
8700
|
if (result.error) {
|
|
7941
|
-
console.error(
|
|
8701
|
+
console.error(import_chalk13.default.red(`\u274C Failed to run command: ${result.error.message}`));
|
|
7942
8702
|
process.exit(1);
|
|
7943
8703
|
}
|
|
7944
8704
|
process.exit(result.status ?? 0);
|
|
@@ -7947,7 +8707,7 @@ function registerWatchCommand(program2) {
|
|
|
7947
8707
|
|
|
7948
8708
|
// src/mcp-gateway/index.ts
|
|
7949
8709
|
var import_readline2 = __toESM(require("readline"));
|
|
7950
|
-
var
|
|
8710
|
+
var import_chalk14 = __toESM(require("chalk"));
|
|
7951
8711
|
var import_child_process12 = require("child_process");
|
|
7952
8712
|
var import_execa3 = require("execa");
|
|
7953
8713
|
init_orchestrator();
|
|
@@ -8011,13 +8771,13 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
8011
8771
|
const prov = checkProvenance(executable);
|
|
8012
8772
|
if (prov.trustLevel === "suspect") {
|
|
8013
8773
|
console.error(
|
|
8014
|
-
|
|
8774
|
+
import_chalk14.default.red(
|
|
8015
8775
|
`\u26A0\uFE0F Node9: Upstream MCP server binary is suspect \u2014 ${prov.reason} (${prov.resolvedPath})`
|
|
8016
8776
|
)
|
|
8017
8777
|
);
|
|
8018
|
-
console.error(
|
|
8778
|
+
console.error(import_chalk14.default.red(" Verify this binary is trusted before proceeding."));
|
|
8019
8779
|
}
|
|
8020
|
-
console.error(
|
|
8780
|
+
console.error(import_chalk14.default.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
|
|
8021
8781
|
const UPSTREAM_INJECTOR_VARS = /* @__PURE__ */ new Set([
|
|
8022
8782
|
"NODE_OPTIONS",
|
|
8023
8783
|
"NODE_PATH",
|
|
@@ -8081,10 +8841,10 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
8081
8841
|
mcpServer
|
|
8082
8842
|
});
|
|
8083
8843
|
if (!result.approved) {
|
|
8084
|
-
console.error(
|
|
8844
|
+
console.error(import_chalk14.default.red(`
|
|
8085
8845
|
\u{1F6D1} Node9 MCP Gateway: Action Blocked`));
|
|
8086
|
-
console.error(
|
|
8087
|
-
console.error(
|
|
8846
|
+
console.error(import_chalk14.default.gray(` Tool: ${toolName}`));
|
|
8847
|
+
console.error(import_chalk14.default.gray(` Reason: ${result.reason ?? "Security Policy"}
|
|
8088
8848
|
`));
|
|
8089
8849
|
const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
|
|
8090
8850
|
const isHumanDecision = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
|
|
@@ -8157,7 +8917,7 @@ function registerMcpGatewayCommand(program2) {
|
|
|
8157
8917
|
}
|
|
8158
8918
|
|
|
8159
8919
|
// src/cli/commands/trust.ts
|
|
8160
|
-
var
|
|
8920
|
+
var import_chalk15 = __toESM(require("chalk"));
|
|
8161
8921
|
init_trusted_hosts();
|
|
8162
8922
|
function isValidHost(host) {
|
|
8163
8923
|
return /^(\*\.)?[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/.test(host);
|
|
@@ -8168,44 +8928,44 @@ function registerTrustCommand(program2) {
|
|
|
8168
8928
|
const normalized = normalizeHost(host.trim());
|
|
8169
8929
|
if (!isValidHost(normalized)) {
|
|
8170
8930
|
console.error(
|
|
8171
|
-
|
|
8931
|
+
import_chalk15.default.red(`
|
|
8172
8932
|
\u274C Invalid host: "${host}"
|
|
8173
|
-
`) +
|
|
8933
|
+
`) + import_chalk15.default.gray(" Use an FQDN like api.mycompany.com or *.mycompany.com\n")
|
|
8174
8934
|
);
|
|
8175
8935
|
process.exit(1);
|
|
8176
8936
|
}
|
|
8177
8937
|
addTrustedHost(normalized);
|
|
8178
|
-
console.log(
|
|
8938
|
+
console.log(import_chalk15.default.green(`
|
|
8179
8939
|
\u2705 ${normalized} added to trusted hosts.`));
|
|
8180
8940
|
console.log(
|
|
8181
|
-
|
|
8941
|
+
import_chalk15.default.gray(" Pipe-chain blocks to this host: critical \u2192 review, high \u2192 allow\n")
|
|
8182
8942
|
);
|
|
8183
8943
|
});
|
|
8184
8944
|
trustCmd.command("remove <host>").description("Remove a trusted host").action((host) => {
|
|
8185
8945
|
const normalized = normalizeHost(host.trim());
|
|
8186
8946
|
const removed = removeTrustedHost(normalized);
|
|
8187
8947
|
if (!removed) {
|
|
8188
|
-
console.error(
|
|
8948
|
+
console.error(import_chalk15.default.yellow(`
|
|
8189
8949
|
\u26A0\uFE0F "${normalized}" is not in the trusted hosts list.
|
|
8190
8950
|
`));
|
|
8191
8951
|
process.exit(1);
|
|
8192
8952
|
}
|
|
8193
|
-
console.log(
|
|
8953
|
+
console.log(import_chalk15.default.green(`
|
|
8194
8954
|
\u2705 ${normalized} removed from trusted hosts.
|
|
8195
8955
|
`));
|
|
8196
8956
|
});
|
|
8197
8957
|
trustCmd.command("list").description("Show all trusted hosts").action(() => {
|
|
8198
8958
|
const hosts = readTrustedHosts();
|
|
8199
8959
|
if (hosts.length === 0) {
|
|
8200
|
-
console.log(
|
|
8201
|
-
console.log(` Add one: ${
|
|
8960
|
+
console.log(import_chalk15.default.gray("\n No trusted hosts configured.\n"));
|
|
8961
|
+
console.log(` Add one: ${import_chalk15.default.cyan("node9 trust add api.mycompany.com")}
|
|
8202
8962
|
`);
|
|
8203
8963
|
return;
|
|
8204
8964
|
}
|
|
8205
|
-
console.log(
|
|
8965
|
+
console.log(import_chalk15.default.bold("\n\u{1F513} Trusted Hosts\n"));
|
|
8206
8966
|
for (const entry of hosts) {
|
|
8207
8967
|
const date = new Date(entry.addedAt).toLocaleDateString();
|
|
8208
|
-
console.log(` ${
|
|
8968
|
+
console.log(` ${import_chalk15.default.cyan(entry.host.padEnd(40))} ${import_chalk15.default.gray(`added ${date}`)}`);
|
|
8209
8969
|
}
|
|
8210
8970
|
console.log("");
|
|
8211
8971
|
});
|
|
@@ -8213,20 +8973,20 @@ function registerTrustCommand(program2) {
|
|
|
8213
8973
|
|
|
8214
8974
|
// src/cli.ts
|
|
8215
8975
|
var { version } = JSON.parse(
|
|
8216
|
-
|
|
8976
|
+
import_fs24.default.readFileSync(import_path26.default.join(__dirname, "../package.json"), "utf-8")
|
|
8217
8977
|
);
|
|
8218
8978
|
var program = new import_commander.Command();
|
|
8219
8979
|
program.name("node9").description("The Sudo Command for AI Agents").version(version);
|
|
8220
8980
|
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) => {
|
|
8221
8981
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
8222
|
-
const credPath =
|
|
8223
|
-
if (!
|
|
8224
|
-
|
|
8982
|
+
const credPath = import_path26.default.join(import_os22.default.homedir(), ".node9", "credentials.json");
|
|
8983
|
+
if (!import_fs24.default.existsSync(import_path26.default.dirname(credPath)))
|
|
8984
|
+
import_fs24.default.mkdirSync(import_path26.default.dirname(credPath), { recursive: true });
|
|
8225
8985
|
const profileName = options.profile || "default";
|
|
8226
8986
|
let existingCreds = {};
|
|
8227
8987
|
try {
|
|
8228
|
-
if (
|
|
8229
|
-
const raw = JSON.parse(
|
|
8988
|
+
if (import_fs24.default.existsSync(credPath)) {
|
|
8989
|
+
const raw = JSON.parse(import_fs24.default.readFileSync(credPath, "utf-8"));
|
|
8230
8990
|
if (raw.apiKey) {
|
|
8231
8991
|
existingCreds = {
|
|
8232
8992
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
|
|
@@ -8238,13 +8998,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
8238
8998
|
} catch {
|
|
8239
8999
|
}
|
|
8240
9000
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
8241
|
-
|
|
9001
|
+
import_fs24.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
8242
9002
|
if (profileName === "default") {
|
|
8243
|
-
const configPath =
|
|
9003
|
+
const configPath = import_path26.default.join(import_os22.default.homedir(), ".node9", "config.json");
|
|
8244
9004
|
let config = {};
|
|
8245
9005
|
try {
|
|
8246
|
-
if (
|
|
8247
|
-
config = JSON.parse(
|
|
9006
|
+
if (import_fs24.default.existsSync(configPath))
|
|
9007
|
+
config = JSON.parse(import_fs24.default.readFileSync(configPath, "utf-8"));
|
|
8248
9008
|
} catch {
|
|
8249
9009
|
}
|
|
8250
9010
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -8259,36 +9019,36 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
8259
9019
|
approvers.cloud = false;
|
|
8260
9020
|
}
|
|
8261
9021
|
s.approvers = approvers;
|
|
8262
|
-
if (!
|
|
8263
|
-
|
|
8264
|
-
|
|
9022
|
+
if (!import_fs24.default.existsSync(import_path26.default.dirname(configPath)))
|
|
9023
|
+
import_fs24.default.mkdirSync(import_path26.default.dirname(configPath), { recursive: true });
|
|
9024
|
+
import_fs24.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
8265
9025
|
}
|
|
8266
9026
|
if (options.profile && profileName !== "default") {
|
|
8267
|
-
console.log(
|
|
8268
|
-
console.log(
|
|
9027
|
+
console.log(import_chalk17.default.green(`\u2705 Profile "${profileName}" saved`));
|
|
9028
|
+
console.log(import_chalk17.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
8269
9029
|
} else if (options.local) {
|
|
8270
|
-
console.log(
|
|
8271
|
-
console.log(
|
|
9030
|
+
console.log(import_chalk17.default.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
9031
|
+
console.log(import_chalk17.default.gray(` All decisions stay on this machine.`));
|
|
8272
9032
|
} else {
|
|
8273
|
-
console.log(
|
|
8274
|
-
console.log(
|
|
9033
|
+
console.log(import_chalk17.default.green(`\u2705 Logged in \u2014 agent mode`));
|
|
9034
|
+
console.log(import_chalk17.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
8275
9035
|
}
|
|
8276
9036
|
});
|
|
8277
9037
|
program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to protect: claude | gemini | cursor").action(async (target) => {
|
|
8278
9038
|
if (target === "gemini") return await setupGemini();
|
|
8279
9039
|
if (target === "claude") return await setupClaude();
|
|
8280
9040
|
if (target === "cursor") return await setupCursor();
|
|
8281
|
-
console.error(
|
|
9041
|
+
console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
8282
9042
|
process.exit(1);
|
|
8283
9043
|
});
|
|
8284
9044
|
program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor").argument("[target]", "The agent to protect: claude | gemini | cursor").action(async (target) => {
|
|
8285
9045
|
if (!target) {
|
|
8286
|
-
console.log(
|
|
8287
|
-
console.log(" Usage: " +
|
|
9046
|
+
console.log(import_chalk17.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
9047
|
+
console.log(" Usage: " + import_chalk17.default.white("node9 setup <target>") + "\n");
|
|
8288
9048
|
console.log(" Targets:");
|
|
8289
|
-
console.log(" " +
|
|
8290
|
-
console.log(" " +
|
|
8291
|
-
console.log(" " +
|
|
9049
|
+
console.log(" " + import_chalk17.default.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
9050
|
+
console.log(" " + import_chalk17.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
9051
|
+
console.log(" " + import_chalk17.default.green("cursor") + " \u2014 Cursor (hook mode)");
|
|
8292
9052
|
console.log("");
|
|
8293
9053
|
return;
|
|
8294
9054
|
}
|
|
@@ -8296,7 +9056,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
8296
9056
|
if (t === "gemini") return await setupGemini();
|
|
8297
9057
|
if (t === "claude") return await setupClaude();
|
|
8298
9058
|
if (t === "cursor") return await setupCursor();
|
|
8299
|
-
console.error(
|
|
9059
|
+
console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
8300
9060
|
process.exit(1);
|
|
8301
9061
|
});
|
|
8302
9062
|
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) => {
|
|
@@ -8305,30 +9065,30 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
|
|
|
8305
9065
|
else if (target === "gemini") fn = teardownGemini;
|
|
8306
9066
|
else if (target === "cursor") fn = teardownCursor;
|
|
8307
9067
|
else {
|
|
8308
|
-
console.error(
|
|
9068
|
+
console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
8309
9069
|
process.exit(1);
|
|
8310
9070
|
}
|
|
8311
|
-
console.log(
|
|
9071
|
+
console.log(import_chalk17.default.cyan(`
|
|
8312
9072
|
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
8313
9073
|
`));
|
|
8314
9074
|
try {
|
|
8315
9075
|
fn();
|
|
8316
9076
|
} catch (err) {
|
|
8317
|
-
console.error(
|
|
9077
|
+
console.error(import_chalk17.default.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
8318
9078
|
process.exit(1);
|
|
8319
9079
|
}
|
|
8320
|
-
console.log(
|
|
9080
|
+
console.log(import_chalk17.default.gray("\n Restart the agent for changes to take effect."));
|
|
8321
9081
|
});
|
|
8322
9082
|
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) => {
|
|
8323
|
-
console.log(
|
|
8324
|
-
console.log(
|
|
9083
|
+
console.log(import_chalk17.default.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
9084
|
+
console.log(import_chalk17.default.bold("Stopping daemon..."));
|
|
8325
9085
|
try {
|
|
8326
9086
|
stopDaemon();
|
|
8327
|
-
console.log(
|
|
9087
|
+
console.log(import_chalk17.default.green(" \u2705 Daemon stopped"));
|
|
8328
9088
|
} catch {
|
|
8329
|
-
console.log(
|
|
9089
|
+
console.log(import_chalk17.default.blue(" \u2139\uFE0F Daemon was not running"));
|
|
8330
9090
|
}
|
|
8331
|
-
console.log(
|
|
9091
|
+
console.log(import_chalk17.default.bold("\nRemoving hooks..."));
|
|
8332
9092
|
let teardownFailed = false;
|
|
8333
9093
|
for (const [label, fn] of [
|
|
8334
9094
|
["Claude", teardownClaude],
|
|
@@ -8340,45 +9100,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
8340
9100
|
} catch (err) {
|
|
8341
9101
|
teardownFailed = true;
|
|
8342
9102
|
console.error(
|
|
8343
|
-
|
|
9103
|
+
import_chalk17.default.red(
|
|
8344
9104
|
` \u26A0\uFE0F Failed to remove ${label} hooks: ${err instanceof Error ? err.message : String(err)}`
|
|
8345
9105
|
)
|
|
8346
9106
|
);
|
|
8347
9107
|
}
|
|
8348
9108
|
}
|
|
8349
9109
|
if (options.purge) {
|
|
8350
|
-
const node9Dir =
|
|
8351
|
-
if (
|
|
9110
|
+
const node9Dir = import_path26.default.join(import_os22.default.homedir(), ".node9");
|
|
9111
|
+
if (import_fs24.default.existsSync(node9Dir)) {
|
|
8352
9112
|
const confirmed = await (0, import_prompts3.confirm)({
|
|
8353
9113
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
8354
9114
|
default: false
|
|
8355
9115
|
});
|
|
8356
9116
|
if (confirmed) {
|
|
8357
|
-
|
|
8358
|
-
if (
|
|
9117
|
+
import_fs24.default.rmSync(node9Dir, { recursive: true });
|
|
9118
|
+
if (import_fs24.default.existsSync(node9Dir)) {
|
|
8359
9119
|
console.error(
|
|
8360
|
-
|
|
9120
|
+
import_chalk17.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
8361
9121
|
);
|
|
8362
9122
|
} else {
|
|
8363
|
-
console.log(
|
|
9123
|
+
console.log(import_chalk17.default.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
8364
9124
|
}
|
|
8365
9125
|
} else {
|
|
8366
|
-
console.log(
|
|
9126
|
+
console.log(import_chalk17.default.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
8367
9127
|
}
|
|
8368
9128
|
} else {
|
|
8369
|
-
console.log(
|
|
9129
|
+
console.log(import_chalk17.default.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
8370
9130
|
}
|
|
8371
9131
|
} else {
|
|
8372
9132
|
console.log(
|
|
8373
|
-
|
|
9133
|
+
import_chalk17.default.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
8374
9134
|
);
|
|
8375
9135
|
}
|
|
8376
9136
|
if (teardownFailed) {
|
|
8377
|
-
console.error(
|
|
9137
|
+
console.error(import_chalk17.default.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
8378
9138
|
process.exit(1);
|
|
8379
9139
|
}
|
|
8380
|
-
console.log(
|
|
8381
|
-
console.log(
|
|
9140
|
+
console.log(import_chalk17.default.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
|
|
9141
|
+
console.log(import_chalk17.default.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
|
|
8382
9142
|
});
|
|
8383
9143
|
registerDoctorCommand(program, version);
|
|
8384
9144
|
program.command("explain").description(
|
|
@@ -8391,7 +9151,7 @@ program.command("explain").description(
|
|
|
8391
9151
|
try {
|
|
8392
9152
|
args = JSON.parse(trimmed);
|
|
8393
9153
|
} catch {
|
|
8394
|
-
console.error(
|
|
9154
|
+
console.error(import_chalk17.default.red(`
|
|
8395
9155
|
\u274C Invalid JSON: ${trimmed}
|
|
8396
9156
|
`));
|
|
8397
9157
|
process.exit(1);
|
|
@@ -8402,83 +9162,59 @@ program.command("explain").description(
|
|
|
8402
9162
|
}
|
|
8403
9163
|
const result = await explainPolicy(tool, args);
|
|
8404
9164
|
console.log("");
|
|
8405
|
-
console.log(
|
|
9165
|
+
console.log(import_chalk17.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
8406
9166
|
console.log("");
|
|
8407
|
-
console.log(` ${
|
|
9167
|
+
console.log(` ${import_chalk17.default.bold("Tool:")} ${import_chalk17.default.white(result.tool)}`);
|
|
8408
9168
|
if (argsRaw) {
|
|
8409
9169
|
const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
8410
|
-
console.log(` ${
|
|
9170
|
+
console.log(` ${import_chalk17.default.bold("Input:")} ${import_chalk17.default.gray(preview)}`);
|
|
8411
9171
|
}
|
|
8412
9172
|
console.log("");
|
|
8413
|
-
console.log(
|
|
9173
|
+
console.log(import_chalk17.default.bold("Config Sources (Waterfall):"));
|
|
8414
9174
|
for (const tier of result.waterfall) {
|
|
8415
|
-
const num =
|
|
9175
|
+
const num = import_chalk17.default.gray(` ${tier.tier}.`);
|
|
8416
9176
|
const label = tier.label.padEnd(16);
|
|
8417
9177
|
let statusStr;
|
|
8418
9178
|
if (tier.tier === 1) {
|
|
8419
|
-
statusStr =
|
|
9179
|
+
statusStr = import_chalk17.default.gray(tier.note ?? "");
|
|
8420
9180
|
} else if (tier.status === "active") {
|
|
8421
|
-
const loc = tier.path ?
|
|
8422
|
-
const note = tier.note ?
|
|
8423
|
-
statusStr =
|
|
9181
|
+
const loc = tier.path ? import_chalk17.default.gray(tier.path) : "";
|
|
9182
|
+
const note = tier.note ? import_chalk17.default.gray(`(${tier.note})`) : "";
|
|
9183
|
+
statusStr = import_chalk17.default.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
|
|
8424
9184
|
} else {
|
|
8425
|
-
statusStr =
|
|
9185
|
+
statusStr = import_chalk17.default.gray("\u25CB " + (tier.note ?? "not found"));
|
|
8426
9186
|
}
|
|
8427
|
-
console.log(`${num} ${
|
|
9187
|
+
console.log(`${num} ${import_chalk17.default.white(label)} ${statusStr}`);
|
|
8428
9188
|
}
|
|
8429
9189
|
console.log("");
|
|
8430
|
-
console.log(
|
|
9190
|
+
console.log(import_chalk17.default.bold("Policy Evaluation:"));
|
|
8431
9191
|
for (const step of result.steps) {
|
|
8432
9192
|
const isFinal = step.isFinal;
|
|
8433
9193
|
let icon;
|
|
8434
|
-
if (step.outcome === "allow") icon =
|
|
8435
|
-
else if (step.outcome === "review") icon =
|
|
8436
|
-
else if (step.outcome === "skip") icon =
|
|
8437
|
-
else icon =
|
|
9194
|
+
if (step.outcome === "allow") icon = import_chalk17.default.green(" \u2705");
|
|
9195
|
+
else if (step.outcome === "review") icon = import_chalk17.default.red(" \u{1F534}");
|
|
9196
|
+
else if (step.outcome === "skip") icon = import_chalk17.default.gray(" \u2500 ");
|
|
9197
|
+
else icon = import_chalk17.default.gray(" \u25CB ");
|
|
8438
9198
|
const name = step.name.padEnd(18);
|
|
8439
|
-
const nameStr = isFinal ?
|
|
8440
|
-
const detail = isFinal ?
|
|
8441
|
-
const arrow = isFinal ?
|
|
9199
|
+
const nameStr = isFinal ? import_chalk17.default.white.bold(name) : import_chalk17.default.white(name);
|
|
9200
|
+
const detail = isFinal ? import_chalk17.default.white(step.detail) : import_chalk17.default.gray(step.detail);
|
|
9201
|
+
const arrow = isFinal ? import_chalk17.default.yellow(" \u2190 STOP") : "";
|
|
8442
9202
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
8443
9203
|
}
|
|
8444
9204
|
console.log("");
|
|
8445
9205
|
if (result.decision === "allow") {
|
|
8446
|
-
console.log(
|
|
9206
|
+
console.log(import_chalk17.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk17.default.gray(" \u2014 no approval needed"));
|
|
8447
9207
|
} else {
|
|
8448
9208
|
console.log(
|
|
8449
|
-
|
|
9209
|
+
import_chalk17.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk17.default.gray(" \u2014 human approval required")
|
|
8450
9210
|
);
|
|
8451
9211
|
if (result.blockedByLabel) {
|
|
8452
|
-
console.log(
|
|
9212
|
+
console.log(import_chalk17.default.gray(` Reason: ${result.blockedByLabel}`));
|
|
8453
9213
|
}
|
|
8454
9214
|
}
|
|
8455
9215
|
console.log("");
|
|
8456
9216
|
});
|
|
8457
|
-
program
|
|
8458
|
-
const configPath = import_path24.default.join(import_os20.default.homedir(), ".node9", "config.json");
|
|
8459
|
-
if (import_fs22.default.existsSync(configPath) && !options.force) {
|
|
8460
|
-
console.log(import_chalk16.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
8461
|
-
console.log(import_chalk16.default.gray(` Run with --force to overwrite.`));
|
|
8462
|
-
return;
|
|
8463
|
-
}
|
|
8464
|
-
const requestedMode = options.mode.toLowerCase();
|
|
8465
|
-
const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
|
|
8466
|
-
const configToSave = {
|
|
8467
|
-
...DEFAULT_CONFIG,
|
|
8468
|
-
settings: {
|
|
8469
|
-
...DEFAULT_CONFIG.settings,
|
|
8470
|
-
mode: safeMode
|
|
8471
|
-
}
|
|
8472
|
-
};
|
|
8473
|
-
const dir = import_path24.default.dirname(configPath);
|
|
8474
|
-
if (!import_fs22.default.existsSync(dir)) import_fs22.default.mkdirSync(dir, { recursive: true });
|
|
8475
|
-
import_fs22.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
8476
|
-
console.log(import_chalk16.default.green(`\u2705 Global config created: ${configPath}`));
|
|
8477
|
-
console.log(import_chalk16.default.cyan(` Mode set to: ${safeMode}`));
|
|
8478
|
-
console.log(
|
|
8479
|
-
import_chalk16.default.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
|
|
8480
|
-
);
|
|
8481
|
-
});
|
|
9217
|
+
registerInitCommand(program);
|
|
8482
9218
|
registerAuditCommand(program);
|
|
8483
9219
|
registerStatusCommand(program);
|
|
8484
9220
|
registerDaemonCommand(program);
|
|
@@ -8487,7 +9223,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
|
|
|
8487
9223
|
try {
|
|
8488
9224
|
await startTail2(options);
|
|
8489
9225
|
} catch (err) {
|
|
8490
|
-
console.error(
|
|
9226
|
+
console.error(import_chalk17.default.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
|
|
8491
9227
|
process.exit(1);
|
|
8492
9228
|
}
|
|
8493
9229
|
});
|
|
@@ -8499,7 +9235,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
8499
9235
|
const ms = parseDuration(options.duration);
|
|
8500
9236
|
if (ms === null) {
|
|
8501
9237
|
console.error(
|
|
8502
|
-
|
|
9238
|
+
import_chalk17.default.red(`
|
|
8503
9239
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
8504
9240
|
`)
|
|
8505
9241
|
);
|
|
@@ -8507,20 +9243,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
8507
9243
|
}
|
|
8508
9244
|
pauseNode9(ms, options.duration);
|
|
8509
9245
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
8510
|
-
console.log(
|
|
9246
|
+
console.log(import_chalk17.default.yellow(`
|
|
8511
9247
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
8512
|
-
console.log(
|
|
8513
|
-
console.log(
|
|
9248
|
+
console.log(import_chalk17.default.gray(` All tool calls will be allowed without review.`));
|
|
9249
|
+
console.log(import_chalk17.default.gray(` Run "node9 resume" to re-enable early.
|
|
8514
9250
|
`));
|
|
8515
9251
|
});
|
|
8516
9252
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
8517
9253
|
const { paused } = checkPause();
|
|
8518
9254
|
if (!paused) {
|
|
8519
|
-
console.log(
|
|
9255
|
+
console.log(import_chalk17.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
8520
9256
|
return;
|
|
8521
9257
|
}
|
|
8522
9258
|
resumeNode9();
|
|
8523
|
-
console.log(
|
|
9259
|
+
console.log(import_chalk17.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
8524
9260
|
});
|
|
8525
9261
|
var HOOK_BASED_AGENTS = {
|
|
8526
9262
|
claude: "claude",
|
|
@@ -8533,15 +9269,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
8533
9269
|
if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
|
|
8534
9270
|
const target = HOOK_BASED_AGENTS[firstArg2];
|
|
8535
9271
|
console.error(
|
|
8536
|
-
|
|
9272
|
+
import_chalk17.default.yellow(`
|
|
8537
9273
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
8538
9274
|
);
|
|
8539
|
-
console.error(
|
|
9275
|
+
console.error(import_chalk17.default.white(`
|
|
8540
9276
|
"${target}" uses its own hook system. Use:`));
|
|
8541
9277
|
console.error(
|
|
8542
|
-
|
|
9278
|
+
import_chalk17.default.green(` node9 addto ${target} `) + import_chalk17.default.gray("# one-time setup")
|
|
8543
9279
|
);
|
|
8544
|
-
console.error(
|
|
9280
|
+
console.error(import_chalk17.default.green(` ${target} `) + import_chalk17.default.gray("# run normally"));
|
|
8545
9281
|
process.exit(1);
|
|
8546
9282
|
}
|
|
8547
9283
|
const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
|
|
@@ -8558,7 +9294,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
8558
9294
|
}
|
|
8559
9295
|
);
|
|
8560
9296
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
8561
|
-
console.error(
|
|
9297
|
+
console.error(import_chalk17.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
8562
9298
|
const daemonReady = await autoStartDaemonAndWait();
|
|
8563
9299
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
8564
9300
|
}
|
|
@@ -8571,12 +9307,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
8571
9307
|
}
|
|
8572
9308
|
if (!result.approved) {
|
|
8573
9309
|
console.error(
|
|
8574
|
-
|
|
9310
|
+
import_chalk17.default.red(`
|
|
8575
9311
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
8576
9312
|
);
|
|
8577
9313
|
process.exit(1);
|
|
8578
9314
|
}
|
|
8579
|
-
console.error(
|
|
9315
|
+
console.error(import_chalk17.default.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
8580
9316
|
await runProxy(fullCommand);
|
|
8581
9317
|
} else {
|
|
8582
9318
|
program.help();
|
|
@@ -8591,9 +9327,9 @@ if (process.argv[2] !== "daemon") {
|
|
|
8591
9327
|
const isCheckHook = process.argv[2] === "check";
|
|
8592
9328
|
if (isCheckHook) {
|
|
8593
9329
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
8594
|
-
const logPath =
|
|
9330
|
+
const logPath = import_path26.default.join(import_os22.default.homedir(), ".node9", "hook-debug.log");
|
|
8595
9331
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
8596
|
-
|
|
9332
|
+
import_fs24.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
8597
9333
|
`);
|
|
8598
9334
|
}
|
|
8599
9335
|
process.exit(0);
|