@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.mjs
CHANGED
|
@@ -94,8 +94,8 @@ function sanitizeConfig(raw) {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
const lines = result.error.issues.map((issue) => {
|
|
97
|
-
const
|
|
98
|
-
return ` \u2022 ${
|
|
97
|
+
const path27 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
98
|
+
return ` \u2022 ${path27}: ${issue.message}`;
|
|
99
99
|
});
|
|
100
100
|
return {
|
|
101
101
|
sanitized,
|
|
@@ -1605,17 +1605,44 @@ function readTrustedHosts() {
|
|
|
1605
1605
|
return [];
|
|
1606
1606
|
}
|
|
1607
1607
|
}
|
|
1608
|
+
function getFileMtime() {
|
|
1609
|
+
try {
|
|
1610
|
+
return fs6.statSync(getTrustedHostsPath()).mtimeMs;
|
|
1611
|
+
} catch {
|
|
1612
|
+
return 0;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
function getCachedHosts() {
|
|
1616
|
+
const now = Date.now();
|
|
1617
|
+
if (_cache && now < _cache.expiry) {
|
|
1618
|
+
const mtime = getFileMtime();
|
|
1619
|
+
if (mtime === _cache.mtime) return _cache.hosts;
|
|
1620
|
+
}
|
|
1621
|
+
const hosts = readTrustedHosts();
|
|
1622
|
+
_cache = { hosts, expiry: now + CACHE_TTL_MS, mtime: getFileMtime() };
|
|
1623
|
+
return hosts;
|
|
1624
|
+
}
|
|
1608
1625
|
function writeTrustedHosts(hosts) {
|
|
1609
1626
|
const filePath = getTrustedHostsPath();
|
|
1610
1627
|
fs6.mkdirSync(path7.dirname(filePath), { recursive: true });
|
|
1611
1628
|
const tmp = filePath + ".node9-tmp";
|
|
1612
|
-
fs6.writeFileSync(tmp, JSON.stringify({ hosts }, null, 2));
|
|
1629
|
+
fs6.writeFileSync(tmp, JSON.stringify({ hosts }, null, 2), { mode: 384 });
|
|
1613
1630
|
fs6.renameSync(tmp, filePath);
|
|
1631
|
+
_cache = { hosts, expiry: Date.now() + CACHE_TTL_MS, mtime: getFileMtime() };
|
|
1614
1632
|
}
|
|
1615
1633
|
function addTrustedHost(host) {
|
|
1634
|
+
const normalized = normalizeHost(host);
|
|
1635
|
+
if (normalized.startsWith("*.")) {
|
|
1636
|
+
const base = normalized.slice(2);
|
|
1637
|
+
if (!base.includes(".")) {
|
|
1638
|
+
throw new Error(
|
|
1639
|
+
`Wildcard pattern '${normalized}' is too broad \u2014 the base domain must have at least one dot (e.g. '*.mycompany.com', not '*.com').`
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1616
1643
|
const hosts = readTrustedHosts();
|
|
1617
|
-
if (hosts.some((h) => h.host ===
|
|
1618
|
-
hosts.push({ host, addedAt: Date.now(), addedBy: "user" });
|
|
1644
|
+
if (hosts.some((h) => h.host === normalized)) return;
|
|
1645
|
+
hosts.push({ host: normalized, addedAt: Date.now(), addedBy: "user" });
|
|
1619
1646
|
writeTrustedHosts(hosts);
|
|
1620
1647
|
}
|
|
1621
1648
|
function removeTrustedHost(host) {
|
|
@@ -1630,18 +1657,21 @@ function normalizeHost(raw) {
|
|
|
1630
1657
|
}
|
|
1631
1658
|
function isTrustedHost(host) {
|
|
1632
1659
|
const normalized = normalizeHost(host);
|
|
1633
|
-
return
|
|
1660
|
+
return getCachedHosts().some((entry) => {
|
|
1634
1661
|
const entryHost = entry.host.toLowerCase();
|
|
1635
1662
|
if (entryHost.startsWith("*.")) {
|
|
1636
1663
|
const domain = entryHost.slice(2);
|
|
1637
|
-
return normalized
|
|
1664
|
+
return normalized.endsWith("." + domain);
|
|
1638
1665
|
}
|
|
1639
1666
|
return normalized === entryHost;
|
|
1640
1667
|
});
|
|
1641
1668
|
}
|
|
1669
|
+
var _cache, CACHE_TTL_MS;
|
|
1642
1670
|
var init_trusted_hosts = __esm({
|
|
1643
1671
|
"src/auth/trusted-hosts.ts"() {
|
|
1644
1672
|
"use strict";
|
|
1673
|
+
_cache = null;
|
|
1674
|
+
CACHE_TTL_MS = 5e3;
|
|
1645
1675
|
}
|
|
1646
1676
|
});
|
|
1647
1677
|
|
|
@@ -1664,9 +1694,9 @@ function matchesPattern(text, patterns) {
|
|
|
1664
1694
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1665
1695
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1666
1696
|
}
|
|
1667
|
-
function getNestedValue(obj,
|
|
1697
|
+
function getNestedValue(obj, path27) {
|
|
1668
1698
|
if (!obj || typeof obj !== "object") return null;
|
|
1669
|
-
return
|
|
1699
|
+
return path27.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1670
1700
|
}
|
|
1671
1701
|
function shouldSnapshot(toolName, args, config) {
|
|
1672
1702
|
if (!config.settings.enableUndo) return false;
|
|
@@ -1852,7 +1882,12 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1852
1882
|
};
|
|
1853
1883
|
}
|
|
1854
1884
|
if (allTrusted) {
|
|
1855
|
-
return {
|
|
1885
|
+
return {
|
|
1886
|
+
decision: "allow",
|
|
1887
|
+
blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
|
|
1888
|
+
reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
|
|
1889
|
+
tier: 3
|
|
1890
|
+
};
|
|
1856
1891
|
}
|
|
1857
1892
|
return {
|
|
1858
1893
|
decision: "review",
|
|
@@ -2393,8 +2428,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
2393
2428
|
signal: ctrl.signal
|
|
2394
2429
|
});
|
|
2395
2430
|
if (!res.ok) throw new Error("Daemon fail");
|
|
2396
|
-
const { id } = await res.json();
|
|
2397
|
-
return id;
|
|
2431
|
+
const { id, allowCount } = await res.json();
|
|
2432
|
+
return { id, allowCount: allowCount ?? 1 };
|
|
2398
2433
|
} finally {
|
|
2399
2434
|
clearTimeout(timer);
|
|
2400
2435
|
}
|
|
@@ -2433,15 +2468,15 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
|
|
|
2433
2468
|
signal: AbortSignal.timeout(3e3)
|
|
2434
2469
|
});
|
|
2435
2470
|
if (!res.ok) throw new Error("Daemon unreachable");
|
|
2436
|
-
const { id } = await res.json();
|
|
2437
|
-
return id;
|
|
2471
|
+
const { id, allowCount } = await res.json();
|
|
2472
|
+
return { id, allowCount: allowCount ?? 1 };
|
|
2438
2473
|
}
|
|
2439
|
-
async function resolveViaDaemon(id, decision, internalToken) {
|
|
2474
|
+
async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
2440
2475
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2441
2476
|
await fetch(`${base}/resolve/${id}`, {
|
|
2442
2477
|
method: "POST",
|
|
2443
2478
|
headers: { "Content-Type": "application/json", "X-Node9-Internal": internalToken },
|
|
2444
|
-
body: JSON.stringify({ decision }),
|
|
2479
|
+
body: JSON.stringify({ decision, ...source && { source } }),
|
|
2445
2480
|
signal: AbortSignal.timeout(3e3)
|
|
2446
2481
|
});
|
|
2447
2482
|
}
|
|
@@ -2646,20 +2681,24 @@ ${smartTruncate(str, 500)}`
|
|
|
2646
2681
|
function escapePango(text) {
|
|
2647
2682
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2648
2683
|
}
|
|
2649
|
-
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
2684
|
+
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
|
|
2650
2685
|
const lines = [];
|
|
2651
2686
|
if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
|
|
2652
2687
|
lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
|
|
2653
2688
|
lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
|
|
2654
2689
|
lines.push("");
|
|
2655
2690
|
lines.push(formattedArgs);
|
|
2691
|
+
if (allowCount >= 3) {
|
|
2692
|
+
lines.push("");
|
|
2693
|
+
lines.push(`\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule`);
|
|
2694
|
+
}
|
|
2656
2695
|
if (!locked) {
|
|
2657
2696
|
lines.push("");
|
|
2658
2697
|
lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
|
|
2659
2698
|
}
|
|
2660
2699
|
return lines.join("\n");
|
|
2661
2700
|
}
|
|
2662
|
-
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
2701
|
+
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
|
|
2663
2702
|
const lines = [];
|
|
2664
2703
|
if (locked) {
|
|
2665
2704
|
lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
|
|
@@ -2671,6 +2710,12 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
2671
2710
|
lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
|
|
2672
2711
|
lines.push("");
|
|
2673
2712
|
lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
|
|
2713
|
+
if (allowCount >= 3) {
|
|
2714
|
+
lines.push("");
|
|
2715
|
+
lines.push(
|
|
2716
|
+
`<span foreground="#f0c040">\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</span>`
|
|
2717
|
+
);
|
|
2718
|
+
}
|
|
2674
2719
|
if (!locked) {
|
|
2675
2720
|
lines.push("");
|
|
2676
2721
|
lines.push(
|
|
@@ -2679,12 +2724,19 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
2679
2724
|
}
|
|
2680
2725
|
return lines.join("\n");
|
|
2681
2726
|
}
|
|
2682
|
-
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord) {
|
|
2727
|
+
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1) {
|
|
2683
2728
|
if (isTestEnv()) return "deny";
|
|
2684
2729
|
const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
|
|
2685
2730
|
const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
|
|
2686
2731
|
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
|
|
2687
|
-
const message = buildPlainMessage(
|
|
2732
|
+
const message = buildPlainMessage(
|
|
2733
|
+
toolName,
|
|
2734
|
+
formattedArgs,
|
|
2735
|
+
agent,
|
|
2736
|
+
explainableLabel,
|
|
2737
|
+
locked,
|
|
2738
|
+
allowCount
|
|
2739
|
+
);
|
|
2688
2740
|
return new Promise((resolve) => {
|
|
2689
2741
|
let childProcess = null;
|
|
2690
2742
|
const onAbort = () => {
|
|
@@ -2716,7 +2768,8 @@ end run`;
|
|
|
2716
2768
|
formattedArgs,
|
|
2717
2769
|
agent,
|
|
2718
2770
|
explainableLabel,
|
|
2719
|
-
locked
|
|
2771
|
+
locked,
|
|
2772
|
+
allowCount
|
|
2720
2773
|
);
|
|
2721
2774
|
const argsList = [
|
|
2722
2775
|
locked ? "--info" : "--question",
|
|
@@ -3080,13 +3133,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3080
3133
|
let viewerId = null;
|
|
3081
3134
|
const internalToken = getInternalToken();
|
|
3082
3135
|
let daemonEntryId = null;
|
|
3136
|
+
let daemonAllowCount = 1;
|
|
3083
3137
|
if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
|
|
3084
3138
|
if (cloudEnforced && cloudRequestId) {
|
|
3085
|
-
|
|
3139
|
+
const viewer = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
|
|
3140
|
+
viewerId = viewer?.id ?? null;
|
|
3086
3141
|
daemonEntryId = viewerId;
|
|
3142
|
+
if (viewer) daemonAllowCount = viewer.allowCount;
|
|
3087
3143
|
} else {
|
|
3088
3144
|
try {
|
|
3089
|
-
|
|
3145
|
+
const entry = await registerDaemonEntry(
|
|
3090
3146
|
toolName,
|
|
3091
3147
|
args,
|
|
3092
3148
|
meta,
|
|
@@ -3094,6 +3150,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3094
3150
|
options?.activityId,
|
|
3095
3151
|
options?.cwd
|
|
3096
3152
|
);
|
|
3153
|
+
daemonEntryId = entry.id;
|
|
3154
|
+
daemonAllowCount = entry.allowCount;
|
|
3097
3155
|
} catch {
|
|
3098
3156
|
}
|
|
3099
3157
|
}
|
|
@@ -3129,7 +3187,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3129
3187
|
false,
|
|
3130
3188
|
signal,
|
|
3131
3189
|
policyMatchedField,
|
|
3132
|
-
policyMatchedWord
|
|
3190
|
+
policyMatchedWord,
|
|
3191
|
+
daemonAllowCount
|
|
3133
3192
|
);
|
|
3134
3193
|
if (decision === "always_allow") {
|
|
3135
3194
|
writeTrustSession(toolName, 36e5);
|
|
@@ -3187,10 +3246,13 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
3187
3246
|
if (!resolved) {
|
|
3188
3247
|
resolved = true;
|
|
3189
3248
|
abortController.abort();
|
|
3190
|
-
if (
|
|
3191
|
-
resolveViaDaemon(
|
|
3192
|
-
|
|
3193
|
-
|
|
3249
|
+
if (daemonEntryId && internalToken) {
|
|
3250
|
+
resolveViaDaemon(
|
|
3251
|
+
daemonEntryId,
|
|
3252
|
+
res.approved ? "allow" : "deny",
|
|
3253
|
+
internalToken,
|
|
3254
|
+
res.decisionSource
|
|
3255
|
+
).catch(() => null);
|
|
3194
3256
|
}
|
|
3195
3257
|
resolve(res);
|
|
3196
3258
|
}
|
|
@@ -3543,6 +3605,15 @@ var init_ui = __esm({
|
|
|
3543
3605
|
padding: 5px 10px;
|
|
3544
3606
|
margin-bottom: 14px;
|
|
3545
3607
|
}
|
|
3608
|
+
.insight-hint {
|
|
3609
|
+
font-size: 12px;
|
|
3610
|
+
color: #f0c040;
|
|
3611
|
+
background: rgba(240, 192, 64, 0.08);
|
|
3612
|
+
border: 1px solid rgba(240, 192, 64, 0.25);
|
|
3613
|
+
border-radius: 6px;
|
|
3614
|
+
padding: 6px 10px;
|
|
3615
|
+
margin-bottom: 12px;
|
|
3616
|
+
}
|
|
3546
3617
|
pre {
|
|
3547
3618
|
background: #0d1117;
|
|
3548
3619
|
padding: 14px 16px;
|
|
@@ -4015,6 +4086,78 @@ var init_ui = __esm({
|
|
|
4015
4086
|
color: var(--danger);
|
|
4016
4087
|
}
|
|
4017
4088
|
|
|
4089
|
+
/* \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 */
|
|
4090
|
+
.suggestion-card {
|
|
4091
|
+
background: rgba(82, 130, 255, 0.06);
|
|
4092
|
+
border: 1px solid rgba(82, 130, 255, 0.25);
|
|
4093
|
+
border-radius: 8px;
|
|
4094
|
+
padding: 10px 12px;
|
|
4095
|
+
margin-bottom: 8px;
|
|
4096
|
+
}
|
|
4097
|
+
.suggestion-card:last-child {
|
|
4098
|
+
margin-bottom: 0;
|
|
4099
|
+
}
|
|
4100
|
+
.suggestion-header {
|
|
4101
|
+
display: flex;
|
|
4102
|
+
align-items: center;
|
|
4103
|
+
gap: 8px;
|
|
4104
|
+
margin-bottom: 6px;
|
|
4105
|
+
}
|
|
4106
|
+
.suggestion-tool {
|
|
4107
|
+
font-family: 'Fira Code', monospace;
|
|
4108
|
+
font-size: 11px;
|
|
4109
|
+
color: var(--text-bright);
|
|
4110
|
+
flex: 1;
|
|
4111
|
+
word-break: break-all;
|
|
4112
|
+
}
|
|
4113
|
+
.suggestion-count {
|
|
4114
|
+
font-size: 10px;
|
|
4115
|
+
color: var(--muted);
|
|
4116
|
+
white-space: nowrap;
|
|
4117
|
+
}
|
|
4118
|
+
.suggestion-rule {
|
|
4119
|
+
font-family: 'Fira Code', monospace;
|
|
4120
|
+
font-size: 10px;
|
|
4121
|
+
color: #79c0ff;
|
|
4122
|
+
background: rgba(0, 0, 0, 0.25);
|
|
4123
|
+
border-radius: 4px;
|
|
4124
|
+
padding: 4px 8px;
|
|
4125
|
+
margin-bottom: 8px;
|
|
4126
|
+
word-break: break-all;
|
|
4127
|
+
white-space: pre-wrap;
|
|
4128
|
+
}
|
|
4129
|
+
.suggestion-actions {
|
|
4130
|
+
display: flex;
|
|
4131
|
+
gap: 6px;
|
|
4132
|
+
}
|
|
4133
|
+
.btn-apply {
|
|
4134
|
+
background: rgba(52, 125, 57, 0.2);
|
|
4135
|
+
border: 1px solid rgba(87, 171, 90, 0.4);
|
|
4136
|
+
color: #57ab5a;
|
|
4137
|
+
padding: 4px 10px;
|
|
4138
|
+
font-size: 11px;
|
|
4139
|
+
border-radius: 5px;
|
|
4140
|
+
font-family: inherit;
|
|
4141
|
+
cursor: pointer;
|
|
4142
|
+
}
|
|
4143
|
+
.btn-apply:hover {
|
|
4144
|
+
background: rgba(52, 125, 57, 0.35);
|
|
4145
|
+
}
|
|
4146
|
+
.btn-dismiss-suggestion {
|
|
4147
|
+
background: transparent;
|
|
4148
|
+
border: 1px solid var(--border);
|
|
4149
|
+
color: var(--muted);
|
|
4150
|
+
padding: 4px 10px;
|
|
4151
|
+
font-size: 11px;
|
|
4152
|
+
border-radius: 5px;
|
|
4153
|
+
font-family: inherit;
|
|
4154
|
+
cursor: pointer;
|
|
4155
|
+
}
|
|
4156
|
+
.btn-dismiss-suggestion:hover {
|
|
4157
|
+
border-color: var(--danger);
|
|
4158
|
+
color: var(--danger);
|
|
4159
|
+
}
|
|
4160
|
+
|
|
4018
4161
|
.modal-overlay {
|
|
4019
4162
|
display: none;
|
|
4020
4163
|
position: fixed;
|
|
@@ -4196,6 +4339,11 @@ var init_ui = __esm({
|
|
|
4196
4339
|
<div class="panel-title">\u{1F4CB} Persistent Decisions</div>
|
|
4197
4340
|
<div id="decisionsList"><span class="decisions-empty">None yet.</span></div>
|
|
4198
4341
|
</div>
|
|
4342
|
+
|
|
4343
|
+
<div class="panel" id="suggestionsPanel" style="display: none">
|
|
4344
|
+
<div class="panel-title">\u{1F4A1} Smart Rule Suggestions</div>
|
|
4345
|
+
<div id="suggestionsList"></div>
|
|
4346
|
+
</div>
|
|
4199
4347
|
</div>
|
|
4200
4348
|
</div>
|
|
4201
4349
|
</div>
|
|
@@ -4385,6 +4533,7 @@ var init_ui = __esm({
|
|
|
4385
4533
|
</div>
|
|
4386
4534
|
<div class="tool-chip">\${esc(req.toolName)}</div>
|
|
4387
4535
|
\${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Cloud approval \u2014 view only</div>' : ''}
|
|
4536
|
+
\${req.allowCount >= 3 ? \`<div class="insight-hint">\u{1F4A1} Approved \${req.allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</div>\` : ''}
|
|
4388
4537
|
\${renderPayload(req)}
|
|
4389
4538
|
<div class="actions" id="act-\${req.id}">
|
|
4390
4539
|
<button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>\u2705 Allow this Action</button>
|
|
@@ -4451,6 +4600,14 @@ var init_ui = __esm({
|
|
|
4451
4600
|
ev.addEventListener('shields-status', (e) => {
|
|
4452
4601
|
renderShields(JSON.parse(e.data).shields);
|
|
4453
4602
|
});
|
|
4603
|
+
ev.addEventListener('suggestion:new', (e) => {
|
|
4604
|
+
const s = JSON.parse(e.data);
|
|
4605
|
+
addSuggestionCard(s);
|
|
4606
|
+
});
|
|
4607
|
+
ev.addEventListener('suggestion:resolved', (e) => {
|
|
4608
|
+
const { id } = JSON.parse(e.data);
|
|
4609
|
+
removeSuggestionCard(id);
|
|
4610
|
+
});
|
|
4454
4611
|
|
|
4455
4612
|
// \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
|
|
4456
4613
|
ev.addEventListener('activity', (e) => {
|
|
@@ -4700,6 +4857,74 @@ var init_ui = __esm({
|
|
|
4700
4857
|
.then((r) => r.json())
|
|
4701
4858
|
.then(renderDecisions)
|
|
4702
4859
|
.catch(() => {});
|
|
4860
|
+
|
|
4861
|
+
// \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
|
|
4862
|
+
function rulePreview(suggestion) {
|
|
4863
|
+
const r = suggestion.suggestedRule;
|
|
4864
|
+
if (r.type === 'ignoredTool') return \`ignoredTool: "\${r.toolName}"\`;
|
|
4865
|
+
const cond = r.rule.conditions?.[0];
|
|
4866
|
+
const condStr = cond ? \` where \${cond.field} \${cond.op} "\${cond.value}"\` : '';
|
|
4867
|
+
return \`allow \${r.rule.tool}\${condStr}\`;
|
|
4868
|
+
}
|
|
4869
|
+
|
|
4870
|
+
function addSuggestionCard(s) {
|
|
4871
|
+
const panel = document.getElementById('suggestionsPanel');
|
|
4872
|
+
const list = document.getElementById('suggestionsList');
|
|
4873
|
+
panel.style.display = '';
|
|
4874
|
+
|
|
4875
|
+
const card = document.createElement('div');
|
|
4876
|
+
card.className = 'suggestion-card';
|
|
4877
|
+
card.id = 'sg-' + s.id;
|
|
4878
|
+
card.innerHTML = \`
|
|
4879
|
+
<div class="suggestion-header">
|
|
4880
|
+
<span class="suggestion-tool">\${esc(s.toolName)}</span>
|
|
4881
|
+
<span class="suggestion-count">allowed \${s.allowCount}\xD7</span>
|
|
4882
|
+
</div>
|
|
4883
|
+
<div class="suggestion-rule">\${esc(rulePreview(s))}</div>
|
|
4884
|
+
<div class="suggestion-actions">
|
|
4885
|
+
<button class="btn-apply" onclick="applySuggestion('\${esc(s.id)}')">Apply rule</button>
|
|
4886
|
+
<button class="btn-dismiss-suggestion" onclick="dismissSuggestion('\${esc(s.id)}')">Dismiss</button>
|
|
4887
|
+
</div>
|
|
4888
|
+
\`;
|
|
4889
|
+
list.appendChild(card);
|
|
4890
|
+
}
|
|
4891
|
+
|
|
4892
|
+
function removeSuggestionCard(id) {
|
|
4893
|
+
document.getElementById('sg-' + id)?.remove();
|
|
4894
|
+
const list = document.getElementById('suggestionsList');
|
|
4895
|
+
if (!list.querySelector('.suggestion-card')) {
|
|
4896
|
+
document.getElementById('suggestionsPanel').style.display = 'none';
|
|
4897
|
+
}
|
|
4898
|
+
}
|
|
4899
|
+
|
|
4900
|
+
function applySuggestion(id) {
|
|
4901
|
+
fetch('/suggestions/' + id + '/apply', {
|
|
4902
|
+
method: 'POST',
|
|
4903
|
+
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
4904
|
+
body: JSON.stringify({}),
|
|
4905
|
+
})
|
|
4906
|
+
.then((r) => {
|
|
4907
|
+
if (r.ok) removeSuggestionCard(id);
|
|
4908
|
+
})
|
|
4909
|
+
.catch(() => {});
|
|
4910
|
+
}
|
|
4911
|
+
|
|
4912
|
+
function dismissSuggestion(id) {
|
|
4913
|
+
fetch('/suggestions/' + id + '/dismiss', {
|
|
4914
|
+
method: 'POST',
|
|
4915
|
+
headers: { 'X-Node9-Token': CSRF_TOKEN },
|
|
4916
|
+
})
|
|
4917
|
+
.then((r) => {
|
|
4918
|
+
if (r.ok) removeSuggestionCard(id);
|
|
4919
|
+
})
|
|
4920
|
+
.catch(() => {});
|
|
4921
|
+
}
|
|
4922
|
+
|
|
4923
|
+
// Load any suggestions that survived a page reload (daemon still running)
|
|
4924
|
+
fetch('/suggestions')
|
|
4925
|
+
.then((r) => r.json())
|
|
4926
|
+
.then((list) => list.filter((s) => s.status === 'pending').forEach(addSuggestionCard))
|
|
4927
|
+
.catch(() => {});
|
|
4703
4928
|
</script>
|
|
4704
4929
|
</body>
|
|
4705
4930
|
</html>
|
|
@@ -4717,13 +4942,123 @@ var init_ui2 = __esm({
|
|
|
4717
4942
|
}
|
|
4718
4943
|
});
|
|
4719
4944
|
|
|
4945
|
+
// src/daemon/suggestion-tracker.ts
|
|
4946
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
4947
|
+
function extractPath(args) {
|
|
4948
|
+
if (!args || typeof args !== "object") return null;
|
|
4949
|
+
const a = args;
|
|
4950
|
+
for (const key of ["path", "file_path", "filename", "filepath", "dest", "destination"]) {
|
|
4951
|
+
if (typeof a[key] === "string" && a[key]) return a[key];
|
|
4952
|
+
}
|
|
4953
|
+
return null;
|
|
4954
|
+
}
|
|
4955
|
+
function commonPathPrefix(paths) {
|
|
4956
|
+
if (paths.length < 2) return null;
|
|
4957
|
+
const dirParts = paths.map((p) => {
|
|
4958
|
+
const lastSlash = p.lastIndexOf("/");
|
|
4959
|
+
return lastSlash > 0 ? p.slice(0, lastSlash + 1) : "/";
|
|
4960
|
+
});
|
|
4961
|
+
const first = dirParts[0].split("/");
|
|
4962
|
+
const common = [];
|
|
4963
|
+
for (let i = 0; i < first.length; i++) {
|
|
4964
|
+
if (dirParts.every((d) => d.split("/")[i] === first[i])) {
|
|
4965
|
+
common.push(first[i]);
|
|
4966
|
+
} else {
|
|
4967
|
+
break;
|
|
4968
|
+
}
|
|
4969
|
+
}
|
|
4970
|
+
const prefix = common.join("/").replace(/\/?$/, "/");
|
|
4971
|
+
return prefix.length > 1 ? prefix : null;
|
|
4972
|
+
}
|
|
4973
|
+
var SuggestionTracker;
|
|
4974
|
+
var init_suggestion_tracker = __esm({
|
|
4975
|
+
"src/daemon/suggestion-tracker.ts"() {
|
|
4976
|
+
"use strict";
|
|
4977
|
+
SuggestionTracker = class {
|
|
4978
|
+
events = /* @__PURE__ */ new Map();
|
|
4979
|
+
threshold;
|
|
4980
|
+
constructor(threshold = 3) {
|
|
4981
|
+
this.threshold = threshold;
|
|
4982
|
+
}
|
|
4983
|
+
/**
|
|
4984
|
+
* Record a human-allowed review for a tool.
|
|
4985
|
+
* Returns a Suggestion when the threshold is reached, null otherwise.
|
|
4986
|
+
*/
|
|
4987
|
+
recordAllow(toolName, args) {
|
|
4988
|
+
const events = this.events.get(toolName) ?? [];
|
|
4989
|
+
events.push({ args, ts: Date.now() });
|
|
4990
|
+
this.events.set(toolName, events);
|
|
4991
|
+
if (events.length >= this.threshold) {
|
|
4992
|
+
this.events.delete(toolName);
|
|
4993
|
+
return this.generateSuggestion(toolName, events);
|
|
4994
|
+
}
|
|
4995
|
+
return null;
|
|
4996
|
+
}
|
|
4997
|
+
/**
|
|
4998
|
+
* Reset the counter for a tool (e.g. when the user clicks Deny —
|
|
4999
|
+
* don't suggest allowing something they just blocked).
|
|
5000
|
+
*/
|
|
5001
|
+
resetTool(toolName) {
|
|
5002
|
+
this.events.delete(toolName);
|
|
5003
|
+
}
|
|
5004
|
+
/** Current allow count for a tool (for tests). */
|
|
5005
|
+
getCount(toolName) {
|
|
5006
|
+
return this.events.get(toolName)?.length ?? 0;
|
|
5007
|
+
}
|
|
5008
|
+
generateSuggestion(toolName, events) {
|
|
5009
|
+
const paths = events.map((e) => extractPath(e.args)).filter((p) => typeof p === "string" && p.length > 0);
|
|
5010
|
+
const prefix = commonPathPrefix(paths);
|
|
5011
|
+
const suggestedRule = prefix ? {
|
|
5012
|
+
type: "smartRule",
|
|
5013
|
+
rule: {
|
|
5014
|
+
name: `allow-${toolName}-${prefix.replace(/[^a-z0-9]/gi, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}`,
|
|
5015
|
+
tool: toolName,
|
|
5016
|
+
conditions: [{ field: "path", op: "matchesGlob", value: `${prefix}**` }],
|
|
5017
|
+
verdict: "allow",
|
|
5018
|
+
reason: `Auto-suggested: ${toolName} allowed ${events.length}\xD7 in ${prefix}`
|
|
5019
|
+
}
|
|
5020
|
+
} : { type: "ignoredTool", toolName };
|
|
5021
|
+
return {
|
|
5022
|
+
id: randomUUID2(),
|
|
5023
|
+
toolName,
|
|
5024
|
+
allowCount: events.length,
|
|
5025
|
+
suggestedRule,
|
|
5026
|
+
status: "pending",
|
|
5027
|
+
createdAt: Date.now(),
|
|
5028
|
+
exampleArgs: events.slice(0, 3).map((e) => e.args)
|
|
5029
|
+
};
|
|
5030
|
+
}
|
|
5031
|
+
};
|
|
5032
|
+
}
|
|
5033
|
+
});
|
|
5034
|
+
|
|
4720
5035
|
// src/daemon/state.ts
|
|
4721
5036
|
import net2 from "net";
|
|
4722
5037
|
import fs12 from "fs";
|
|
4723
5038
|
import path15 from "path";
|
|
4724
5039
|
import os12 from "os";
|
|
4725
5040
|
import { spawn as spawn2 } from "child_process";
|
|
4726
|
-
import { randomUUID as
|
|
5041
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
5042
|
+
function loadInsightCounts() {
|
|
5043
|
+
try {
|
|
5044
|
+
if (!fs12.existsSync(INSIGHT_COUNTS_FILE)) return;
|
|
5045
|
+
const data = JSON.parse(fs12.readFileSync(INSIGHT_COUNTS_FILE, "utf-8"));
|
|
5046
|
+
for (const [tool, count] of Object.entries(data)) {
|
|
5047
|
+
if (typeof count === "number" && count > 0) insightCounts.set(tool, count);
|
|
5048
|
+
}
|
|
5049
|
+
} catch {
|
|
5050
|
+
}
|
|
5051
|
+
}
|
|
5052
|
+
function saveInsightCounts() {
|
|
5053
|
+
try {
|
|
5054
|
+
const data = {};
|
|
5055
|
+
insightCounts.forEach((count, tool) => {
|
|
5056
|
+
data[tool] = count;
|
|
5057
|
+
});
|
|
5058
|
+
atomicWriteSync2(INSIGHT_COUNTS_FILE, JSON.stringify(data, null, 2), { mode: 384 });
|
|
5059
|
+
} catch {
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
4727
5062
|
function getAbandonTimer() {
|
|
4728
5063
|
return _abandonTimer;
|
|
4729
5064
|
}
|
|
@@ -4748,9 +5083,25 @@ function markRejectionHandlerRegistered() {
|
|
|
4748
5083
|
function atomicWriteSync2(filePath, data, options) {
|
|
4749
5084
|
const dir = path15.dirname(filePath);
|
|
4750
5085
|
if (!fs12.existsSync(dir)) fs12.mkdirSync(dir, { recursive: true });
|
|
4751
|
-
const tmpPath = `${filePath}.${
|
|
4752
|
-
|
|
4753
|
-
|
|
5086
|
+
const tmpPath = `${filePath}.${randomUUID3()}.tmp`;
|
|
5087
|
+
try {
|
|
5088
|
+
fs12.writeFileSync(tmpPath, data, options);
|
|
5089
|
+
} catch (err) {
|
|
5090
|
+
try {
|
|
5091
|
+
fs12.unlinkSync(tmpPath);
|
|
5092
|
+
} catch {
|
|
5093
|
+
}
|
|
5094
|
+
throw err;
|
|
5095
|
+
}
|
|
5096
|
+
try {
|
|
5097
|
+
fs12.renameSync(tmpPath, filePath);
|
|
5098
|
+
} catch (err) {
|
|
5099
|
+
try {
|
|
5100
|
+
fs12.unlinkSync(tmpPath);
|
|
5101
|
+
} catch {
|
|
5102
|
+
}
|
|
5103
|
+
throw err;
|
|
5104
|
+
}
|
|
4754
5105
|
}
|
|
4755
5106
|
function redactArgs(value) {
|
|
4756
5107
|
if (!value || typeof value !== "object") return value;
|
|
@@ -4948,11 +5299,12 @@ function startActivitySocket() {
|
|
|
4948
5299
|
}
|
|
4949
5300
|
});
|
|
4950
5301
|
}
|
|
4951
|
-
var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, pending, sseClients, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE;
|
|
5302
|
+
var 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;
|
|
4952
5303
|
var init_state2 = __esm({
|
|
4953
5304
|
"src/daemon/state.ts"() {
|
|
4954
5305
|
"use strict";
|
|
4955
5306
|
init_daemon();
|
|
5307
|
+
init_suggestion_tracker();
|
|
4956
5308
|
homeDir = os12.homedir();
|
|
4957
5309
|
DAEMON_PID_FILE = path15.join(homeDir, ".node9", "daemon.pid");
|
|
4958
5310
|
DECISIONS_FILE = path15.join(homeDir, ".node9", "decisions.json");
|
|
@@ -4960,8 +5312,12 @@ var init_state2 = __esm({
|
|
|
4960
5312
|
TRUST_FILE2 = path15.join(homeDir, ".node9", "trust.json");
|
|
4961
5313
|
GLOBAL_CONFIG_FILE = path15.join(homeDir, ".node9", "config.json");
|
|
4962
5314
|
CREDENTIALS_FILE = path15.join(homeDir, ".node9", "credentials.json");
|
|
5315
|
+
INSIGHT_COUNTS_FILE = path15.join(homeDir, ".node9", "insight-counts.json");
|
|
4963
5316
|
pending = /* @__PURE__ */ new Map();
|
|
4964
5317
|
sseClients = /* @__PURE__ */ new Set();
|
|
5318
|
+
suggestionTracker = new SuggestionTracker(3);
|
|
5319
|
+
suggestions = /* @__PURE__ */ new Map();
|
|
5320
|
+
insightCounts = /* @__PURE__ */ new Map();
|
|
4965
5321
|
_abandonTimer = null;
|
|
4966
5322
|
_hadBrowserClient = false;
|
|
4967
5323
|
_daemonServer = null;
|
|
@@ -4980,16 +5336,74 @@ var init_state2 = __esm({
|
|
|
4980
5336
|
}
|
|
4981
5337
|
});
|
|
4982
5338
|
|
|
4983
|
-
// src/
|
|
4984
|
-
import http from "http";
|
|
5339
|
+
// src/config/patch.ts
|
|
4985
5340
|
import fs13 from "fs";
|
|
4986
5341
|
import path16 from "path";
|
|
4987
|
-
import
|
|
5342
|
+
import os13 from "os";
|
|
5343
|
+
function patchConfig(configPath, patch) {
|
|
5344
|
+
let config = {};
|
|
5345
|
+
try {
|
|
5346
|
+
if (fs13.existsSync(configPath)) {
|
|
5347
|
+
config = JSON.parse(fs13.readFileSync(configPath, "utf8"));
|
|
5348
|
+
}
|
|
5349
|
+
} catch {
|
|
5350
|
+
throw new Error(`Cannot read config at ${configPath} \u2014 file may be corrupted`);
|
|
5351
|
+
}
|
|
5352
|
+
if (!config.policy || typeof config.policy !== "object") config.policy = {};
|
|
5353
|
+
const policy = config.policy;
|
|
5354
|
+
if (patch.type === "smartRule") {
|
|
5355
|
+
if (!Array.isArray(policy.smartRules)) policy.smartRules = [];
|
|
5356
|
+
const rules = policy.smartRules;
|
|
5357
|
+
if (patch.rule.name && rules.some((r) => r.name === patch.rule.name)) return;
|
|
5358
|
+
rules.push(patch.rule);
|
|
5359
|
+
} else {
|
|
5360
|
+
if (!Array.isArray(policy.ignoredTools)) policy.ignoredTools = [];
|
|
5361
|
+
const ignored = policy.ignoredTools;
|
|
5362
|
+
if (!ignored.includes(patch.toolName)) {
|
|
5363
|
+
ignored.push(patch.toolName);
|
|
5364
|
+
}
|
|
5365
|
+
}
|
|
5366
|
+
const dir = path16.dirname(configPath);
|
|
5367
|
+
fs13.mkdirSync(dir, { recursive: true });
|
|
5368
|
+
const tmp = configPath + ".node9-tmp";
|
|
5369
|
+
try {
|
|
5370
|
+
fs13.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
|
|
5371
|
+
} catch (err) {
|
|
5372
|
+
try {
|
|
5373
|
+
fs13.unlinkSync(tmp);
|
|
5374
|
+
} catch {
|
|
5375
|
+
}
|
|
5376
|
+
throw err;
|
|
5377
|
+
}
|
|
5378
|
+
try {
|
|
5379
|
+
fs13.renameSync(tmp, configPath);
|
|
5380
|
+
} catch (err) {
|
|
5381
|
+
try {
|
|
5382
|
+
fs13.unlinkSync(tmp);
|
|
5383
|
+
} catch {
|
|
5384
|
+
}
|
|
5385
|
+
throw err;
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
var GLOBAL_CONFIG_PATH;
|
|
5389
|
+
var init_patch = __esm({
|
|
5390
|
+
"src/config/patch.ts"() {
|
|
5391
|
+
"use strict";
|
|
5392
|
+
GLOBAL_CONFIG_PATH = path16.join(os13.homedir(), ".node9", "config.json");
|
|
5393
|
+
}
|
|
5394
|
+
});
|
|
5395
|
+
|
|
5396
|
+
// src/daemon/server.ts
|
|
5397
|
+
import http from "http";
|
|
5398
|
+
import fs14 from "fs";
|
|
5399
|
+
import path17 from "path";
|
|
5400
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
4988
5401
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
4989
5402
|
import chalk2 from "chalk";
|
|
4990
5403
|
function startDaemon() {
|
|
4991
|
-
|
|
4992
|
-
const
|
|
5404
|
+
loadInsightCounts();
|
|
5405
|
+
const csrfToken = randomUUID4();
|
|
5406
|
+
const internalToken = randomUUID4();
|
|
4993
5407
|
const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
|
|
4994
5408
|
const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
|
|
4995
5409
|
const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
|
|
@@ -5002,7 +5416,7 @@ function startDaemon() {
|
|
|
5002
5416
|
idleTimer = setTimeout(() => {
|
|
5003
5417
|
if (autoStarted) {
|
|
5004
5418
|
try {
|
|
5005
|
-
|
|
5419
|
+
fs14.unlinkSync(DAEMON_PID_FILE);
|
|
5006
5420
|
} catch {
|
|
5007
5421
|
}
|
|
5008
5422
|
}
|
|
@@ -5011,8 +5425,14 @@ function startDaemon() {
|
|
|
5011
5425
|
idleTimer.unref();
|
|
5012
5426
|
}
|
|
5013
5427
|
resetIdleTimer();
|
|
5428
|
+
const allowedHosts = /* @__PURE__ */ new Set([`127.0.0.1:${DAEMON_PORT}`, `localhost:${DAEMON_PORT}`]);
|
|
5014
5429
|
const server = http.createServer(async (req, res) => {
|
|
5015
|
-
const
|
|
5430
|
+
const host = req.headers.host ?? "";
|
|
5431
|
+
if (!allowedHosts.has(host)) {
|
|
5432
|
+
res.writeHead(421, { "Content-Type": "text/plain" });
|
|
5433
|
+
return res.end("Misdirected Request");
|
|
5434
|
+
}
|
|
5435
|
+
const reqUrl = new URL(req.url || "/", `http://${host}`);
|
|
5016
5436
|
const { pathname } = reqUrl;
|
|
5017
5437
|
if (req.method === "GET" && pathname === "/") {
|
|
5018
5438
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
@@ -5045,7 +5465,8 @@ data: ${JSON.stringify({
|
|
|
5045
5465
|
slackDelegated: e.slackDelegated,
|
|
5046
5466
|
timestamp: e.timestamp,
|
|
5047
5467
|
agent: e.agent,
|
|
5048
|
-
mcpServer: e.mcpServer
|
|
5468
|
+
mcpServer: e.mcpServer,
|
|
5469
|
+
allowCount: (insightCounts.get(e.toolName) ?? 0) + 1
|
|
5049
5470
|
})),
|
|
5050
5471
|
orgName: getOrgName(),
|
|
5051
5472
|
autoDenyMs: getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS
|
|
@@ -5087,6 +5508,12 @@ data: ${JSON.stringify(item.data)}
|
|
|
5087
5508
|
}
|
|
5088
5509
|
});
|
|
5089
5510
|
}
|
|
5511
|
+
if (req.method === "POST" && pathname === "/browser-opened") {
|
|
5512
|
+
if (req.headers["x-node9-internal"] !== internalToken) return res.writeHead(403).end();
|
|
5513
|
+
browserOpened = true;
|
|
5514
|
+
res.writeHead(200).end();
|
|
5515
|
+
return;
|
|
5516
|
+
}
|
|
5090
5517
|
if (req.method === "POST" && pathname === "/check") {
|
|
5091
5518
|
try {
|
|
5092
5519
|
resetIdleTimer();
|
|
@@ -5104,7 +5531,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5104
5531
|
activityId,
|
|
5105
5532
|
cwd
|
|
5106
5533
|
} = JSON.parse(body);
|
|
5107
|
-
const id = fromCLI && typeof activityId === "string" && activityId ||
|
|
5534
|
+
const id = fromCLI && typeof activityId === "string" && activityId || randomUUID4();
|
|
5108
5535
|
const entry = {
|
|
5109
5536
|
id,
|
|
5110
5537
|
toolName,
|
|
@@ -5130,7 +5557,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5130
5557
|
e.earlyReason = "No response \u2014 auto-denied after timeout";
|
|
5131
5558
|
}
|
|
5132
5559
|
pending.delete(id);
|
|
5133
|
-
broadcast("remove", { id });
|
|
5560
|
+
broadcast("remove", { id, decision: "deny" });
|
|
5134
5561
|
}
|
|
5135
5562
|
}, getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS)
|
|
5136
5563
|
};
|
|
@@ -5144,7 +5571,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5144
5571
|
status: "pending"
|
|
5145
5572
|
});
|
|
5146
5573
|
}
|
|
5147
|
-
const projectCwd = typeof cwd === "string" &&
|
|
5574
|
+
const projectCwd = typeof cwd === "string" && path17.isAbsolute(cwd) ? cwd : void 0;
|
|
5148
5575
|
const projectConfig = getConfig(projectCwd);
|
|
5149
5576
|
const browserEnabled = projectConfig.settings.approvers?.browser !== false;
|
|
5150
5577
|
const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
|
|
@@ -5157,7 +5584,10 @@ data: ${JSON.stringify(item.data)}
|
|
|
5157
5584
|
slackDelegated: entry.slackDelegated,
|
|
5158
5585
|
agent: entry.agent,
|
|
5159
5586
|
mcpServer: entry.mcpServer,
|
|
5160
|
-
interactive: terminalEnabled
|
|
5587
|
+
interactive: terminalEnabled,
|
|
5588
|
+
// allowCount = what this count will be if the user allows.
|
|
5589
|
+
// Terminal uses this to show the 💡 insight line on the Nth consecutive approval.
|
|
5590
|
+
allowCount: (insightCounts.get(toolName) ?? 0) + 1
|
|
5161
5591
|
});
|
|
5162
5592
|
const browserAlreadyOpened = process.env.NODE9_BROWSER_OPENED === "1";
|
|
5163
5593
|
if (browserEnabled && !browserOpened && !browserAlreadyOpened) {
|
|
@@ -5166,7 +5596,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5166
5596
|
}
|
|
5167
5597
|
}
|
|
5168
5598
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5169
|
-
res.end(JSON.stringify({ id }));
|
|
5599
|
+
res.end(JSON.stringify({ id, allowCount: (insightCounts.get(toolName) ?? 0) + 1 }));
|
|
5170
5600
|
if (slackDelegated) return;
|
|
5171
5601
|
authorizeHeadless(
|
|
5172
5602
|
toolName,
|
|
@@ -5193,7 +5623,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5193
5623
|
if (e.waiter) {
|
|
5194
5624
|
e.waiter(decision, result.reason);
|
|
5195
5625
|
pending.delete(id);
|
|
5196
|
-
broadcast("remove", { id });
|
|
5626
|
+
broadcast("remove", { id, decision });
|
|
5197
5627
|
} else {
|
|
5198
5628
|
e.earlyDecision = decision;
|
|
5199
5629
|
e.earlyReason = result.reason;
|
|
@@ -5209,7 +5639,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5209
5639
|
e.earlyReason = reason;
|
|
5210
5640
|
}
|
|
5211
5641
|
pending.delete(id);
|
|
5212
|
-
broadcast("remove", { id });
|
|
5642
|
+
broadcast("remove", { id, decision: "deny" });
|
|
5213
5643
|
});
|
|
5214
5644
|
return;
|
|
5215
5645
|
} catch {
|
|
@@ -5240,12 +5670,14 @@ data: ${JSON.stringify(item.data)}
|
|
|
5240
5670
|
res.end(JSON.stringify(body));
|
|
5241
5671
|
};
|
|
5242
5672
|
req.on("close", () => {
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5673
|
+
setTimeout(() => {
|
|
5674
|
+
const e = pending.get(id);
|
|
5675
|
+
if (e && e.waiter && e.earlyDecision === null) {
|
|
5676
|
+
clearTimeout(e.timer);
|
|
5677
|
+
pending.delete(id);
|
|
5678
|
+
broadcast("remove", { id });
|
|
5679
|
+
}
|
|
5680
|
+
}, 200);
|
|
5249
5681
|
});
|
|
5250
5682
|
return;
|
|
5251
5683
|
}
|
|
@@ -5274,10 +5706,10 @@ data: ${JSON.stringify(item.data)}
|
|
|
5274
5706
|
if (entry.waiter) {
|
|
5275
5707
|
entry.waiter("allow");
|
|
5276
5708
|
pending.delete(id);
|
|
5277
|
-
broadcast("remove", { id });
|
|
5709
|
+
broadcast("remove", { id, decision: "allow" });
|
|
5278
5710
|
} else {
|
|
5279
5711
|
entry.earlyDecision = "allow";
|
|
5280
|
-
broadcast("remove", { id });
|
|
5712
|
+
broadcast("remove", { id, decision: "allow" });
|
|
5281
5713
|
entry.timer = setTimeout(() => pending.delete(id), 3e4);
|
|
5282
5714
|
}
|
|
5283
5715
|
res.writeHead(200);
|
|
@@ -5291,16 +5723,29 @@ data: ${JSON.stringify(item.data)}
|
|
|
5291
5723
|
decision: resolvedDecision
|
|
5292
5724
|
});
|
|
5293
5725
|
clearTimeout(entry.timer);
|
|
5726
|
+
if (resolvedDecision === "allow" && !persist) {
|
|
5727
|
+
insightCounts.set(entry.toolName, (insightCounts.get(entry.toolName) ?? 0) + 1);
|
|
5728
|
+
saveInsightCounts();
|
|
5729
|
+
const suggestion = suggestionTracker.recordAllow(entry.toolName, entry.args);
|
|
5730
|
+
if (suggestion) {
|
|
5731
|
+
suggestions.set(suggestion.id, suggestion);
|
|
5732
|
+
broadcast("suggestion:new", suggestion);
|
|
5733
|
+
}
|
|
5734
|
+
} else if (resolvedDecision === "deny") {
|
|
5735
|
+
insightCounts.delete(entry.toolName);
|
|
5736
|
+
saveInsightCounts();
|
|
5737
|
+
suggestionTracker.resetTool(entry.toolName);
|
|
5738
|
+
}
|
|
5294
5739
|
const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
|
|
5295
5740
|
if (source && VALID_SOURCES.has(source)) entry.decisionSource = source;
|
|
5296
5741
|
if (entry.waiter) {
|
|
5297
5742
|
entry.waiter(resolvedDecision, reason);
|
|
5298
5743
|
pending.delete(id);
|
|
5299
|
-
broadcast("remove", { id });
|
|
5744
|
+
broadcast("remove", { id, decision: resolvedDecision });
|
|
5300
5745
|
} else {
|
|
5301
5746
|
entry.earlyDecision = resolvedDecision;
|
|
5302
5747
|
entry.earlyReason = reason;
|
|
5303
|
-
broadcast("remove", { id });
|
|
5748
|
+
broadcast("remove", { id, decision: resolvedDecision });
|
|
5304
5749
|
entry.timer = setTimeout(() => pending.delete(id), 3e4);
|
|
5305
5750
|
}
|
|
5306
5751
|
res.writeHead(200);
|
|
@@ -5388,13 +5833,38 @@ data: ${JSON.stringify(item.data)}
|
|
|
5388
5833
|
const id = pathname.split("/").pop();
|
|
5389
5834
|
const entry = pending.get(id);
|
|
5390
5835
|
if (!entry) return res.writeHead(404).end();
|
|
5391
|
-
const { decision } = JSON.parse(await readBody(req));
|
|
5392
|
-
|
|
5836
|
+
const { decision, source } = JSON.parse(await readBody(req));
|
|
5837
|
+
const resolvedResolveDecision = decision === "allow" ? "allow" : "deny";
|
|
5838
|
+
appendAuditLog({
|
|
5839
|
+
toolName: entry.toolName,
|
|
5840
|
+
args: entry.args,
|
|
5841
|
+
decision: resolvedResolveDecision
|
|
5842
|
+
});
|
|
5393
5843
|
clearTimeout(entry.timer);
|
|
5394
|
-
if (
|
|
5395
|
-
|
|
5844
|
+
if (resolvedResolveDecision === "allow") {
|
|
5845
|
+
insightCounts.set(entry.toolName, (insightCounts.get(entry.toolName) ?? 0) + 1);
|
|
5846
|
+
saveInsightCounts();
|
|
5847
|
+
} else {
|
|
5848
|
+
insightCounts.delete(entry.toolName);
|
|
5849
|
+
saveInsightCounts();
|
|
5850
|
+
}
|
|
5851
|
+
if (!entry.slackDelegated) {
|
|
5852
|
+
if (resolvedResolveDecision === "allow") {
|
|
5853
|
+
const suggestion = suggestionTracker.recordAllow(entry.toolName, entry.args);
|
|
5854
|
+
if (suggestion) {
|
|
5855
|
+
suggestions.set(suggestion.id, suggestion);
|
|
5856
|
+
broadcast("suggestion:new", suggestion);
|
|
5857
|
+
}
|
|
5858
|
+
} else {
|
|
5859
|
+
suggestionTracker.resetTool(entry.toolName);
|
|
5860
|
+
}
|
|
5861
|
+
}
|
|
5862
|
+
const VALID_RESOLVE_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
|
|
5863
|
+
if (source && VALID_RESOLVE_SOURCES.has(source)) entry.decisionSource = source;
|
|
5864
|
+
if (entry.waiter) entry.waiter(resolvedResolveDecision);
|
|
5865
|
+
else entry.earlyDecision = resolvedResolveDecision;
|
|
5396
5866
|
pending.delete(id);
|
|
5397
|
-
broadcast("remove", { id });
|
|
5867
|
+
broadcast("remove", { id, decision: resolvedResolveDecision });
|
|
5398
5868
|
res.writeHead(200);
|
|
5399
5869
|
return res.end(JSON.stringify({ ok: true }));
|
|
5400
5870
|
} catch {
|
|
@@ -5442,20 +5912,79 @@ data: ${JSON.stringify(item.data)}
|
|
|
5442
5912
|
res.writeHead(400).end();
|
|
5443
5913
|
}
|
|
5444
5914
|
}
|
|
5915
|
+
if (req.method === "GET" && pathname === "/suggestions") {
|
|
5916
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5917
|
+
return res.end(JSON.stringify([...suggestions.values()]));
|
|
5918
|
+
}
|
|
5919
|
+
if (req.method === "POST" && pathname.startsWith("/suggestions/") && pathname.endsWith("/apply")) {
|
|
5920
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
5921
|
+
try {
|
|
5922
|
+
const body = await readBody(req);
|
|
5923
|
+
const data = body ? JSON.parse(body) : {};
|
|
5924
|
+
const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
|
|
5925
|
+
const node9Dir = path17.dirname(GLOBAL_CONFIG_PATH);
|
|
5926
|
+
if (!path17.resolve(configPath).startsWith(node9Dir + path17.sep)) {
|
|
5927
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
5928
|
+
return res.end(
|
|
5929
|
+
JSON.stringify({ error: "configPath must be within the node9 config directory" })
|
|
5930
|
+
);
|
|
5931
|
+
}
|
|
5932
|
+
const id = pathname.split("/")[2];
|
|
5933
|
+
const suggestion = suggestions.get(id);
|
|
5934
|
+
if (!suggestion) return res.writeHead(404).end();
|
|
5935
|
+
let patch;
|
|
5936
|
+
if (data.rule !== void 0) {
|
|
5937
|
+
const parsed = SmartRuleSchema.safeParse(data.rule);
|
|
5938
|
+
if (!parsed.success) {
|
|
5939
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
5940
|
+
return res.end(JSON.stringify({ error: parsed.error.message }));
|
|
5941
|
+
}
|
|
5942
|
+
patch = { type: "smartRule", rule: parsed.data };
|
|
5943
|
+
} else {
|
|
5944
|
+
patch = suggestion.suggestedRule;
|
|
5945
|
+
}
|
|
5946
|
+
patchConfig(configPath, patch);
|
|
5947
|
+
_resetConfigCache();
|
|
5948
|
+
insightCounts.delete(suggestion.toolName);
|
|
5949
|
+
saveInsightCounts();
|
|
5950
|
+
suggestion.status = "applied";
|
|
5951
|
+
broadcast("suggestion:resolved", { id, status: "applied" });
|
|
5952
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5953
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
5954
|
+
} catch (err) {
|
|
5955
|
+
console.error(chalk2.red("[node9 daemon] POST /suggestions/:id/apply failed:"), err);
|
|
5956
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
5957
|
+
return res.end(JSON.stringify({ error: String(err) }));
|
|
5958
|
+
}
|
|
5959
|
+
}
|
|
5960
|
+
if (req.method === "POST" && pathname.startsWith("/suggestions/") && pathname.endsWith("/dismiss")) {
|
|
5961
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
5962
|
+
try {
|
|
5963
|
+
const id = pathname.split("/")[2];
|
|
5964
|
+
const suggestion = suggestions.get(id);
|
|
5965
|
+
if (!suggestion) return res.writeHead(404).end();
|
|
5966
|
+
suggestion.status = "dismissed";
|
|
5967
|
+
broadcast("suggestion:resolved", { id, status: "dismissed" });
|
|
5968
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5969
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
5970
|
+
} catch {
|
|
5971
|
+
res.writeHead(400).end();
|
|
5972
|
+
}
|
|
5973
|
+
}
|
|
5445
5974
|
res.writeHead(404).end();
|
|
5446
5975
|
});
|
|
5447
5976
|
setDaemonServer(server);
|
|
5448
5977
|
server.on("error", (e) => {
|
|
5449
5978
|
if (e.code === "EADDRINUSE") {
|
|
5450
5979
|
try {
|
|
5451
|
-
if (
|
|
5452
|
-
const { pid } = JSON.parse(
|
|
5980
|
+
if (fs14.existsSync(DAEMON_PID_FILE)) {
|
|
5981
|
+
const { pid } = JSON.parse(fs14.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
5453
5982
|
process.kill(pid, 0);
|
|
5454
5983
|
return process.exit(0);
|
|
5455
5984
|
}
|
|
5456
5985
|
} catch {
|
|
5457
5986
|
try {
|
|
5458
|
-
|
|
5987
|
+
fs14.unlinkSync(DAEMON_PID_FILE);
|
|
5459
5988
|
} catch {
|
|
5460
5989
|
}
|
|
5461
5990
|
server.listen(DAEMON_PORT, DAEMON_HOST);
|
|
@@ -5521,32 +6050,34 @@ var init_server = __esm({
|
|
|
5521
6050
|
init_shields();
|
|
5522
6051
|
init_ui2();
|
|
5523
6052
|
init_state2();
|
|
6053
|
+
init_patch();
|
|
6054
|
+
init_config_schema();
|
|
5524
6055
|
}
|
|
5525
6056
|
});
|
|
5526
6057
|
|
|
5527
6058
|
// src/daemon/index.ts
|
|
5528
|
-
import
|
|
6059
|
+
import fs15 from "fs";
|
|
5529
6060
|
import chalk3 from "chalk";
|
|
5530
6061
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
5531
6062
|
function stopDaemon() {
|
|
5532
|
-
if (!
|
|
6063
|
+
if (!fs15.existsSync(DAEMON_PID_FILE)) return console.log(chalk3.yellow("Not running."));
|
|
5533
6064
|
try {
|
|
5534
|
-
const { pid } = JSON.parse(
|
|
6065
|
+
const { pid } = JSON.parse(fs15.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
5535
6066
|
process.kill(pid, "SIGTERM");
|
|
5536
6067
|
console.log(chalk3.green("\u2705 Stopped."));
|
|
5537
6068
|
} catch {
|
|
5538
6069
|
console.log(chalk3.gray("Cleaned up stale PID file."));
|
|
5539
6070
|
} finally {
|
|
5540
6071
|
try {
|
|
5541
|
-
|
|
6072
|
+
fs15.unlinkSync(DAEMON_PID_FILE);
|
|
5542
6073
|
} catch {
|
|
5543
6074
|
}
|
|
5544
6075
|
}
|
|
5545
6076
|
}
|
|
5546
6077
|
function daemonStatus() {
|
|
5547
|
-
if (
|
|
6078
|
+
if (fs15.existsSync(DAEMON_PID_FILE)) {
|
|
5548
6079
|
try {
|
|
5549
|
-
const { pid } = JSON.parse(
|
|
6080
|
+
const { pid } = JSON.parse(fs15.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
5550
6081
|
process.kill(pid, 0);
|
|
5551
6082
|
console.log(chalk3.green("Node9 daemon: running"));
|
|
5552
6083
|
return;
|
|
@@ -5580,10 +6111,10 @@ __export(tail_exports, {
|
|
|
5580
6111
|
startTail: () => startTail
|
|
5581
6112
|
});
|
|
5582
6113
|
import http2 from "http";
|
|
5583
|
-
import
|
|
5584
|
-
import
|
|
5585
|
-
import
|
|
5586
|
-
import
|
|
6114
|
+
import chalk16 from "chalk";
|
|
6115
|
+
import fs23 from "fs";
|
|
6116
|
+
import os21 from "os";
|
|
6117
|
+
import path25 from "path";
|
|
5587
6118
|
import readline3 from "readline";
|
|
5588
6119
|
import { spawn as spawn9, execSync as execSync3 } from "child_process";
|
|
5589
6120
|
function getIcon(tool) {
|
|
@@ -5599,17 +6130,17 @@ function formatBase(activity) {
|
|
|
5599
6130
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
5600
6131
|
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
|
|
5601
6132
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
5602
|
-
return `${
|
|
6133
|
+
return `${chalk16.gray(time)} ${icon} ${chalk16.white.bold(toolName)} ${chalk16.dim(argsPreview)}`;
|
|
5603
6134
|
}
|
|
5604
6135
|
function renderResult(activity, result) {
|
|
5605
6136
|
const base = formatBase(activity);
|
|
5606
6137
|
let status;
|
|
5607
6138
|
if (result.status === "allow") {
|
|
5608
|
-
status =
|
|
6139
|
+
status = chalk16.green("\u2713 ALLOW");
|
|
5609
6140
|
} else if (result.status === "dlp") {
|
|
5610
|
-
status =
|
|
6141
|
+
status = chalk16.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
5611
6142
|
} else {
|
|
5612
|
-
status =
|
|
6143
|
+
status = chalk16.red("\u2717 BLOCK");
|
|
5613
6144
|
}
|
|
5614
6145
|
if (process.stdout.isTTY) {
|
|
5615
6146
|
readline3.clearLine(process.stdout, 0);
|
|
@@ -5619,16 +6150,16 @@ function renderResult(activity, result) {
|
|
|
5619
6150
|
}
|
|
5620
6151
|
function renderPending(activity) {
|
|
5621
6152
|
if (!process.stdout.isTTY) return;
|
|
5622
|
-
process.stdout.write(`${formatBase(activity)} ${
|
|
6153
|
+
process.stdout.write(`${formatBase(activity)} ${chalk16.yellow("\u25CF \u2026")}\r`);
|
|
5623
6154
|
}
|
|
5624
6155
|
async function ensureDaemon() {
|
|
5625
6156
|
let pidPort = null;
|
|
5626
|
-
if (
|
|
6157
|
+
if (fs23.existsSync(PID_FILE)) {
|
|
5627
6158
|
try {
|
|
5628
|
-
const { port } = JSON.parse(
|
|
6159
|
+
const { port } = JSON.parse(fs23.readFileSync(PID_FILE, "utf-8"));
|
|
5629
6160
|
pidPort = port;
|
|
5630
6161
|
} catch {
|
|
5631
|
-
console.error(
|
|
6162
|
+
console.error(chalk16.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
5632
6163
|
}
|
|
5633
6164
|
}
|
|
5634
6165
|
const checkPort = pidPort ?? DAEMON_PORT;
|
|
@@ -5639,7 +6170,7 @@ async function ensureDaemon() {
|
|
|
5639
6170
|
if (res.ok) return checkPort;
|
|
5640
6171
|
} catch {
|
|
5641
6172
|
}
|
|
5642
|
-
console.log(
|
|
6173
|
+
console.log(chalk16.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
5643
6174
|
const child = spawn9(process.execPath, [process.argv[1], "daemon"], {
|
|
5644
6175
|
detached: true,
|
|
5645
6176
|
stdio: "ignore",
|
|
@@ -5656,12 +6187,15 @@ async function ensureDaemon() {
|
|
|
5656
6187
|
} catch {
|
|
5657
6188
|
}
|
|
5658
6189
|
}
|
|
5659
|
-
console.error(
|
|
6190
|
+
console.error(chalk16.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
5660
6191
|
process.exit(1);
|
|
5661
6192
|
}
|
|
5662
|
-
function postDecisionHttp(id, decision, csrfToken, port) {
|
|
6193
|
+
function postDecisionHttp(id, decision, csrfToken, port, opts) {
|
|
5663
6194
|
return new Promise((resolve, reject) => {
|
|
5664
|
-
const
|
|
6195
|
+
const bodyObj = { decision, source: "terminal" };
|
|
6196
|
+
if (opts?.persist) bodyObj.persist = true;
|
|
6197
|
+
if (opts?.trustDuration) bodyObj.trustDuration = opts.trustDuration;
|
|
6198
|
+
const body = JSON.stringify(bodyObj);
|
|
5665
6199
|
const req = http2.request(
|
|
5666
6200
|
{
|
|
5667
6201
|
hostname: "127.0.0.1",
|
|
@@ -5684,22 +6218,30 @@ function postDecisionHttp(id, decision, csrfToken, port) {
|
|
|
5684
6218
|
req.end(body);
|
|
5685
6219
|
});
|
|
5686
6220
|
}
|
|
5687
|
-
function buildCardLines(req) {
|
|
6221
|
+
function buildCardLines(req, localCount = 0) {
|
|
5688
6222
|
const argsStr = JSON.stringify(req.args ?? {}).replace(/\s+/g, " ");
|
|
5689
6223
|
const argsPreview = argsStr.length > 60 ? argsStr.slice(0, 60) + "\u2026" : argsStr;
|
|
5690
6224
|
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`;
|
|
5691
6225
|
const blockedBy = req.riskMetadata?.blockedByLabel ?? "Policy rule";
|
|
5692
|
-
|
|
6226
|
+
const lines = [
|
|
5693
6227
|
``,
|
|
5694
6228
|
`${BOLD}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET}`,
|
|
5695
6229
|
`${CYAN}\u2551${RESET} Tool: ${BOLD}${req.toolName}${RESET}`,
|
|
5696
6230
|
`${CYAN}\u2551${RESET} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET}`,
|
|
5697
|
-
`${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}
|
|
6231
|
+
`${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}`
|
|
6232
|
+
];
|
|
6233
|
+
if (localCount >= 2) {
|
|
6234
|
+
lines.push(
|
|
6235
|
+
`${CYAN}\u2551${RESET} ${YELLOW}\u{1F4A1}${RESET} Approved ${localCount}\xD7 before \u2014 ${BOLD}[a]${RESET}${YELLOW} creates a permanent rule${RESET}`
|
|
6236
|
+
);
|
|
6237
|
+
}
|
|
6238
|
+
lines.push(
|
|
5698
6239
|
`${CYAN}\u255A${RESET}`,
|
|
5699
6240
|
``,
|
|
5700
|
-
` ${BOLD}${GREEN}[
|
|
6241
|
+
` ${BOLD}${GREEN}[\u21B5/y]${RESET} Allow ${BOLD}${RED}[n]${RESET} Deny ${BOLD}${YELLOW}[a]${RESET} Always Allow ${BOLD}${CYAN}[t]${RESET} Trust 30m`,
|
|
5701
6242
|
``
|
|
5702
|
-
|
|
6243
|
+
);
|
|
6244
|
+
return lines;
|
|
5703
6245
|
}
|
|
5704
6246
|
async function startTail(options = {}) {
|
|
5705
6247
|
const port = await ensureDaemon();
|
|
@@ -5727,7 +6269,7 @@ async function startTail(options = {}) {
|
|
|
5727
6269
|
req2.end();
|
|
5728
6270
|
});
|
|
5729
6271
|
if (result.ok) {
|
|
5730
|
-
console.log(
|
|
6272
|
+
console.log(chalk16.green("\u2713 Flight Recorder buffer cleared."));
|
|
5731
6273
|
} else if (result.code === "ECONNREFUSED") {
|
|
5732
6274
|
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
5733
6275
|
} else if (result.code === "ETIMEDOUT") {
|
|
@@ -5744,6 +6286,7 @@ async function startTail(options = {}) {
|
|
|
5744
6286
|
let cardActive = false;
|
|
5745
6287
|
let cardLineCount = 0;
|
|
5746
6288
|
let cancelActiveCard = null;
|
|
6289
|
+
const localAllowCounts = /* @__PURE__ */ new Map();
|
|
5747
6290
|
const canApprove = process.stdout.isTTY && process.stdin.isTTY;
|
|
5748
6291
|
if (canApprove) readline3.emitKeypressEvents(process.stdin);
|
|
5749
6292
|
function clearCard() {
|
|
@@ -5754,7 +6297,10 @@ async function startTail(options = {}) {
|
|
|
5754
6297
|
}
|
|
5755
6298
|
function printCard(req2) {
|
|
5756
6299
|
process.stdout.write(HIDE_CURSOR + SAVE_CURSOR);
|
|
5757
|
-
const
|
|
6300
|
+
const daemonPrior = req2.allowCount !== void 0 ? req2.allowCount - 1 : 0;
|
|
6301
|
+
const localPrior = localAllowCounts.get(req2.toolName) ?? 0;
|
|
6302
|
+
const priorCount = Math.max(daemonPrior, localPrior);
|
|
6303
|
+
const lines = buildCardLines(req2, priorCount);
|
|
5758
6304
|
for (const line of lines) process.stdout.write(line + "\n");
|
|
5759
6305
|
cardLineCount = lines.length;
|
|
5760
6306
|
}
|
|
@@ -5782,34 +6328,70 @@ async function startTail(options = {}) {
|
|
|
5782
6328
|
process.stdin.pause();
|
|
5783
6329
|
cancelActiveCard = null;
|
|
5784
6330
|
};
|
|
5785
|
-
const settle = (
|
|
6331
|
+
const settle = (action) => {
|
|
5786
6332
|
if (settled) return;
|
|
5787
6333
|
settled = true;
|
|
5788
6334
|
cleanup();
|
|
5789
|
-
|
|
6335
|
+
process.stdout.write(RESTORE_CURSOR + ERASE_DOWN);
|
|
6336
|
+
const stampedLines = buildCardLines(
|
|
6337
|
+
req2,
|
|
6338
|
+
Math.max(
|
|
6339
|
+
req2.allowCount !== void 0 ? req2.allowCount - 1 : 0,
|
|
6340
|
+
localAllowCounts.get(req2.toolName) ?? 0
|
|
6341
|
+
)
|
|
6342
|
+
);
|
|
6343
|
+
const decisionStamp = action === "always-allow" ? chalk16.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk16.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk16.green("\u2713 ALLOWED") : chalk16.red("\u2717 DENIED");
|
|
6344
|
+
stampedLines.push(` ${BOLD}\u2192${RESET} ${decisionStamp} ${GRAY}(terminal)${RESET}`, ``);
|
|
6345
|
+
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
5790
6346
|
process.stdout.write(SHOW_CURSOR);
|
|
5791
|
-
|
|
6347
|
+
cardLineCount = 0;
|
|
6348
|
+
if (action === "allow" || action === "always-allow" || action === "trust") {
|
|
6349
|
+
localAllowCounts.set(req2.toolName, (localAllowCounts.get(req2.toolName) ?? 0) + 1);
|
|
6350
|
+
} else if (action === "deny") {
|
|
6351
|
+
localAllowCounts.delete(req2.toolName);
|
|
6352
|
+
}
|
|
6353
|
+
let httpDecision;
|
|
6354
|
+
let httpOpts;
|
|
6355
|
+
if (action === "always-allow") {
|
|
6356
|
+
httpDecision = "allow";
|
|
6357
|
+
httpOpts = { persist: true };
|
|
6358
|
+
} else if (action === "trust") {
|
|
6359
|
+
httpDecision = "trust";
|
|
6360
|
+
httpOpts = { trustDuration: "30m" };
|
|
6361
|
+
} else {
|
|
6362
|
+
httpDecision = action;
|
|
6363
|
+
}
|
|
6364
|
+
postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err) => {
|
|
5792
6365
|
try {
|
|
5793
|
-
|
|
5794
|
-
|
|
6366
|
+
fs23.appendFileSync(
|
|
6367
|
+
path25.join(os21.homedir(), ".node9", "hook-debug.log"),
|
|
5795
6368
|
`[tail] POST /decision failed: ${String(err)}
|
|
5796
6369
|
`
|
|
5797
6370
|
);
|
|
5798
6371
|
} catch {
|
|
5799
6372
|
}
|
|
5800
6373
|
});
|
|
5801
|
-
const decisionLabel = decision === "allow" ? chalk15.green("\u2713 ALLOWED (terminal)") : chalk15.red("\u2717 DENIED (terminal)");
|
|
5802
|
-
console.log(`${chalk15.cyan("\u25C6")} ${chalk15.bold(req2.toolName.padEnd(16))} ${decisionLabel}`);
|
|
5803
6374
|
approvalQueue.shift();
|
|
5804
6375
|
cardActive = false;
|
|
5805
6376
|
showNextCard();
|
|
5806
6377
|
};
|
|
5807
|
-
cancelActiveCard = () => {
|
|
6378
|
+
cancelActiveCard = (externalDecision) => {
|
|
5808
6379
|
if (settled) return;
|
|
5809
6380
|
settled = true;
|
|
5810
6381
|
cleanup();
|
|
5811
|
-
|
|
6382
|
+
process.stdout.write(RESTORE_CURSOR + ERASE_DOWN);
|
|
6383
|
+
const priorCount = Math.max(
|
|
6384
|
+
req2.allowCount !== void 0 ? req2.allowCount - 1 : 0,
|
|
6385
|
+
localAllowCounts.get(req2.toolName) ?? 0
|
|
6386
|
+
);
|
|
6387
|
+
const stampedLines = buildCardLines(req2, priorCount);
|
|
6388
|
+
if (externalDecision) {
|
|
6389
|
+
const source = externalDecision === "allow" ? chalk16.green("\u2713 ALLOWED") : chalk16.red("\u2717 DENIED");
|
|
6390
|
+
stampedLines.push(` ${BOLD}\u2192${RESET} ${source} ${GRAY}(external)${RESET}`, ``);
|
|
6391
|
+
}
|
|
6392
|
+
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
5812
6393
|
process.stdout.write(SHOW_CURSOR);
|
|
6394
|
+
cardLineCount = 0;
|
|
5813
6395
|
approvalQueue.shift();
|
|
5814
6396
|
cardActive = false;
|
|
5815
6397
|
showNextCard();
|
|
@@ -5817,10 +6399,14 @@ async function startTail(options = {}) {
|
|
|
5817
6399
|
process.stdin.resume();
|
|
5818
6400
|
onKeypress = (_str, key) => {
|
|
5819
6401
|
const name = key?.name ?? "";
|
|
5820
|
-
if (name === "
|
|
6402
|
+
if (name === "y" || name === "return") {
|
|
5821
6403
|
settle("allow");
|
|
5822
|
-
} else if (name === "
|
|
6404
|
+
} else if (name === "n" || name === "d" || key?.ctrl && name === "c") {
|
|
5823
6405
|
settle("deny");
|
|
6406
|
+
} else if (name === "a") {
|
|
6407
|
+
settle("always-allow");
|
|
6408
|
+
} else if (name === "t") {
|
|
6409
|
+
settle("trust");
|
|
5824
6410
|
}
|
|
5825
6411
|
};
|
|
5826
6412
|
process.stdin.on("keypress", onKeypress);
|
|
@@ -5833,19 +6419,27 @@ async function startTail(options = {}) {
|
|
|
5833
6419
|
else if (process.platform === "win32")
|
|
5834
6420
|
execSync3(`cmd /c start "" "${dashboardUrl}"`, { stdio: "ignore" });
|
|
5835
6421
|
else execSync3(`xdg-open "${dashboardUrl}"`, { stdio: "ignore" });
|
|
6422
|
+
const intToken = getInternalToken();
|
|
6423
|
+
fetch(`http://127.0.0.1:${port}/browser-opened`, {
|
|
6424
|
+
method: "POST",
|
|
6425
|
+
headers: intToken ? { "X-Node9-Internal": intToken } : {}
|
|
6426
|
+
}).catch(() => {
|
|
6427
|
+
});
|
|
5836
6428
|
}
|
|
5837
6429
|
} catch {
|
|
5838
6430
|
}
|
|
5839
|
-
console.log(
|
|
5840
|
-
\u{1F6F0}\uFE0F Node9 tail `) +
|
|
6431
|
+
console.log(chalk16.cyan.bold(`
|
|
6432
|
+
\u{1F6F0}\uFE0F Node9 tail `) + chalk16.dim(`\u2192 ${dashboardUrl}`));
|
|
5841
6433
|
if (canApprove) {
|
|
5842
|
-
console.log(
|
|
6434
|
+
console.log(
|
|
6435
|
+
chalk16.dim("Interactive approvals: [\u21B5/y] Allow [n] Deny [a] Always Allow [t] Trust 30m")
|
|
6436
|
+
);
|
|
5843
6437
|
}
|
|
5844
6438
|
if (options.history) {
|
|
5845
|
-
console.log(
|
|
6439
|
+
console.log(chalk16.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
|
|
5846
6440
|
} else {
|
|
5847
6441
|
console.log(
|
|
5848
|
-
|
|
6442
|
+
chalk16.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
|
|
5849
6443
|
);
|
|
5850
6444
|
}
|
|
5851
6445
|
process.on("SIGINT", () => {
|
|
@@ -5855,13 +6449,13 @@ async function startTail(options = {}) {
|
|
|
5855
6449
|
readline3.clearLine(process.stdout, 0);
|
|
5856
6450
|
readline3.cursorTo(process.stdout, 0);
|
|
5857
6451
|
}
|
|
5858
|
-
console.log(
|
|
6452
|
+
console.log(chalk16.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
5859
6453
|
process.exit(0);
|
|
5860
6454
|
});
|
|
5861
6455
|
const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
|
|
5862
6456
|
const req = http2.get(sseUrl, (res) => {
|
|
5863
6457
|
if (res.statusCode !== 200) {
|
|
5864
|
-
console.error(
|
|
6458
|
+
console.error(chalk16.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
5865
6459
|
process.exit(1);
|
|
5866
6460
|
}
|
|
5867
6461
|
let currentEvent = "";
|
|
@@ -5891,7 +6485,7 @@ async function startTail(options = {}) {
|
|
|
5891
6485
|
readline3.clearLine(process.stdout, 0);
|
|
5892
6486
|
readline3.cursorTo(process.stdout, 0);
|
|
5893
6487
|
}
|
|
5894
|
-
console.log(
|
|
6488
|
+
console.log(chalk16.red("\n\u274C Daemon disconnected."));
|
|
5895
6489
|
process.exit(1);
|
|
5896
6490
|
});
|
|
5897
6491
|
});
|
|
@@ -5932,11 +6526,17 @@ async function startTail(options = {}) {
|
|
|
5932
6526
|
}
|
|
5933
6527
|
if (event === "remove") {
|
|
5934
6528
|
try {
|
|
5935
|
-
const { id } = JSON.parse(rawData);
|
|
6529
|
+
const { id, decision } = JSON.parse(rawData);
|
|
5936
6530
|
const idx = approvalQueue.findIndex((r) => r.id === id);
|
|
5937
6531
|
if (idx !== -1) {
|
|
5938
6532
|
if (idx === 0 && cardActive && cancelActiveCard) {
|
|
5939
|
-
|
|
6533
|
+
const toolName = approvalQueue[0].toolName;
|
|
6534
|
+
if (decision === "allow") {
|
|
6535
|
+
localAllowCounts.set(toolName, (localAllowCounts.get(toolName) ?? 0) + 1);
|
|
6536
|
+
} else if (decision === "deny") {
|
|
6537
|
+
localAllowCounts.delete(toolName);
|
|
6538
|
+
}
|
|
6539
|
+
cancelActiveCard(decision);
|
|
5940
6540
|
} else {
|
|
5941
6541
|
approvalQueue.splice(idx, 1);
|
|
5942
6542
|
}
|
|
@@ -5971,7 +6571,7 @@ async function startTail(options = {}) {
|
|
|
5971
6571
|
}
|
|
5972
6572
|
req.on("error", (err) => {
|
|
5973
6573
|
const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
|
|
5974
|
-
console.error(
|
|
6574
|
+
console.error(chalk16.red(`
|
|
5975
6575
|
\u274C ${msg}`));
|
|
5976
6576
|
process.exit(1);
|
|
5977
6577
|
});
|
|
@@ -5981,8 +6581,9 @@ var init_tail = __esm({
|
|
|
5981
6581
|
"src/tui/tail.ts"() {
|
|
5982
6582
|
"use strict";
|
|
5983
6583
|
init_daemon2();
|
|
6584
|
+
init_daemon();
|
|
5984
6585
|
init_core();
|
|
5985
|
-
PID_FILE =
|
|
6586
|
+
PID_FILE = path25.join(os21.homedir(), ".node9", "daemon.pid");
|
|
5986
6587
|
ICONS = {
|
|
5987
6588
|
bash: "\u{1F4BB}",
|
|
5988
6589
|
shell: "\u{1F4BB}",
|
|
@@ -6330,6 +6931,25 @@ async function setupGemini() {
|
|
|
6330
6931
|
printDaemonTip();
|
|
6331
6932
|
}
|
|
6332
6933
|
}
|
|
6934
|
+
function detectAgents(homeDir2 = os11.homedir()) {
|
|
6935
|
+
const exists = (p) => {
|
|
6936
|
+
try {
|
|
6937
|
+
return fs11.existsSync(p);
|
|
6938
|
+
} catch (err) {
|
|
6939
|
+
const code = err.code;
|
|
6940
|
+
if (code !== "ENOENT") {
|
|
6941
|
+
process.stderr.write(`[node9] detectAgents: cannot access ${p}: ${code ?? String(err)}
|
|
6942
|
+
`);
|
|
6943
|
+
}
|
|
6944
|
+
return false;
|
|
6945
|
+
}
|
|
6946
|
+
};
|
|
6947
|
+
return {
|
|
6948
|
+
claude: exists(path14.join(homeDir2, ".claude")) || exists(path14.join(homeDir2, ".claude.json")),
|
|
6949
|
+
gemini: exists(path14.join(homeDir2, ".gemini")),
|
|
6950
|
+
cursor: exists(path14.join(homeDir2, ".cursor"))
|
|
6951
|
+
};
|
|
6952
|
+
}
|
|
6333
6953
|
async function setupCursor() {
|
|
6334
6954
|
const homeDir2 = os11.homedir();
|
|
6335
6955
|
const mcpPath = path14.join(homeDir2, ".cursor", "mcp.json");
|
|
@@ -6388,10 +7008,10 @@ async function setupCursor() {
|
|
|
6388
7008
|
|
|
6389
7009
|
// src/cli.ts
|
|
6390
7010
|
init_daemon2();
|
|
6391
|
-
import
|
|
6392
|
-
import
|
|
6393
|
-
import
|
|
6394
|
-
import
|
|
7011
|
+
import chalk17 from "chalk";
|
|
7012
|
+
import fs24 from "fs";
|
|
7013
|
+
import path26 from "path";
|
|
7014
|
+
import os22 from "os";
|
|
6395
7015
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
6396
7016
|
|
|
6397
7017
|
// src/utils/duration.ts
|
|
@@ -6616,32 +7236,32 @@ init_daemon();
|
|
|
6616
7236
|
init_config();
|
|
6617
7237
|
init_policy();
|
|
6618
7238
|
import chalk5 from "chalk";
|
|
6619
|
-
import
|
|
6620
|
-
import
|
|
6621
|
-
import
|
|
7239
|
+
import fs17 from "fs";
|
|
7240
|
+
import path19 from "path";
|
|
7241
|
+
import os15 from "os";
|
|
6622
7242
|
|
|
6623
7243
|
// src/undo.ts
|
|
6624
7244
|
import { spawnSync as spawnSync4, spawn as spawn5 } from "child_process";
|
|
6625
7245
|
import crypto2 from "crypto";
|
|
6626
|
-
import
|
|
6627
|
-
import
|
|
6628
|
-
import
|
|
6629
|
-
var SNAPSHOT_STACK_PATH =
|
|
6630
|
-
var UNDO_LATEST_PATH =
|
|
7246
|
+
import fs16 from "fs";
|
|
7247
|
+
import path18 from "path";
|
|
7248
|
+
import os14 from "os";
|
|
7249
|
+
var SNAPSHOT_STACK_PATH = path18.join(os14.homedir(), ".node9", "snapshots.json");
|
|
7250
|
+
var UNDO_LATEST_PATH = path18.join(os14.homedir(), ".node9", "undo_latest.txt");
|
|
6631
7251
|
var MAX_SNAPSHOTS = 10;
|
|
6632
7252
|
var GIT_TIMEOUT = 15e3;
|
|
6633
7253
|
function readStack() {
|
|
6634
7254
|
try {
|
|
6635
|
-
if (
|
|
6636
|
-
return JSON.parse(
|
|
7255
|
+
if (fs16.existsSync(SNAPSHOT_STACK_PATH))
|
|
7256
|
+
return JSON.parse(fs16.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
|
|
6637
7257
|
} catch {
|
|
6638
7258
|
}
|
|
6639
7259
|
return [];
|
|
6640
7260
|
}
|
|
6641
7261
|
function writeStack(stack) {
|
|
6642
|
-
const dir =
|
|
6643
|
-
if (!
|
|
6644
|
-
|
|
7262
|
+
const dir = path18.dirname(SNAPSHOT_STACK_PATH);
|
|
7263
|
+
if (!fs16.existsSync(dir)) fs16.mkdirSync(dir, { recursive: true });
|
|
7264
|
+
fs16.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
6645
7265
|
}
|
|
6646
7266
|
function buildArgsSummary(tool, args) {
|
|
6647
7267
|
if (!args || typeof args !== "object") return "";
|
|
@@ -6657,7 +7277,7 @@ function buildArgsSummary(tool, args) {
|
|
|
6657
7277
|
function normalizeCwdForHash(cwd) {
|
|
6658
7278
|
let normalized;
|
|
6659
7279
|
try {
|
|
6660
|
-
normalized =
|
|
7280
|
+
normalized = fs16.realpathSync(cwd);
|
|
6661
7281
|
} catch {
|
|
6662
7282
|
normalized = cwd;
|
|
6663
7283
|
}
|
|
@@ -6667,16 +7287,16 @@ function normalizeCwdForHash(cwd) {
|
|
|
6667
7287
|
}
|
|
6668
7288
|
function getShadowRepoDir(cwd) {
|
|
6669
7289
|
const hash = crypto2.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
|
|
6670
|
-
return
|
|
7290
|
+
return path18.join(os14.homedir(), ".node9", "snapshots", hash);
|
|
6671
7291
|
}
|
|
6672
7292
|
function cleanOrphanedIndexFiles(shadowDir) {
|
|
6673
7293
|
try {
|
|
6674
7294
|
const cutoff = Date.now() - 6e4;
|
|
6675
|
-
for (const f of
|
|
7295
|
+
for (const f of fs16.readdirSync(shadowDir)) {
|
|
6676
7296
|
if (f.startsWith("index_")) {
|
|
6677
|
-
const fp =
|
|
7297
|
+
const fp = path18.join(shadowDir, f);
|
|
6678
7298
|
try {
|
|
6679
|
-
if (
|
|
7299
|
+
if (fs16.statSync(fp).mtimeMs < cutoff) fs16.unlinkSync(fp);
|
|
6680
7300
|
} catch {
|
|
6681
7301
|
}
|
|
6682
7302
|
}
|
|
@@ -6688,7 +7308,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
|
|
|
6688
7308
|
const hardcoded = [".git", ".node9"];
|
|
6689
7309
|
const lines = [...hardcoded, ...ignorePaths].join("\n");
|
|
6690
7310
|
try {
|
|
6691
|
-
|
|
7311
|
+
fs16.writeFileSync(path18.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
|
|
6692
7312
|
} catch {
|
|
6693
7313
|
}
|
|
6694
7314
|
}
|
|
@@ -6701,25 +7321,25 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
6701
7321
|
timeout: 3e3
|
|
6702
7322
|
});
|
|
6703
7323
|
if (check.status === 0) {
|
|
6704
|
-
const ptPath =
|
|
7324
|
+
const ptPath = path18.join(shadowDir, "project-path.txt");
|
|
6705
7325
|
try {
|
|
6706
|
-
const stored =
|
|
7326
|
+
const stored = fs16.readFileSync(ptPath, "utf8").trim();
|
|
6707
7327
|
if (stored === normalizedCwd) return true;
|
|
6708
7328
|
if (process.env.NODE9_DEBUG === "1")
|
|
6709
7329
|
console.error(
|
|
6710
7330
|
`[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
|
|
6711
7331
|
);
|
|
6712
|
-
|
|
7332
|
+
fs16.rmSync(shadowDir, { recursive: true, force: true });
|
|
6713
7333
|
} catch {
|
|
6714
7334
|
try {
|
|
6715
|
-
|
|
7335
|
+
fs16.writeFileSync(ptPath, normalizedCwd, "utf8");
|
|
6716
7336
|
} catch {
|
|
6717
7337
|
}
|
|
6718
7338
|
return true;
|
|
6719
7339
|
}
|
|
6720
7340
|
}
|
|
6721
7341
|
try {
|
|
6722
|
-
|
|
7342
|
+
fs16.mkdirSync(shadowDir, { recursive: true });
|
|
6723
7343
|
} catch {
|
|
6724
7344
|
}
|
|
6725
7345
|
const init = spawnSync4("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
|
|
@@ -6728,7 +7348,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
6728
7348
|
console.error("[Node9] git init --bare failed:", init.stderr?.toString());
|
|
6729
7349
|
return false;
|
|
6730
7350
|
}
|
|
6731
|
-
const configFile =
|
|
7351
|
+
const configFile = path18.join(shadowDir, "config");
|
|
6732
7352
|
spawnSync4("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
|
|
6733
7353
|
timeout: 3e3
|
|
6734
7354
|
});
|
|
@@ -6736,7 +7356,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
6736
7356
|
timeout: 3e3
|
|
6737
7357
|
});
|
|
6738
7358
|
try {
|
|
6739
|
-
|
|
7359
|
+
fs16.writeFileSync(path18.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
|
|
6740
7360
|
} catch {
|
|
6741
7361
|
}
|
|
6742
7362
|
return true;
|
|
@@ -6759,7 +7379,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
6759
7379
|
const shadowDir = getShadowRepoDir(cwd);
|
|
6760
7380
|
if (!ensureShadowRepo(shadowDir, cwd)) return null;
|
|
6761
7381
|
writeShadowExcludes(shadowDir, ignorePaths);
|
|
6762
|
-
indexFile =
|
|
7382
|
+
indexFile = path18.join(shadowDir, `index_${process.pid}_${Date.now()}`);
|
|
6763
7383
|
const shadowEnv = {
|
|
6764
7384
|
...process.env,
|
|
6765
7385
|
GIT_DIR: shadowDir,
|
|
@@ -6788,7 +7408,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
6788
7408
|
const shouldGc = stack.length % 5 === 0;
|
|
6789
7409
|
if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
|
|
6790
7410
|
writeStack(stack);
|
|
6791
|
-
|
|
7411
|
+
fs16.writeFileSync(UNDO_LATEST_PATH, commitHash);
|
|
6792
7412
|
if (shouldGc) {
|
|
6793
7413
|
spawn5("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
|
|
6794
7414
|
}
|
|
@@ -6799,7 +7419,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
6799
7419
|
} finally {
|
|
6800
7420
|
if (indexFile) {
|
|
6801
7421
|
try {
|
|
6802
|
-
|
|
7422
|
+
fs16.unlinkSync(indexFile);
|
|
6803
7423
|
} catch {
|
|
6804
7424
|
}
|
|
6805
7425
|
}
|
|
@@ -6868,9 +7488,9 @@ function applyUndo(hash, cwd) {
|
|
|
6868
7488
|
timeout: GIT_TIMEOUT
|
|
6869
7489
|
}).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
6870
7490
|
for (const file of [...tracked, ...untracked]) {
|
|
6871
|
-
const fullPath =
|
|
6872
|
-
if (!snapshotFiles.has(file) &&
|
|
6873
|
-
|
|
7491
|
+
const fullPath = path18.join(dir, file);
|
|
7492
|
+
if (!snapshotFiles.has(file) && fs16.existsSync(fullPath)) {
|
|
7493
|
+
fs16.unlinkSync(fullPath);
|
|
6874
7494
|
}
|
|
6875
7495
|
}
|
|
6876
7496
|
return true;
|
|
@@ -6894,9 +7514,9 @@ function registerCheckCommand(program2) {
|
|
|
6894
7514
|
} catch (err) {
|
|
6895
7515
|
const tempConfig = getConfig();
|
|
6896
7516
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
6897
|
-
const logPath =
|
|
7517
|
+
const logPath = path19.join(os15.homedir(), ".node9", "hook-debug.log");
|
|
6898
7518
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
6899
|
-
|
|
7519
|
+
fs17.appendFileSync(
|
|
6900
7520
|
logPath,
|
|
6901
7521
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
6902
7522
|
RAW: ${raw}
|
|
@@ -6907,10 +7527,10 @@ RAW: ${raw}
|
|
|
6907
7527
|
}
|
|
6908
7528
|
const config = getConfig(payload.cwd || void 0);
|
|
6909
7529
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
6910
|
-
const logPath =
|
|
6911
|
-
if (!
|
|
6912
|
-
|
|
6913
|
-
|
|
7530
|
+
const logPath = path19.join(os15.homedir(), ".node9", "hook-debug.log");
|
|
7531
|
+
if (!fs17.existsSync(path19.dirname(logPath)))
|
|
7532
|
+
fs17.mkdirSync(path19.dirname(logPath), { recursive: true });
|
|
7533
|
+
fs17.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
6914
7534
|
`);
|
|
6915
7535
|
}
|
|
6916
7536
|
const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
|
|
@@ -6923,8 +7543,8 @@ RAW: ${raw}
|
|
|
6923
7543
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
6924
7544
|
let ttyFd = null;
|
|
6925
7545
|
try {
|
|
6926
|
-
ttyFd =
|
|
6927
|
-
const writeTty = (line) =>
|
|
7546
|
+
ttyFd = fs17.openSync("/dev/tty", "w");
|
|
7547
|
+
const writeTty = (line) => fs17.writeSync(ttyFd, line + "\n");
|
|
6928
7548
|
if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
|
|
6929
7549
|
writeTty(chalk5.bgRed.white.bold(`
|
|
6930
7550
|
\u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
@@ -6940,7 +7560,7 @@ RAW: ${raw}
|
|
|
6940
7560
|
} finally {
|
|
6941
7561
|
if (ttyFd !== null)
|
|
6942
7562
|
try {
|
|
6943
|
-
|
|
7563
|
+
fs17.closeSync(ttyFd);
|
|
6944
7564
|
} catch {
|
|
6945
7565
|
}
|
|
6946
7566
|
}
|
|
@@ -6971,7 +7591,7 @@ RAW: ${raw}
|
|
|
6971
7591
|
if (shouldSnapshot(toolName, toolInput, config)) {
|
|
6972
7592
|
await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
|
|
6973
7593
|
}
|
|
6974
|
-
const safeCwdForAuth = typeof payload.cwd === "string" &&
|
|
7594
|
+
const safeCwdForAuth = typeof payload.cwd === "string" && path19.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
6975
7595
|
const result = await authorizeHeadless(toolName, toolInput, meta, {
|
|
6976
7596
|
cwd: safeCwdForAuth
|
|
6977
7597
|
});
|
|
@@ -6983,12 +7603,12 @@ RAW: ${raw}
|
|
|
6983
7603
|
}
|
|
6984
7604
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
6985
7605
|
try {
|
|
6986
|
-
const tty =
|
|
6987
|
-
|
|
7606
|
+
const tty = fs17.openSync("/dev/tty", "w");
|
|
7607
|
+
fs17.writeSync(
|
|
6988
7608
|
tty,
|
|
6989
7609
|
chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
|
|
6990
7610
|
);
|
|
6991
|
-
|
|
7611
|
+
fs17.closeSync(tty);
|
|
6992
7612
|
} catch {
|
|
6993
7613
|
}
|
|
6994
7614
|
const daemonReady = await autoStartDaemonAndWait();
|
|
@@ -7015,9 +7635,9 @@ RAW: ${raw}
|
|
|
7015
7635
|
});
|
|
7016
7636
|
} catch (err) {
|
|
7017
7637
|
if (process.env.NODE9_DEBUG === "1") {
|
|
7018
|
-
const logPath =
|
|
7638
|
+
const logPath = path19.join(os15.homedir(), ".node9", "hook-debug.log");
|
|
7019
7639
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
7020
|
-
|
|
7640
|
+
fs17.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
7021
7641
|
`);
|
|
7022
7642
|
}
|
|
7023
7643
|
process.exit(0);
|
|
@@ -7054,9 +7674,9 @@ RAW: ${raw}
|
|
|
7054
7674
|
init_audit();
|
|
7055
7675
|
init_config();
|
|
7056
7676
|
init_policy();
|
|
7057
|
-
import
|
|
7058
|
-
import
|
|
7059
|
-
import
|
|
7677
|
+
import fs18 from "fs";
|
|
7678
|
+
import path20 from "path";
|
|
7679
|
+
import os16 from "os";
|
|
7060
7680
|
function sanitize3(value) {
|
|
7061
7681
|
return value.replace(/[\x00-\x1F\x7F]/g, "");
|
|
7062
7682
|
}
|
|
@@ -7075,11 +7695,11 @@ function registerLogCommand(program2) {
|
|
|
7075
7695
|
decision: "allowed",
|
|
7076
7696
|
source: "post-hook"
|
|
7077
7697
|
};
|
|
7078
|
-
const logPath =
|
|
7079
|
-
if (!
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
const safeCwd = typeof payload.cwd === "string" &&
|
|
7698
|
+
const logPath = path20.join(os16.homedir(), ".node9", "audit.log");
|
|
7699
|
+
if (!fs18.existsSync(path20.dirname(logPath)))
|
|
7700
|
+
fs18.mkdirSync(path20.dirname(logPath), { recursive: true });
|
|
7701
|
+
fs18.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
7702
|
+
const safeCwd = typeof payload.cwd === "string" && path20.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
7083
7703
|
const config = getConfig(safeCwd);
|
|
7084
7704
|
if (shouldSnapshot(tool, {}, config)) {
|
|
7085
7705
|
await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
|
|
@@ -7088,9 +7708,9 @@ function registerLogCommand(program2) {
|
|
|
7088
7708
|
const msg = err instanceof Error ? err.message : String(err);
|
|
7089
7709
|
process.stderr.write(`[Node9] audit log error: ${msg}
|
|
7090
7710
|
`);
|
|
7091
|
-
const debugPath =
|
|
7711
|
+
const debugPath = path20.join(os16.homedir(), ".node9", "hook-debug.log");
|
|
7092
7712
|
try {
|
|
7093
|
-
|
|
7713
|
+
fs18.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
|
|
7094
7714
|
`);
|
|
7095
7715
|
} catch {
|
|
7096
7716
|
}
|
|
@@ -7395,13 +8015,13 @@ function registerConfigShowCommand(program2) {
|
|
|
7395
8015
|
// src/cli/commands/doctor.ts
|
|
7396
8016
|
init_daemon();
|
|
7397
8017
|
import chalk7 from "chalk";
|
|
7398
|
-
import
|
|
7399
|
-
import
|
|
7400
|
-
import
|
|
8018
|
+
import fs19 from "fs";
|
|
8019
|
+
import path21 from "path";
|
|
8020
|
+
import os17 from "os";
|
|
7401
8021
|
import { execSync as execSync2 } from "child_process";
|
|
7402
8022
|
function registerDoctorCommand(program2, version2) {
|
|
7403
8023
|
program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
7404
|
-
const homeDir2 =
|
|
8024
|
+
const homeDir2 = os17.homedir();
|
|
7405
8025
|
let failures = 0;
|
|
7406
8026
|
function pass(msg) {
|
|
7407
8027
|
console.log(chalk7.green(" \u2705 ") + msg);
|
|
@@ -7450,10 +8070,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7450
8070
|
);
|
|
7451
8071
|
}
|
|
7452
8072
|
section("Configuration");
|
|
7453
|
-
const globalConfigPath =
|
|
7454
|
-
if (
|
|
8073
|
+
const globalConfigPath = path21.join(homeDir2, ".node9", "config.json");
|
|
8074
|
+
if (fs19.existsSync(globalConfigPath)) {
|
|
7455
8075
|
try {
|
|
7456
|
-
JSON.parse(
|
|
8076
|
+
JSON.parse(fs19.readFileSync(globalConfigPath, "utf-8"));
|
|
7457
8077
|
pass("~/.node9/config.json found and valid");
|
|
7458
8078
|
} catch {
|
|
7459
8079
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -7461,10 +8081,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7461
8081
|
} else {
|
|
7462
8082
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
7463
8083
|
}
|
|
7464
|
-
const projectConfigPath =
|
|
7465
|
-
if (
|
|
8084
|
+
const projectConfigPath = path21.join(process.cwd(), "node9.config.json");
|
|
8085
|
+
if (fs19.existsSync(projectConfigPath)) {
|
|
7466
8086
|
try {
|
|
7467
|
-
JSON.parse(
|
|
8087
|
+
JSON.parse(fs19.readFileSync(projectConfigPath, "utf-8"));
|
|
7468
8088
|
pass("node9.config.json found and valid (project)");
|
|
7469
8089
|
} catch {
|
|
7470
8090
|
fail(
|
|
@@ -7473,8 +8093,8 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7473
8093
|
);
|
|
7474
8094
|
}
|
|
7475
8095
|
}
|
|
7476
|
-
const credsPath =
|
|
7477
|
-
if (
|
|
8096
|
+
const credsPath = path21.join(homeDir2, ".node9", "credentials.json");
|
|
8097
|
+
if (fs19.existsSync(credsPath)) {
|
|
7478
8098
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
7479
8099
|
} else {
|
|
7480
8100
|
warn(
|
|
@@ -7483,10 +8103,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7483
8103
|
);
|
|
7484
8104
|
}
|
|
7485
8105
|
section("Agent Hooks");
|
|
7486
|
-
const claudeSettingsPath =
|
|
7487
|
-
if (
|
|
8106
|
+
const claudeSettingsPath = path21.join(homeDir2, ".claude", "settings.json");
|
|
8107
|
+
if (fs19.existsSync(claudeSettingsPath)) {
|
|
7488
8108
|
try {
|
|
7489
|
-
const cs = JSON.parse(
|
|
8109
|
+
const cs = JSON.parse(fs19.readFileSync(claudeSettingsPath, "utf-8"));
|
|
7490
8110
|
const hasHook = cs.hooks?.PreToolUse?.some(
|
|
7491
8111
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
7492
8112
|
);
|
|
@@ -7502,10 +8122,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7502
8122
|
} else {
|
|
7503
8123
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
7504
8124
|
}
|
|
7505
|
-
const geminiSettingsPath =
|
|
7506
|
-
if (
|
|
8125
|
+
const geminiSettingsPath = path21.join(homeDir2, ".gemini", "settings.json");
|
|
8126
|
+
if (fs19.existsSync(geminiSettingsPath)) {
|
|
7507
8127
|
try {
|
|
7508
|
-
const gs = JSON.parse(
|
|
8128
|
+
const gs = JSON.parse(fs19.readFileSync(geminiSettingsPath, "utf-8"));
|
|
7509
8129
|
const hasHook = gs.hooks?.BeforeTool?.some(
|
|
7510
8130
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
7511
8131
|
);
|
|
@@ -7521,10 +8141,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7521
8141
|
} else {
|
|
7522
8142
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
7523
8143
|
}
|
|
7524
|
-
const cursorHooksPath =
|
|
7525
|
-
if (
|
|
8144
|
+
const cursorHooksPath = path21.join(homeDir2, ".cursor", "hooks.json");
|
|
8145
|
+
if (fs19.existsSync(cursorHooksPath)) {
|
|
7526
8146
|
try {
|
|
7527
|
-
const cur = JSON.parse(
|
|
8147
|
+
const cur = JSON.parse(fs19.readFileSync(cursorHooksPath, "utf-8"));
|
|
7528
8148
|
const hasHook = cur.hooks?.preToolUse?.some(
|
|
7529
8149
|
(h) => h.command?.includes("node9") || h.command?.includes("cli.js")
|
|
7530
8150
|
);
|
|
@@ -7562,9 +8182,9 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7562
8182
|
|
|
7563
8183
|
// src/cli/commands/audit.ts
|
|
7564
8184
|
import chalk8 from "chalk";
|
|
7565
|
-
import
|
|
7566
|
-
import
|
|
7567
|
-
import
|
|
8185
|
+
import fs20 from "fs";
|
|
8186
|
+
import path22 from "path";
|
|
8187
|
+
import os18 from "os";
|
|
7568
8188
|
function formatRelativeTime(timestamp) {
|
|
7569
8189
|
const diff = Date.now() - new Date(timestamp).getTime();
|
|
7570
8190
|
const sec = Math.floor(diff / 1e3);
|
|
@@ -7577,14 +8197,14 @@ function formatRelativeTime(timestamp) {
|
|
|
7577
8197
|
}
|
|
7578
8198
|
function registerAuditCommand(program2) {
|
|
7579
8199
|
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) => {
|
|
7580
|
-
const logPath =
|
|
7581
|
-
if (!
|
|
8200
|
+
const logPath = path22.join(os18.homedir(), ".node9", "audit.log");
|
|
8201
|
+
if (!fs20.existsSync(logPath)) {
|
|
7582
8202
|
console.log(
|
|
7583
8203
|
chalk8.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
7584
8204
|
);
|
|
7585
8205
|
return;
|
|
7586
8206
|
}
|
|
7587
|
-
const raw =
|
|
8207
|
+
const raw = fs20.readFileSync(logPath, "utf-8");
|
|
7588
8208
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
7589
8209
|
let entries = lines.flatMap((line) => {
|
|
7590
8210
|
try {
|
|
@@ -7704,9 +8324,42 @@ function registerDaemonCommand(program2) {
|
|
|
7704
8324
|
init_core();
|
|
7705
8325
|
init_daemon();
|
|
7706
8326
|
import chalk10 from "chalk";
|
|
7707
|
-
import
|
|
7708
|
-
import
|
|
7709
|
-
import
|
|
8327
|
+
import fs21 from "fs";
|
|
8328
|
+
import path23 from "path";
|
|
8329
|
+
import os19 from "os";
|
|
8330
|
+
function readJson2(filePath) {
|
|
8331
|
+
try {
|
|
8332
|
+
if (fs21.existsSync(filePath)) return JSON.parse(fs21.readFileSync(filePath, "utf-8"));
|
|
8333
|
+
} catch {
|
|
8334
|
+
}
|
|
8335
|
+
return null;
|
|
8336
|
+
}
|
|
8337
|
+
function isNode9Hook2(cmd) {
|
|
8338
|
+
if (!cmd) return false;
|
|
8339
|
+
return /(?:^|[\s/\\])node9 (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js (?:check|log)/.test(cmd);
|
|
8340
|
+
}
|
|
8341
|
+
function wrappedMcpServers(servers) {
|
|
8342
|
+
if (!servers) return [];
|
|
8343
|
+
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(" ")}`);
|
|
8344
|
+
}
|
|
8345
|
+
function printAgentSection(label, hookPairs, wrapped) {
|
|
8346
|
+
console.log(chalk10.bold(` ${label}`));
|
|
8347
|
+
for (const { name, present } of hookPairs) {
|
|
8348
|
+
if (present) {
|
|
8349
|
+
console.log(chalk10.green(` \u2713 ${name}`));
|
|
8350
|
+
} else {
|
|
8351
|
+
console.log(chalk10.red(` \u2717 ${name}`) + chalk10.gray(" (not wired)"));
|
|
8352
|
+
}
|
|
8353
|
+
}
|
|
8354
|
+
if (wrapped.length > 0) {
|
|
8355
|
+
console.log(chalk10.cyan(` MCP proxied:`));
|
|
8356
|
+
for (const entry of wrapped) {
|
|
8357
|
+
console.log(chalk10.gray(` \u2022 ${entry}`));
|
|
8358
|
+
}
|
|
8359
|
+
} else {
|
|
8360
|
+
console.log(chalk10.gray(` MCP proxied: none`));
|
|
8361
|
+
}
|
|
8362
|
+
}
|
|
7710
8363
|
function registerStatusCommand(program2) {
|
|
7711
8364
|
program2.command("status").description("Show current Node9 mode, policy source, and persistent decisions").action(() => {
|
|
7712
8365
|
const creds = getCredentials();
|
|
@@ -7741,19 +8394,72 @@ function registerStatusCommand(program2) {
|
|
|
7741
8394
|
console.log("");
|
|
7742
8395
|
const modeLabel = settings.mode === "audit" ? chalk10.blue("audit") : settings.mode === "strict" ? chalk10.red("strict") : chalk10.white("standard");
|
|
7743
8396
|
console.log(` Mode: ${modeLabel}`);
|
|
7744
|
-
const projectConfig =
|
|
7745
|
-
const globalConfig =
|
|
8397
|
+
const projectConfig = path23.join(process.cwd(), "node9.config.json");
|
|
8398
|
+
const globalConfig = path23.join(os19.homedir(), ".node9", "config.json");
|
|
7746
8399
|
console.log(
|
|
7747
|
-
` Local: ${
|
|
8400
|
+
` Local: ${fs21.existsSync(projectConfig) ? chalk10.green("Active (node9.config.json)") : chalk10.gray("Not present")}`
|
|
7748
8401
|
);
|
|
7749
8402
|
console.log(
|
|
7750
|
-
` Global: ${
|
|
8403
|
+
` Global: ${fs21.existsSync(globalConfig) ? chalk10.green("Active (~/.node9/config.json)") : chalk10.gray("Not present")}`
|
|
7751
8404
|
);
|
|
7752
8405
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
7753
8406
|
console.log(
|
|
7754
8407
|
` Sandbox: ${chalk10.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
7755
8408
|
);
|
|
7756
8409
|
}
|
|
8410
|
+
const homeDir2 = os19.homedir();
|
|
8411
|
+
const claudeSettings = readJson2(
|
|
8412
|
+
path23.join(homeDir2, ".claude", "settings.json")
|
|
8413
|
+
);
|
|
8414
|
+
const claudeConfig = readJson2(path23.join(homeDir2, ".claude.json"));
|
|
8415
|
+
const geminiSettings = readJson2(
|
|
8416
|
+
path23.join(homeDir2, ".gemini", "settings.json")
|
|
8417
|
+
);
|
|
8418
|
+
const cursorConfig = readJson2(path23.join(homeDir2, ".cursor", "mcp.json"));
|
|
8419
|
+
const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
|
|
8420
|
+
if (agentFound) {
|
|
8421
|
+
console.log("");
|
|
8422
|
+
console.log(chalk10.bold(" Agent Wiring:"));
|
|
8423
|
+
console.log("");
|
|
8424
|
+
if (claudeSettings || claudeConfig) {
|
|
8425
|
+
const preHook = claudeSettings?.hooks?.PreToolUse?.some(
|
|
8426
|
+
(m) => m.hooks.some((h) => isNode9Hook2(h.command))
|
|
8427
|
+
) ?? false;
|
|
8428
|
+
const postHook = claudeSettings?.hooks?.PostToolUse?.some(
|
|
8429
|
+
(m) => m.hooks.some((h) => isNode9Hook2(h.command))
|
|
8430
|
+
) ?? false;
|
|
8431
|
+
printAgentSection(
|
|
8432
|
+
"Claude Code",
|
|
8433
|
+
[
|
|
8434
|
+
{ name: "PreToolUse (node9 check)", present: preHook },
|
|
8435
|
+
{ name: "PostToolUse (node9 log)", present: postHook }
|
|
8436
|
+
],
|
|
8437
|
+
wrappedMcpServers(claudeConfig?.mcpServers)
|
|
8438
|
+
);
|
|
8439
|
+
console.log("");
|
|
8440
|
+
}
|
|
8441
|
+
if (geminiSettings) {
|
|
8442
|
+
const beforeHook = geminiSettings.hooks?.BeforeTool?.some(
|
|
8443
|
+
(m) => m.hooks.some((h) => isNode9Hook2(h.command))
|
|
8444
|
+
) ?? false;
|
|
8445
|
+
const afterHook = geminiSettings.hooks?.AfterTool?.some(
|
|
8446
|
+
(m) => m.hooks.some((h) => isNode9Hook2(h.command))
|
|
8447
|
+
) ?? false;
|
|
8448
|
+
printAgentSection(
|
|
8449
|
+
"Gemini CLI",
|
|
8450
|
+
[
|
|
8451
|
+
{ name: "BeforeTool (node9 check)", present: beforeHook },
|
|
8452
|
+
{ name: "AfterTool (node9 log)", present: afterHook }
|
|
8453
|
+
],
|
|
8454
|
+
wrappedMcpServers(geminiSettings.mcpServers)
|
|
8455
|
+
);
|
|
8456
|
+
console.log("");
|
|
8457
|
+
}
|
|
8458
|
+
if (cursorConfig) {
|
|
8459
|
+
printAgentSection("Cursor", [], wrappedMcpServers(cursorConfig.mcpServers));
|
|
8460
|
+
console.log("");
|
|
8461
|
+
}
|
|
8462
|
+
}
|
|
7757
8463
|
const pauseState = checkPause();
|
|
7758
8464
|
if (pauseState.paused) {
|
|
7759
8465
|
const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
|
|
@@ -7766,8 +8472,63 @@ function registerStatusCommand(program2) {
|
|
|
7766
8472
|
});
|
|
7767
8473
|
}
|
|
7768
8474
|
|
|
7769
|
-
// src/cli/commands/
|
|
8475
|
+
// src/cli/commands/init.ts
|
|
8476
|
+
init_core();
|
|
7770
8477
|
import chalk11 from "chalk";
|
|
8478
|
+
import fs22 from "fs";
|
|
8479
|
+
import path24 from "path";
|
|
8480
|
+
import os20 from "os";
|
|
8481
|
+
function registerInitCommand(program2) {
|
|
8482
|
+
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) => {
|
|
8483
|
+
console.log(chalk11.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
|
|
8484
|
+
const configPath = path24.join(os20.homedir(), ".node9", "config.json");
|
|
8485
|
+
if (fs22.existsSync(configPath) && !options.force) {
|
|
8486
|
+
console.log(chalk11.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
8487
|
+
} else {
|
|
8488
|
+
const requestedMode = options.mode.toLowerCase();
|
|
8489
|
+
const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
|
|
8490
|
+
const configToSave = {
|
|
8491
|
+
...DEFAULT_CONFIG,
|
|
8492
|
+
settings: { ...DEFAULT_CONFIG.settings, mode: safeMode }
|
|
8493
|
+
};
|
|
8494
|
+
const dir = path24.dirname(configPath);
|
|
8495
|
+
if (!fs22.existsSync(dir)) fs22.mkdirSync(dir, { recursive: true });
|
|
8496
|
+
fs22.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
8497
|
+
console.log(chalk11.green(`\u2705 Config created: ${configPath}`));
|
|
8498
|
+
console.log(chalk11.gray(` Mode: ${safeMode}`));
|
|
8499
|
+
}
|
|
8500
|
+
if (options.skipSetup) return;
|
|
8501
|
+
console.log("");
|
|
8502
|
+
const detected = detectAgents();
|
|
8503
|
+
const found = Object.keys(detected).filter(
|
|
8504
|
+
(k) => detected[k]
|
|
8505
|
+
);
|
|
8506
|
+
if (found.length === 0) {
|
|
8507
|
+
console.log(
|
|
8508
|
+
chalk11.gray("No AI agents detected. Install Claude Code, Gemini CLI, or Cursor")
|
|
8509
|
+
);
|
|
8510
|
+
console.log(chalk11.gray("then run: node9 addto <claude|gemini|cursor>"));
|
|
8511
|
+
return;
|
|
8512
|
+
}
|
|
8513
|
+
console.log(chalk11.bold("Detected agents:"));
|
|
8514
|
+
for (const agent of found) {
|
|
8515
|
+
console.log(chalk11.green(` \u2713 ${agent}`));
|
|
8516
|
+
}
|
|
8517
|
+
console.log("");
|
|
8518
|
+
for (const agent of found) {
|
|
8519
|
+
console.log(chalk11.bold(`Wiring ${agent}...`));
|
|
8520
|
+
if (agent === "claude") await setupClaude();
|
|
8521
|
+
else if (agent === "gemini") await setupGemini();
|
|
8522
|
+
else if (agent === "cursor") await setupCursor();
|
|
8523
|
+
console.log("");
|
|
8524
|
+
}
|
|
8525
|
+
console.log(chalk11.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
|
|
8526
|
+
console.log(chalk11.gray(" Run: node9 daemon start"));
|
|
8527
|
+
});
|
|
8528
|
+
}
|
|
8529
|
+
|
|
8530
|
+
// src/cli/commands/undo.ts
|
|
8531
|
+
import chalk12 from "chalk";
|
|
7771
8532
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
7772
8533
|
function registerUndoCommand(program2) {
|
|
7773
8534
|
program2.command("undo").description(
|
|
@@ -7779,22 +8540,22 @@ function registerUndoCommand(program2) {
|
|
|
7779
8540
|
if (history.length === 0) {
|
|
7780
8541
|
if (!options.all && allHistory.length > 0) {
|
|
7781
8542
|
console.log(
|
|
7782
|
-
|
|
8543
|
+
chalk12.yellow(
|
|
7783
8544
|
`
|
|
7784
8545
|
\u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
|
|
7785
|
-
Run ${
|
|
8546
|
+
Run ${chalk12.cyan("node9 undo --all")} to see snapshots from all projects.
|
|
7786
8547
|
`
|
|
7787
8548
|
)
|
|
7788
8549
|
);
|
|
7789
8550
|
} else {
|
|
7790
|
-
console.log(
|
|
8551
|
+
console.log(chalk12.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
|
|
7791
8552
|
}
|
|
7792
8553
|
return;
|
|
7793
8554
|
}
|
|
7794
8555
|
const idx = history.length - steps;
|
|
7795
8556
|
if (idx < 0) {
|
|
7796
8557
|
console.log(
|
|
7797
|
-
|
|
8558
|
+
chalk12.yellow(
|
|
7798
8559
|
`
|
|
7799
8560
|
\u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
|
|
7800
8561
|
`
|
|
@@ -7806,19 +8567,19 @@ function registerUndoCommand(program2) {
|
|
|
7806
8567
|
const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
|
|
7807
8568
|
const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
|
|
7808
8569
|
console.log(
|
|
7809
|
-
|
|
8570
|
+
chalk12.magenta.bold(`
|
|
7810
8571
|
\u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`)
|
|
7811
8572
|
);
|
|
7812
8573
|
console.log(
|
|
7813
|
-
|
|
7814
|
-
` Tool: ${
|
|
8574
|
+
chalk12.white(
|
|
8575
|
+
` Tool: ${chalk12.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk12.gray(" \u2192 " + snapshot.argsSummary) : ""}`
|
|
7815
8576
|
)
|
|
7816
8577
|
);
|
|
7817
|
-
console.log(
|
|
7818
|
-
console.log(
|
|
8578
|
+
console.log(chalk12.white(` When: ${chalk12.gray(ageStr)}`));
|
|
8579
|
+
console.log(chalk12.white(` Dir: ${chalk12.gray(snapshot.cwd)}`));
|
|
7819
8580
|
if (steps > 1)
|
|
7820
8581
|
console.log(
|
|
7821
|
-
|
|
8582
|
+
chalk12.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
|
|
7822
8583
|
);
|
|
7823
8584
|
console.log("");
|
|
7824
8585
|
const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
|
|
@@ -7826,21 +8587,21 @@ function registerUndoCommand(program2) {
|
|
|
7826
8587
|
const lines = diff.split("\n");
|
|
7827
8588
|
for (const line of lines) {
|
|
7828
8589
|
if (line.startsWith("+++") || line.startsWith("---")) {
|
|
7829
|
-
console.log(
|
|
8590
|
+
console.log(chalk12.bold(line));
|
|
7830
8591
|
} else if (line.startsWith("+")) {
|
|
7831
|
-
console.log(
|
|
8592
|
+
console.log(chalk12.green(line));
|
|
7832
8593
|
} else if (line.startsWith("-")) {
|
|
7833
|
-
console.log(
|
|
8594
|
+
console.log(chalk12.red(line));
|
|
7834
8595
|
} else if (line.startsWith("@@")) {
|
|
7835
|
-
console.log(
|
|
8596
|
+
console.log(chalk12.cyan(line));
|
|
7836
8597
|
} else {
|
|
7837
|
-
console.log(
|
|
8598
|
+
console.log(chalk12.gray(line));
|
|
7838
8599
|
}
|
|
7839
8600
|
}
|
|
7840
8601
|
console.log("");
|
|
7841
8602
|
} else {
|
|
7842
8603
|
console.log(
|
|
7843
|
-
|
|
8604
|
+
chalk12.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
|
|
7844
8605
|
);
|
|
7845
8606
|
}
|
|
7846
8607
|
const proceed = await confirm2({
|
|
@@ -7849,19 +8610,19 @@ function registerUndoCommand(program2) {
|
|
|
7849
8610
|
});
|
|
7850
8611
|
if (proceed) {
|
|
7851
8612
|
if (applyUndo(snapshot.hash, snapshot.cwd)) {
|
|
7852
|
-
console.log(
|
|
8613
|
+
console.log(chalk12.green("\n\u2705 Reverted successfully.\n"));
|
|
7853
8614
|
} else {
|
|
7854
|
-
console.error(
|
|
8615
|
+
console.error(chalk12.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
|
|
7855
8616
|
}
|
|
7856
8617
|
} else {
|
|
7857
|
-
console.log(
|
|
8618
|
+
console.log(chalk12.gray("\nCancelled.\n"));
|
|
7858
8619
|
}
|
|
7859
8620
|
});
|
|
7860
8621
|
}
|
|
7861
8622
|
|
|
7862
8623
|
// src/cli/commands/watch.ts
|
|
7863
8624
|
init_daemon();
|
|
7864
|
-
import
|
|
8625
|
+
import chalk13 from "chalk";
|
|
7865
8626
|
import { spawn as spawn7, spawnSync as spawnSync5 } from "child_process";
|
|
7866
8627
|
function registerWatchCommand(program2) {
|
|
7867
8628
|
program2.command("watch").description("Run a command under Node9 watch mode (daemon stays alive for the session)").argument("<command>", "Command to run").argument("[args...]", "Arguments for the command").action(async (cmd, args) => {
|
|
@@ -7877,7 +8638,7 @@ function registerWatchCommand(program2) {
|
|
|
7877
8638
|
throw new Error("not running");
|
|
7878
8639
|
}
|
|
7879
8640
|
} catch {
|
|
7880
|
-
console.error(
|
|
8641
|
+
console.error(chalk13.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
|
|
7881
8642
|
const child = spawn7(process.execPath, [process.argv[1], "daemon"], {
|
|
7882
8643
|
detached: true,
|
|
7883
8644
|
stdio: "ignore",
|
|
@@ -7899,12 +8660,12 @@ function registerWatchCommand(program2) {
|
|
|
7899
8660
|
}
|
|
7900
8661
|
}
|
|
7901
8662
|
if (!ready) {
|
|
7902
|
-
console.error(
|
|
8663
|
+
console.error(chalk13.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
7903
8664
|
process.exit(1);
|
|
7904
8665
|
}
|
|
7905
8666
|
}
|
|
7906
8667
|
console.error(
|
|
7907
|
-
|
|
8668
|
+
chalk13.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + chalk13.dim(` \u2192 localhost:${port}`) + chalk13.dim(
|
|
7908
8669
|
"\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
|
|
7909
8670
|
)
|
|
7910
8671
|
);
|
|
@@ -7913,7 +8674,7 @@ function registerWatchCommand(program2) {
|
|
|
7913
8674
|
env: { ...process.env, NODE9_WATCH_MODE: "1" }
|
|
7914
8675
|
});
|
|
7915
8676
|
if (result.error) {
|
|
7916
|
-
console.error(
|
|
8677
|
+
console.error(chalk13.red(`\u274C Failed to run command: ${result.error.message}`));
|
|
7917
8678
|
process.exit(1);
|
|
7918
8679
|
}
|
|
7919
8680
|
process.exit(result.status ?? 0);
|
|
@@ -7923,7 +8684,7 @@ function registerWatchCommand(program2) {
|
|
|
7923
8684
|
// src/mcp-gateway/index.ts
|
|
7924
8685
|
init_orchestrator();
|
|
7925
8686
|
import readline2 from "readline";
|
|
7926
|
-
import
|
|
8687
|
+
import chalk14 from "chalk";
|
|
7927
8688
|
import { spawn as spawn8 } from "child_process";
|
|
7928
8689
|
import { execa as execa2 } from "execa";
|
|
7929
8690
|
init_provenance();
|
|
@@ -7986,13 +8747,13 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
7986
8747
|
const prov = checkProvenance(executable);
|
|
7987
8748
|
if (prov.trustLevel === "suspect") {
|
|
7988
8749
|
console.error(
|
|
7989
|
-
|
|
8750
|
+
chalk14.red(
|
|
7990
8751
|
`\u26A0\uFE0F Node9: Upstream MCP server binary is suspect \u2014 ${prov.reason} (${prov.resolvedPath})`
|
|
7991
8752
|
)
|
|
7992
8753
|
);
|
|
7993
|
-
console.error(
|
|
8754
|
+
console.error(chalk14.red(" Verify this binary is trusted before proceeding."));
|
|
7994
8755
|
}
|
|
7995
|
-
console.error(
|
|
8756
|
+
console.error(chalk14.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
|
|
7996
8757
|
const UPSTREAM_INJECTOR_VARS = /* @__PURE__ */ new Set([
|
|
7997
8758
|
"NODE_OPTIONS",
|
|
7998
8759
|
"NODE_PATH",
|
|
@@ -8056,10 +8817,10 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
8056
8817
|
mcpServer
|
|
8057
8818
|
});
|
|
8058
8819
|
if (!result.approved) {
|
|
8059
|
-
console.error(
|
|
8820
|
+
console.error(chalk14.red(`
|
|
8060
8821
|
\u{1F6D1} Node9 MCP Gateway: Action Blocked`));
|
|
8061
|
-
console.error(
|
|
8062
|
-
console.error(
|
|
8822
|
+
console.error(chalk14.gray(` Tool: ${toolName}`));
|
|
8823
|
+
console.error(chalk14.gray(` Reason: ${result.reason ?? "Security Policy"}
|
|
8063
8824
|
`));
|
|
8064
8825
|
const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
|
|
8065
8826
|
const isHumanDecision = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
|
|
@@ -8133,7 +8894,7 @@ function registerMcpGatewayCommand(program2) {
|
|
|
8133
8894
|
|
|
8134
8895
|
// src/cli/commands/trust.ts
|
|
8135
8896
|
init_trusted_hosts();
|
|
8136
|
-
import
|
|
8897
|
+
import chalk15 from "chalk";
|
|
8137
8898
|
function isValidHost(host) {
|
|
8138
8899
|
return /^(\*\.)?[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/.test(host);
|
|
8139
8900
|
}
|
|
@@ -8143,44 +8904,44 @@ function registerTrustCommand(program2) {
|
|
|
8143
8904
|
const normalized = normalizeHost(host.trim());
|
|
8144
8905
|
if (!isValidHost(normalized)) {
|
|
8145
8906
|
console.error(
|
|
8146
|
-
|
|
8907
|
+
chalk15.red(`
|
|
8147
8908
|
\u274C Invalid host: "${host}"
|
|
8148
|
-
`) +
|
|
8909
|
+
`) + chalk15.gray(" Use an FQDN like api.mycompany.com or *.mycompany.com\n")
|
|
8149
8910
|
);
|
|
8150
8911
|
process.exit(1);
|
|
8151
8912
|
}
|
|
8152
8913
|
addTrustedHost(normalized);
|
|
8153
|
-
console.log(
|
|
8914
|
+
console.log(chalk15.green(`
|
|
8154
8915
|
\u2705 ${normalized} added to trusted hosts.`));
|
|
8155
8916
|
console.log(
|
|
8156
|
-
|
|
8917
|
+
chalk15.gray(" Pipe-chain blocks to this host: critical \u2192 review, high \u2192 allow\n")
|
|
8157
8918
|
);
|
|
8158
8919
|
});
|
|
8159
8920
|
trustCmd.command("remove <host>").description("Remove a trusted host").action((host) => {
|
|
8160
8921
|
const normalized = normalizeHost(host.trim());
|
|
8161
8922
|
const removed = removeTrustedHost(normalized);
|
|
8162
8923
|
if (!removed) {
|
|
8163
|
-
console.error(
|
|
8924
|
+
console.error(chalk15.yellow(`
|
|
8164
8925
|
\u26A0\uFE0F "${normalized}" is not in the trusted hosts list.
|
|
8165
8926
|
`));
|
|
8166
8927
|
process.exit(1);
|
|
8167
8928
|
}
|
|
8168
|
-
console.log(
|
|
8929
|
+
console.log(chalk15.green(`
|
|
8169
8930
|
\u2705 ${normalized} removed from trusted hosts.
|
|
8170
8931
|
`));
|
|
8171
8932
|
});
|
|
8172
8933
|
trustCmd.command("list").description("Show all trusted hosts").action(() => {
|
|
8173
8934
|
const hosts = readTrustedHosts();
|
|
8174
8935
|
if (hosts.length === 0) {
|
|
8175
|
-
console.log(
|
|
8176
|
-
console.log(` Add one: ${
|
|
8936
|
+
console.log(chalk15.gray("\n No trusted hosts configured.\n"));
|
|
8937
|
+
console.log(` Add one: ${chalk15.cyan("node9 trust add api.mycompany.com")}
|
|
8177
8938
|
`);
|
|
8178
8939
|
return;
|
|
8179
8940
|
}
|
|
8180
|
-
console.log(
|
|
8941
|
+
console.log(chalk15.bold("\n\u{1F513} Trusted Hosts\n"));
|
|
8181
8942
|
for (const entry of hosts) {
|
|
8182
8943
|
const date = new Date(entry.addedAt).toLocaleDateString();
|
|
8183
|
-
console.log(` ${
|
|
8944
|
+
console.log(` ${chalk15.cyan(entry.host.padEnd(40))} ${chalk15.gray(`added ${date}`)}`);
|
|
8184
8945
|
}
|
|
8185
8946
|
console.log("");
|
|
8186
8947
|
});
|
|
@@ -8188,20 +8949,20 @@ function registerTrustCommand(program2) {
|
|
|
8188
8949
|
|
|
8189
8950
|
// src/cli.ts
|
|
8190
8951
|
var { version } = JSON.parse(
|
|
8191
|
-
|
|
8952
|
+
fs24.readFileSync(path26.join(__dirname, "../package.json"), "utf-8")
|
|
8192
8953
|
);
|
|
8193
8954
|
var program = new Command();
|
|
8194
8955
|
program.name("node9").description("The Sudo Command for AI Agents").version(version);
|
|
8195
8956
|
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) => {
|
|
8196
8957
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
8197
|
-
const credPath =
|
|
8198
|
-
if (!
|
|
8199
|
-
|
|
8958
|
+
const credPath = path26.join(os22.homedir(), ".node9", "credentials.json");
|
|
8959
|
+
if (!fs24.existsSync(path26.dirname(credPath)))
|
|
8960
|
+
fs24.mkdirSync(path26.dirname(credPath), { recursive: true });
|
|
8200
8961
|
const profileName = options.profile || "default";
|
|
8201
8962
|
let existingCreds = {};
|
|
8202
8963
|
try {
|
|
8203
|
-
if (
|
|
8204
|
-
const raw = JSON.parse(
|
|
8964
|
+
if (fs24.existsSync(credPath)) {
|
|
8965
|
+
const raw = JSON.parse(fs24.readFileSync(credPath, "utf-8"));
|
|
8205
8966
|
if (raw.apiKey) {
|
|
8206
8967
|
existingCreds = {
|
|
8207
8968
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
|
|
@@ -8213,13 +8974,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
8213
8974
|
} catch {
|
|
8214
8975
|
}
|
|
8215
8976
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
8216
|
-
|
|
8977
|
+
fs24.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
8217
8978
|
if (profileName === "default") {
|
|
8218
|
-
const configPath =
|
|
8979
|
+
const configPath = path26.join(os22.homedir(), ".node9", "config.json");
|
|
8219
8980
|
let config = {};
|
|
8220
8981
|
try {
|
|
8221
|
-
if (
|
|
8222
|
-
config = JSON.parse(
|
|
8982
|
+
if (fs24.existsSync(configPath))
|
|
8983
|
+
config = JSON.parse(fs24.readFileSync(configPath, "utf-8"));
|
|
8223
8984
|
} catch {
|
|
8224
8985
|
}
|
|
8225
8986
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -8234,36 +8995,36 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
8234
8995
|
approvers.cloud = false;
|
|
8235
8996
|
}
|
|
8236
8997
|
s.approvers = approvers;
|
|
8237
|
-
if (!
|
|
8238
|
-
|
|
8239
|
-
|
|
8998
|
+
if (!fs24.existsSync(path26.dirname(configPath)))
|
|
8999
|
+
fs24.mkdirSync(path26.dirname(configPath), { recursive: true });
|
|
9000
|
+
fs24.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
8240
9001
|
}
|
|
8241
9002
|
if (options.profile && profileName !== "default") {
|
|
8242
|
-
console.log(
|
|
8243
|
-
console.log(
|
|
9003
|
+
console.log(chalk17.green(`\u2705 Profile "${profileName}" saved`));
|
|
9004
|
+
console.log(chalk17.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
8244
9005
|
} else if (options.local) {
|
|
8245
|
-
console.log(
|
|
8246
|
-
console.log(
|
|
9006
|
+
console.log(chalk17.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
9007
|
+
console.log(chalk17.gray(` All decisions stay on this machine.`));
|
|
8247
9008
|
} else {
|
|
8248
|
-
console.log(
|
|
8249
|
-
console.log(
|
|
9009
|
+
console.log(chalk17.green(`\u2705 Logged in \u2014 agent mode`));
|
|
9010
|
+
console.log(chalk17.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
8250
9011
|
}
|
|
8251
9012
|
});
|
|
8252
9013
|
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) => {
|
|
8253
9014
|
if (target === "gemini") return await setupGemini();
|
|
8254
9015
|
if (target === "claude") return await setupClaude();
|
|
8255
9016
|
if (target === "cursor") return await setupCursor();
|
|
8256
|
-
console.error(
|
|
9017
|
+
console.error(chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
8257
9018
|
process.exit(1);
|
|
8258
9019
|
});
|
|
8259
9020
|
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) => {
|
|
8260
9021
|
if (!target) {
|
|
8261
|
-
console.log(
|
|
8262
|
-
console.log(" Usage: " +
|
|
9022
|
+
console.log(chalk17.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
9023
|
+
console.log(" Usage: " + chalk17.white("node9 setup <target>") + "\n");
|
|
8263
9024
|
console.log(" Targets:");
|
|
8264
|
-
console.log(" " +
|
|
8265
|
-
console.log(" " +
|
|
8266
|
-
console.log(" " +
|
|
9025
|
+
console.log(" " + chalk17.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
9026
|
+
console.log(" " + chalk17.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
9027
|
+
console.log(" " + chalk17.green("cursor") + " \u2014 Cursor (hook mode)");
|
|
8267
9028
|
console.log("");
|
|
8268
9029
|
return;
|
|
8269
9030
|
}
|
|
@@ -8271,7 +9032,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
8271
9032
|
if (t === "gemini") return await setupGemini();
|
|
8272
9033
|
if (t === "claude") return await setupClaude();
|
|
8273
9034
|
if (t === "cursor") return await setupCursor();
|
|
8274
|
-
console.error(
|
|
9035
|
+
console.error(chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
8275
9036
|
process.exit(1);
|
|
8276
9037
|
});
|
|
8277
9038
|
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) => {
|
|
@@ -8280,30 +9041,30 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
|
|
|
8280
9041
|
else if (target === "gemini") fn = teardownGemini;
|
|
8281
9042
|
else if (target === "cursor") fn = teardownCursor;
|
|
8282
9043
|
else {
|
|
8283
|
-
console.error(
|
|
9044
|
+
console.error(chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
8284
9045
|
process.exit(1);
|
|
8285
9046
|
}
|
|
8286
|
-
console.log(
|
|
9047
|
+
console.log(chalk17.cyan(`
|
|
8287
9048
|
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
8288
9049
|
`));
|
|
8289
9050
|
try {
|
|
8290
9051
|
fn();
|
|
8291
9052
|
} catch (err) {
|
|
8292
|
-
console.error(
|
|
9053
|
+
console.error(chalk17.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
8293
9054
|
process.exit(1);
|
|
8294
9055
|
}
|
|
8295
|
-
console.log(
|
|
9056
|
+
console.log(chalk17.gray("\n Restart the agent for changes to take effect."));
|
|
8296
9057
|
});
|
|
8297
9058
|
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) => {
|
|
8298
|
-
console.log(
|
|
8299
|
-
console.log(
|
|
9059
|
+
console.log(chalk17.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
9060
|
+
console.log(chalk17.bold("Stopping daemon..."));
|
|
8300
9061
|
try {
|
|
8301
9062
|
stopDaemon();
|
|
8302
|
-
console.log(
|
|
9063
|
+
console.log(chalk17.green(" \u2705 Daemon stopped"));
|
|
8303
9064
|
} catch {
|
|
8304
|
-
console.log(
|
|
9065
|
+
console.log(chalk17.blue(" \u2139\uFE0F Daemon was not running"));
|
|
8305
9066
|
}
|
|
8306
|
-
console.log(
|
|
9067
|
+
console.log(chalk17.bold("\nRemoving hooks..."));
|
|
8307
9068
|
let teardownFailed = false;
|
|
8308
9069
|
for (const [label, fn] of [
|
|
8309
9070
|
["Claude", teardownClaude],
|
|
@@ -8315,45 +9076,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
8315
9076
|
} catch (err) {
|
|
8316
9077
|
teardownFailed = true;
|
|
8317
9078
|
console.error(
|
|
8318
|
-
|
|
9079
|
+
chalk17.red(
|
|
8319
9080
|
` \u26A0\uFE0F Failed to remove ${label} hooks: ${err instanceof Error ? err.message : String(err)}`
|
|
8320
9081
|
)
|
|
8321
9082
|
);
|
|
8322
9083
|
}
|
|
8323
9084
|
}
|
|
8324
9085
|
if (options.purge) {
|
|
8325
|
-
const node9Dir =
|
|
8326
|
-
if (
|
|
9086
|
+
const node9Dir = path26.join(os22.homedir(), ".node9");
|
|
9087
|
+
if (fs24.existsSync(node9Dir)) {
|
|
8327
9088
|
const confirmed = await confirm3({
|
|
8328
9089
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
8329
9090
|
default: false
|
|
8330
9091
|
});
|
|
8331
9092
|
if (confirmed) {
|
|
8332
|
-
|
|
8333
|
-
if (
|
|
9093
|
+
fs24.rmSync(node9Dir, { recursive: true });
|
|
9094
|
+
if (fs24.existsSync(node9Dir)) {
|
|
8334
9095
|
console.error(
|
|
8335
|
-
|
|
9096
|
+
chalk17.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
8336
9097
|
);
|
|
8337
9098
|
} else {
|
|
8338
|
-
console.log(
|
|
9099
|
+
console.log(chalk17.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
8339
9100
|
}
|
|
8340
9101
|
} else {
|
|
8341
|
-
console.log(
|
|
9102
|
+
console.log(chalk17.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
8342
9103
|
}
|
|
8343
9104
|
} else {
|
|
8344
|
-
console.log(
|
|
9105
|
+
console.log(chalk17.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
8345
9106
|
}
|
|
8346
9107
|
} else {
|
|
8347
9108
|
console.log(
|
|
8348
|
-
|
|
9109
|
+
chalk17.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
8349
9110
|
);
|
|
8350
9111
|
}
|
|
8351
9112
|
if (teardownFailed) {
|
|
8352
|
-
console.error(
|
|
9113
|
+
console.error(chalk17.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
8353
9114
|
process.exit(1);
|
|
8354
9115
|
}
|
|
8355
|
-
console.log(
|
|
8356
|
-
console.log(
|
|
9116
|
+
console.log(chalk17.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
|
|
9117
|
+
console.log(chalk17.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
|
|
8357
9118
|
});
|
|
8358
9119
|
registerDoctorCommand(program, version);
|
|
8359
9120
|
program.command("explain").description(
|
|
@@ -8366,7 +9127,7 @@ program.command("explain").description(
|
|
|
8366
9127
|
try {
|
|
8367
9128
|
args = JSON.parse(trimmed);
|
|
8368
9129
|
} catch {
|
|
8369
|
-
console.error(
|
|
9130
|
+
console.error(chalk17.red(`
|
|
8370
9131
|
\u274C Invalid JSON: ${trimmed}
|
|
8371
9132
|
`));
|
|
8372
9133
|
process.exit(1);
|
|
@@ -8377,83 +9138,59 @@ program.command("explain").description(
|
|
|
8377
9138
|
}
|
|
8378
9139
|
const result = await explainPolicy(tool, args);
|
|
8379
9140
|
console.log("");
|
|
8380
|
-
console.log(
|
|
9141
|
+
console.log(chalk17.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
8381
9142
|
console.log("");
|
|
8382
|
-
console.log(` ${
|
|
9143
|
+
console.log(` ${chalk17.bold("Tool:")} ${chalk17.white(result.tool)}`);
|
|
8383
9144
|
if (argsRaw) {
|
|
8384
9145
|
const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
8385
|
-
console.log(` ${
|
|
9146
|
+
console.log(` ${chalk17.bold("Input:")} ${chalk17.gray(preview)}`);
|
|
8386
9147
|
}
|
|
8387
9148
|
console.log("");
|
|
8388
|
-
console.log(
|
|
9149
|
+
console.log(chalk17.bold("Config Sources (Waterfall):"));
|
|
8389
9150
|
for (const tier of result.waterfall) {
|
|
8390
|
-
const num =
|
|
9151
|
+
const num = chalk17.gray(` ${tier.tier}.`);
|
|
8391
9152
|
const label = tier.label.padEnd(16);
|
|
8392
9153
|
let statusStr;
|
|
8393
9154
|
if (tier.tier === 1) {
|
|
8394
|
-
statusStr =
|
|
9155
|
+
statusStr = chalk17.gray(tier.note ?? "");
|
|
8395
9156
|
} else if (tier.status === "active") {
|
|
8396
|
-
const loc = tier.path ?
|
|
8397
|
-
const note = tier.note ?
|
|
8398
|
-
statusStr =
|
|
9157
|
+
const loc = tier.path ? chalk17.gray(tier.path) : "";
|
|
9158
|
+
const note = tier.note ? chalk17.gray(`(${tier.note})`) : "";
|
|
9159
|
+
statusStr = chalk17.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
|
|
8399
9160
|
} else {
|
|
8400
|
-
statusStr =
|
|
9161
|
+
statusStr = chalk17.gray("\u25CB " + (tier.note ?? "not found"));
|
|
8401
9162
|
}
|
|
8402
|
-
console.log(`${num} ${
|
|
9163
|
+
console.log(`${num} ${chalk17.white(label)} ${statusStr}`);
|
|
8403
9164
|
}
|
|
8404
9165
|
console.log("");
|
|
8405
|
-
console.log(
|
|
9166
|
+
console.log(chalk17.bold("Policy Evaluation:"));
|
|
8406
9167
|
for (const step of result.steps) {
|
|
8407
9168
|
const isFinal = step.isFinal;
|
|
8408
9169
|
let icon;
|
|
8409
|
-
if (step.outcome === "allow") icon =
|
|
8410
|
-
else if (step.outcome === "review") icon =
|
|
8411
|
-
else if (step.outcome === "skip") icon =
|
|
8412
|
-
else icon =
|
|
9170
|
+
if (step.outcome === "allow") icon = chalk17.green(" \u2705");
|
|
9171
|
+
else if (step.outcome === "review") icon = chalk17.red(" \u{1F534}");
|
|
9172
|
+
else if (step.outcome === "skip") icon = chalk17.gray(" \u2500 ");
|
|
9173
|
+
else icon = chalk17.gray(" \u25CB ");
|
|
8413
9174
|
const name = step.name.padEnd(18);
|
|
8414
|
-
const nameStr = isFinal ?
|
|
8415
|
-
const detail = isFinal ?
|
|
8416
|
-
const arrow = isFinal ?
|
|
9175
|
+
const nameStr = isFinal ? chalk17.white.bold(name) : chalk17.white(name);
|
|
9176
|
+
const detail = isFinal ? chalk17.white(step.detail) : chalk17.gray(step.detail);
|
|
9177
|
+
const arrow = isFinal ? chalk17.yellow(" \u2190 STOP") : "";
|
|
8417
9178
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
8418
9179
|
}
|
|
8419
9180
|
console.log("");
|
|
8420
9181
|
if (result.decision === "allow") {
|
|
8421
|
-
console.log(
|
|
9182
|
+
console.log(chalk17.green.bold(" Decision: \u2705 ALLOW") + chalk17.gray(" \u2014 no approval needed"));
|
|
8422
9183
|
} else {
|
|
8423
9184
|
console.log(
|
|
8424
|
-
|
|
9185
|
+
chalk17.red.bold(" Decision: \u{1F534} REVIEW") + chalk17.gray(" \u2014 human approval required")
|
|
8425
9186
|
);
|
|
8426
9187
|
if (result.blockedByLabel) {
|
|
8427
|
-
console.log(
|
|
9188
|
+
console.log(chalk17.gray(` Reason: ${result.blockedByLabel}`));
|
|
8428
9189
|
}
|
|
8429
9190
|
}
|
|
8430
9191
|
console.log("");
|
|
8431
9192
|
});
|
|
8432
|
-
program
|
|
8433
|
-
const configPath = path24.join(os20.homedir(), ".node9", "config.json");
|
|
8434
|
-
if (fs22.existsSync(configPath) && !options.force) {
|
|
8435
|
-
console.log(chalk16.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
8436
|
-
console.log(chalk16.gray(` Run with --force to overwrite.`));
|
|
8437
|
-
return;
|
|
8438
|
-
}
|
|
8439
|
-
const requestedMode = options.mode.toLowerCase();
|
|
8440
|
-
const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
|
|
8441
|
-
const configToSave = {
|
|
8442
|
-
...DEFAULT_CONFIG,
|
|
8443
|
-
settings: {
|
|
8444
|
-
...DEFAULT_CONFIG.settings,
|
|
8445
|
-
mode: safeMode
|
|
8446
|
-
}
|
|
8447
|
-
};
|
|
8448
|
-
const dir = path24.dirname(configPath);
|
|
8449
|
-
if (!fs22.existsSync(dir)) fs22.mkdirSync(dir, { recursive: true });
|
|
8450
|
-
fs22.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
8451
|
-
console.log(chalk16.green(`\u2705 Global config created: ${configPath}`));
|
|
8452
|
-
console.log(chalk16.cyan(` Mode set to: ${safeMode}`));
|
|
8453
|
-
console.log(
|
|
8454
|
-
chalk16.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
|
|
8455
|
-
);
|
|
8456
|
-
});
|
|
9193
|
+
registerInitCommand(program);
|
|
8457
9194
|
registerAuditCommand(program);
|
|
8458
9195
|
registerStatusCommand(program);
|
|
8459
9196
|
registerDaemonCommand(program);
|
|
@@ -8462,7 +9199,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
|
|
|
8462
9199
|
try {
|
|
8463
9200
|
await startTail2(options);
|
|
8464
9201
|
} catch (err) {
|
|
8465
|
-
console.error(
|
|
9202
|
+
console.error(chalk17.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
|
|
8466
9203
|
process.exit(1);
|
|
8467
9204
|
}
|
|
8468
9205
|
});
|
|
@@ -8474,7 +9211,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
8474
9211
|
const ms = parseDuration(options.duration);
|
|
8475
9212
|
if (ms === null) {
|
|
8476
9213
|
console.error(
|
|
8477
|
-
|
|
9214
|
+
chalk17.red(`
|
|
8478
9215
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
8479
9216
|
`)
|
|
8480
9217
|
);
|
|
@@ -8482,20 +9219,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
8482
9219
|
}
|
|
8483
9220
|
pauseNode9(ms, options.duration);
|
|
8484
9221
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
8485
|
-
console.log(
|
|
9222
|
+
console.log(chalk17.yellow(`
|
|
8486
9223
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
8487
|
-
console.log(
|
|
8488
|
-
console.log(
|
|
9224
|
+
console.log(chalk17.gray(` All tool calls will be allowed without review.`));
|
|
9225
|
+
console.log(chalk17.gray(` Run "node9 resume" to re-enable early.
|
|
8489
9226
|
`));
|
|
8490
9227
|
});
|
|
8491
9228
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
8492
9229
|
const { paused } = checkPause();
|
|
8493
9230
|
if (!paused) {
|
|
8494
|
-
console.log(
|
|
9231
|
+
console.log(chalk17.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
8495
9232
|
return;
|
|
8496
9233
|
}
|
|
8497
9234
|
resumeNode9();
|
|
8498
|
-
console.log(
|
|
9235
|
+
console.log(chalk17.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
8499
9236
|
});
|
|
8500
9237
|
var HOOK_BASED_AGENTS = {
|
|
8501
9238
|
claude: "claude",
|
|
@@ -8508,15 +9245,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
8508
9245
|
if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
|
|
8509
9246
|
const target = HOOK_BASED_AGENTS[firstArg2];
|
|
8510
9247
|
console.error(
|
|
8511
|
-
|
|
9248
|
+
chalk17.yellow(`
|
|
8512
9249
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
8513
9250
|
);
|
|
8514
|
-
console.error(
|
|
9251
|
+
console.error(chalk17.white(`
|
|
8515
9252
|
"${target}" uses its own hook system. Use:`));
|
|
8516
9253
|
console.error(
|
|
8517
|
-
|
|
9254
|
+
chalk17.green(` node9 addto ${target} `) + chalk17.gray("# one-time setup")
|
|
8518
9255
|
);
|
|
8519
|
-
console.error(
|
|
9256
|
+
console.error(chalk17.green(` ${target} `) + chalk17.gray("# run normally"));
|
|
8520
9257
|
process.exit(1);
|
|
8521
9258
|
}
|
|
8522
9259
|
const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
|
|
@@ -8533,7 +9270,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
8533
9270
|
}
|
|
8534
9271
|
);
|
|
8535
9272
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
8536
|
-
console.error(
|
|
9273
|
+
console.error(chalk17.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
8537
9274
|
const daemonReady = await autoStartDaemonAndWait();
|
|
8538
9275
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
8539
9276
|
}
|
|
@@ -8546,12 +9283,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
8546
9283
|
}
|
|
8547
9284
|
if (!result.approved) {
|
|
8548
9285
|
console.error(
|
|
8549
|
-
|
|
9286
|
+
chalk17.red(`
|
|
8550
9287
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
8551
9288
|
);
|
|
8552
9289
|
process.exit(1);
|
|
8553
9290
|
}
|
|
8554
|
-
console.error(
|
|
9291
|
+
console.error(chalk17.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
8555
9292
|
await runProxy(fullCommand);
|
|
8556
9293
|
} else {
|
|
8557
9294
|
program.help();
|
|
@@ -8566,9 +9303,9 @@ if (process.argv[2] !== "daemon") {
|
|
|
8566
9303
|
const isCheckHook = process.argv[2] === "check";
|
|
8567
9304
|
if (isCheckHook) {
|
|
8568
9305
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
8569
|
-
const logPath =
|
|
9306
|
+
const logPath = path26.join(os22.homedir(), ".node9", "hook-debug.log");
|
|
8570
9307
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
8571
|
-
|
|
9308
|
+
fs24.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
8572
9309
|
`);
|
|
8573
9310
|
}
|
|
8574
9311
|
process.exit(0);
|