@node9/proxy 1.3.2 → 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 +1338 -477
- package/dist/cli.mjs +1324 -463
- package/dist/index.js +168 -75
- package/dist/index.mjs +168 -75
- 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,
|
|
@@ -1589,10 +1589,96 @@ var init_ssh_parser = __esm({
|
|
|
1589
1589
|
}
|
|
1590
1590
|
});
|
|
1591
1591
|
|
|
1592
|
-
// src/
|
|
1592
|
+
// src/auth/trusted-hosts.ts
|
|
1593
1593
|
import fs6 from "fs";
|
|
1594
1594
|
import path7 from "path";
|
|
1595
1595
|
import os5 from "os";
|
|
1596
|
+
function getTrustedHostsPath() {
|
|
1597
|
+
return path7.join(os5.homedir(), ".node9", "trusted-hosts.json");
|
|
1598
|
+
}
|
|
1599
|
+
function readTrustedHosts() {
|
|
1600
|
+
try {
|
|
1601
|
+
const raw = fs6.readFileSync(getTrustedHostsPath(), "utf8");
|
|
1602
|
+
const parsed = JSON.parse(raw);
|
|
1603
|
+
return Array.isArray(parsed.hosts) ? parsed.hosts : [];
|
|
1604
|
+
} catch {
|
|
1605
|
+
return [];
|
|
1606
|
+
}
|
|
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
|
+
}
|
|
1625
|
+
function writeTrustedHosts(hosts) {
|
|
1626
|
+
const filePath = getTrustedHostsPath();
|
|
1627
|
+
fs6.mkdirSync(path7.dirname(filePath), { recursive: true });
|
|
1628
|
+
const tmp = filePath + ".node9-tmp";
|
|
1629
|
+
fs6.writeFileSync(tmp, JSON.stringify({ hosts }, null, 2), { mode: 384 });
|
|
1630
|
+
fs6.renameSync(tmp, filePath);
|
|
1631
|
+
_cache = { hosts, expiry: Date.now() + CACHE_TTL_MS, mtime: getFileMtime() };
|
|
1632
|
+
}
|
|
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
|
+
}
|
|
1643
|
+
const hosts = readTrustedHosts();
|
|
1644
|
+
if (hosts.some((h) => h.host === normalized)) return;
|
|
1645
|
+
hosts.push({ host: normalized, addedAt: Date.now(), addedBy: "user" });
|
|
1646
|
+
writeTrustedHosts(hosts);
|
|
1647
|
+
}
|
|
1648
|
+
function removeTrustedHost(host) {
|
|
1649
|
+
const hosts = readTrustedHosts();
|
|
1650
|
+
const filtered = hosts.filter((h) => h.host !== host);
|
|
1651
|
+
if (filtered.length === hosts.length) return false;
|
|
1652
|
+
writeTrustedHosts(filtered);
|
|
1653
|
+
return true;
|
|
1654
|
+
}
|
|
1655
|
+
function normalizeHost(raw) {
|
|
1656
|
+
return raw.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^[^@]+@/, "").replace(/:\d+$/, "");
|
|
1657
|
+
}
|
|
1658
|
+
function isTrustedHost(host) {
|
|
1659
|
+
const normalized = normalizeHost(host);
|
|
1660
|
+
return getCachedHosts().some((entry) => {
|
|
1661
|
+
const entryHost = entry.host.toLowerCase();
|
|
1662
|
+
if (entryHost.startsWith("*.")) {
|
|
1663
|
+
const domain = entryHost.slice(2);
|
|
1664
|
+
return normalized.endsWith("." + domain);
|
|
1665
|
+
}
|
|
1666
|
+
return normalized === entryHost;
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
var _cache, CACHE_TTL_MS;
|
|
1670
|
+
var init_trusted_hosts = __esm({
|
|
1671
|
+
"src/auth/trusted-hosts.ts"() {
|
|
1672
|
+
"use strict";
|
|
1673
|
+
_cache = null;
|
|
1674
|
+
CACHE_TTL_MS = 5e3;
|
|
1675
|
+
}
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
// src/policy/index.ts
|
|
1679
|
+
import fs7 from "fs";
|
|
1680
|
+
import path8 from "path";
|
|
1681
|
+
import os6 from "os";
|
|
1596
1682
|
import pm from "picomatch";
|
|
1597
1683
|
import { parse } from "sh-syntax";
|
|
1598
1684
|
function tokenize2(toolName) {
|
|
@@ -1608,9 +1694,9 @@ function matchesPattern(text, patterns) {
|
|
|
1608
1694
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1609
1695
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1610
1696
|
}
|
|
1611
|
-
function getNestedValue(obj,
|
|
1697
|
+
function getNestedValue(obj, path27) {
|
|
1612
1698
|
if (!obj || typeof obj !== "object") return null;
|
|
1613
|
-
return
|
|
1699
|
+
return path27.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1614
1700
|
}
|
|
1615
1701
|
function shouldSnapshot(toolName, args, config) {
|
|
1616
1702
|
if (!config.settings.enableUndo) return false;
|
|
@@ -1776,23 +1862,39 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1776
1862
|
return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
|
|
1777
1863
|
}
|
|
1778
1864
|
const pipeAnalysis = analyzePipeChain(shellCommand);
|
|
1779
|
-
if (pipeAnalysis.isPipeline) {
|
|
1865
|
+
if (pipeAnalysis.isPipeline && (pipeAnalysis.risk === "critical" || pipeAnalysis.risk === "high")) {
|
|
1866
|
+
const sinks = pipeAnalysis.sinkTargets;
|
|
1867
|
+
const allTrusted = sinks.length > 0 && sinks.every(isTrustedHost);
|
|
1780
1868
|
if (pipeAnalysis.risk === "critical") {
|
|
1869
|
+
if (allTrusted) {
|
|
1870
|
+
return {
|
|
1871
|
+
decision: "review",
|
|
1872
|
+
blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
|
|
1873
|
+
reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
|
|
1874
|
+
tier: 3
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1781
1877
|
return {
|
|
1782
1878
|
decision: "block",
|
|
1783
1879
|
blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
|
|
1784
|
-
reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${
|
|
1880
|
+
reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
1785
1881
|
tier: 3
|
|
1786
1882
|
};
|
|
1787
1883
|
}
|
|
1788
|
-
if (
|
|
1884
|
+
if (allTrusted) {
|
|
1789
1885
|
return {
|
|
1790
|
-
decision: "
|
|
1791
|
-
blockedByLabel: "Node9: Pipe-Chain
|
|
1792
|
-
reason: `Sensitive file piped to
|
|
1886
|
+
decision: "allow",
|
|
1887
|
+
blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
|
|
1888
|
+
reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
|
|
1793
1889
|
tier: 3
|
|
1794
1890
|
};
|
|
1795
1891
|
}
|
|
1892
|
+
return {
|
|
1893
|
+
decision: "review",
|
|
1894
|
+
blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
|
|
1895
|
+
reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
1896
|
+
tier: 3
|
|
1897
|
+
};
|
|
1796
1898
|
}
|
|
1797
1899
|
const firstToken = analyzed.actions[0] ?? "";
|
|
1798
1900
|
if (["ssh", "scp", "rsync"].includes(firstToken)) {
|
|
@@ -1800,7 +1902,7 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1800
1902
|
const sshHosts = extractAllSshHosts(rawTokens.slice(1));
|
|
1801
1903
|
allTokens.push(...sshHosts);
|
|
1802
1904
|
}
|
|
1803
|
-
if (firstToken &&
|
|
1905
|
+
if (firstToken && path8.posix.isAbsolute(firstToken)) {
|
|
1804
1906
|
const prov = checkProvenance(firstToken, cwd);
|
|
1805
1907
|
if (prov.trustLevel === "suspect") {
|
|
1806
1908
|
return {
|
|
@@ -1897,9 +1999,9 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1897
1999
|
}
|
|
1898
2000
|
async function explainPolicy(toolName, args) {
|
|
1899
2001
|
const steps = [];
|
|
1900
|
-
const globalPath =
|
|
1901
|
-
const projectPath =
|
|
1902
|
-
const credsPath =
|
|
2002
|
+
const globalPath = path8.join(os6.homedir(), ".node9", "config.json");
|
|
2003
|
+
const projectPath = path8.join(process.cwd(), "node9.config.json");
|
|
2004
|
+
const credsPath = path8.join(os6.homedir(), ".node9", "credentials.json");
|
|
1903
2005
|
const waterfall = [
|
|
1904
2006
|
{
|
|
1905
2007
|
tier: 1,
|
|
@@ -1910,19 +2012,19 @@ async function explainPolicy(toolName, args) {
|
|
|
1910
2012
|
{
|
|
1911
2013
|
tier: 2,
|
|
1912
2014
|
label: "Cloud policy",
|
|
1913
|
-
status:
|
|
1914
|
-
note:
|
|
2015
|
+
status: fs7.existsSync(credsPath) ? "active" : "missing",
|
|
2016
|
+
note: fs7.existsSync(credsPath) ? "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
|
|
1915
2017
|
},
|
|
1916
2018
|
{
|
|
1917
2019
|
tier: 3,
|
|
1918
2020
|
label: "Project config",
|
|
1919
|
-
status:
|
|
2021
|
+
status: fs7.existsSync(projectPath) ? "active" : "missing",
|
|
1920
2022
|
path: projectPath
|
|
1921
2023
|
},
|
|
1922
2024
|
{
|
|
1923
2025
|
tier: 4,
|
|
1924
2026
|
label: "Global config",
|
|
1925
|
-
status:
|
|
2027
|
+
status: fs7.existsSync(globalPath) ? "active" : "missing",
|
|
1926
2028
|
path: globalPath
|
|
1927
2029
|
},
|
|
1928
2030
|
{
|
|
@@ -2169,21 +2271,22 @@ var init_policy = __esm({
|
|
|
2169
2271
|
init_provenance();
|
|
2170
2272
|
init_pipe_chain();
|
|
2171
2273
|
init_ssh_parser();
|
|
2274
|
+
init_trusted_hosts();
|
|
2172
2275
|
SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
2173
2276
|
}
|
|
2174
2277
|
});
|
|
2175
2278
|
|
|
2176
2279
|
// src/auth/state.ts
|
|
2177
|
-
import
|
|
2178
|
-
import
|
|
2179
|
-
import
|
|
2280
|
+
import fs8 from "fs";
|
|
2281
|
+
import path9 from "path";
|
|
2282
|
+
import os7 from "os";
|
|
2180
2283
|
function checkPause() {
|
|
2181
2284
|
try {
|
|
2182
|
-
if (!
|
|
2183
|
-
const state = JSON.parse(
|
|
2285
|
+
if (!fs8.existsSync(PAUSED_FILE)) return { paused: false };
|
|
2286
|
+
const state = JSON.parse(fs8.readFileSync(PAUSED_FILE, "utf-8"));
|
|
2184
2287
|
if (state.expiry > 0 && Date.now() >= state.expiry) {
|
|
2185
2288
|
try {
|
|
2186
|
-
|
|
2289
|
+
fs8.unlinkSync(PAUSED_FILE);
|
|
2187
2290
|
} catch {
|
|
2188
2291
|
}
|
|
2189
2292
|
return { paused: false };
|
|
@@ -2194,11 +2297,11 @@ function checkPause() {
|
|
|
2194
2297
|
}
|
|
2195
2298
|
}
|
|
2196
2299
|
function atomicWriteSync(filePath, data, options) {
|
|
2197
|
-
const dir =
|
|
2198
|
-
if (!
|
|
2199
|
-
const tmpPath = `${filePath}.${
|
|
2200
|
-
|
|
2201
|
-
|
|
2300
|
+
const dir = path9.dirname(filePath);
|
|
2301
|
+
if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
|
|
2302
|
+
const tmpPath = `${filePath}.${os7.hostname()}.${process.pid}.tmp`;
|
|
2303
|
+
fs8.writeFileSync(tmpPath, data, options);
|
|
2304
|
+
fs8.renameSync(tmpPath, filePath);
|
|
2202
2305
|
}
|
|
2203
2306
|
function pauseNode9(durationMs, durationStr) {
|
|
2204
2307
|
const state = { expiry: Date.now() + durationMs, duration: durationStr };
|
|
@@ -2206,18 +2309,18 @@ function pauseNode9(durationMs, durationStr) {
|
|
|
2206
2309
|
}
|
|
2207
2310
|
function resumeNode9() {
|
|
2208
2311
|
try {
|
|
2209
|
-
if (
|
|
2312
|
+
if (fs8.existsSync(PAUSED_FILE)) fs8.unlinkSync(PAUSED_FILE);
|
|
2210
2313
|
} catch {
|
|
2211
2314
|
}
|
|
2212
2315
|
}
|
|
2213
2316
|
function getActiveTrustSession(toolName) {
|
|
2214
2317
|
try {
|
|
2215
|
-
if (!
|
|
2216
|
-
const trust = JSON.parse(
|
|
2318
|
+
if (!fs8.existsSync(TRUST_FILE)) return false;
|
|
2319
|
+
const trust = JSON.parse(fs8.readFileSync(TRUST_FILE, "utf-8"));
|
|
2217
2320
|
const now = Date.now();
|
|
2218
2321
|
const active = trust.entries.filter((e) => e.expiry > now);
|
|
2219
2322
|
if (active.length !== trust.entries.length) {
|
|
2220
|
-
|
|
2323
|
+
fs8.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
|
|
2221
2324
|
}
|
|
2222
2325
|
return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
|
|
2223
2326
|
} catch {
|
|
@@ -2228,8 +2331,8 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
2228
2331
|
try {
|
|
2229
2332
|
let trust = { entries: [] };
|
|
2230
2333
|
try {
|
|
2231
|
-
if (
|
|
2232
|
-
trust = JSON.parse(
|
|
2334
|
+
if (fs8.existsSync(TRUST_FILE)) {
|
|
2335
|
+
trust = JSON.parse(fs8.readFileSync(TRUST_FILE, "utf-8"));
|
|
2233
2336
|
}
|
|
2234
2337
|
} catch {
|
|
2235
2338
|
}
|
|
@@ -2245,9 +2348,9 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
2245
2348
|
}
|
|
2246
2349
|
function getPersistentDecision(toolName) {
|
|
2247
2350
|
try {
|
|
2248
|
-
const file =
|
|
2249
|
-
if (!
|
|
2250
|
-
const decisions = JSON.parse(
|
|
2351
|
+
const file = path9.join(os7.homedir(), ".node9", "decisions.json");
|
|
2352
|
+
if (!fs8.existsSync(file)) return null;
|
|
2353
|
+
const decisions = JSON.parse(fs8.readFileSync(file, "utf-8"));
|
|
2251
2354
|
const d = decisions[toolName];
|
|
2252
2355
|
if (d === "allow" || d === "deny") return d;
|
|
2253
2356
|
} catch {
|
|
@@ -2259,21 +2362,21 @@ var init_state = __esm({
|
|
|
2259
2362
|
"src/auth/state.ts"() {
|
|
2260
2363
|
"use strict";
|
|
2261
2364
|
init_policy();
|
|
2262
|
-
PAUSED_FILE =
|
|
2263
|
-
TRUST_FILE =
|
|
2365
|
+
PAUSED_FILE = path9.join(os7.homedir(), ".node9", "PAUSED");
|
|
2366
|
+
TRUST_FILE = path9.join(os7.homedir(), ".node9", "trust.json");
|
|
2264
2367
|
}
|
|
2265
2368
|
});
|
|
2266
2369
|
|
|
2267
2370
|
// src/auth/daemon.ts
|
|
2268
|
-
import
|
|
2269
|
-
import
|
|
2270
|
-
import
|
|
2371
|
+
import fs9 from "fs";
|
|
2372
|
+
import path10 from "path";
|
|
2373
|
+
import os8 from "os";
|
|
2271
2374
|
import { spawnSync } from "child_process";
|
|
2272
2375
|
function getInternalToken() {
|
|
2273
2376
|
try {
|
|
2274
|
-
const pidFile =
|
|
2275
|
-
if (!
|
|
2276
|
-
const data = JSON.parse(
|
|
2377
|
+
const pidFile = path10.join(os8.homedir(), ".node9", "daemon.pid");
|
|
2378
|
+
if (!fs9.existsSync(pidFile)) return null;
|
|
2379
|
+
const data = JSON.parse(fs9.readFileSync(pidFile, "utf-8"));
|
|
2277
2380
|
process.kill(data.pid, 0);
|
|
2278
2381
|
return data.internalToken ?? null;
|
|
2279
2382
|
} catch {
|
|
@@ -2281,10 +2384,10 @@ function getInternalToken() {
|
|
|
2281
2384
|
}
|
|
2282
2385
|
}
|
|
2283
2386
|
function isDaemonRunning() {
|
|
2284
|
-
const pidFile =
|
|
2285
|
-
if (
|
|
2387
|
+
const pidFile = path10.join(os8.homedir(), ".node9", "daemon.pid");
|
|
2388
|
+
if (fs9.existsSync(pidFile)) {
|
|
2286
2389
|
try {
|
|
2287
|
-
const { pid, port } = JSON.parse(
|
|
2390
|
+
const { pid, port } = JSON.parse(fs9.readFileSync(pidFile, "utf-8"));
|
|
2288
2391
|
if (port !== DAEMON_PORT) return false;
|
|
2289
2392
|
process.kill(pid, 0);
|
|
2290
2393
|
return true;
|
|
@@ -2325,8 +2428,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
2325
2428
|
signal: ctrl.signal
|
|
2326
2429
|
});
|
|
2327
2430
|
if (!res.ok) throw new Error("Daemon fail");
|
|
2328
|
-
const { id } = await res.json();
|
|
2329
|
-
return id;
|
|
2431
|
+
const { id, allowCount } = await res.json();
|
|
2432
|
+
return { id, allowCount: allowCount ?? 1 };
|
|
2330
2433
|
} finally {
|
|
2331
2434
|
clearTimeout(timer);
|
|
2332
2435
|
}
|
|
@@ -2365,15 +2468,15 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
|
|
|
2365
2468
|
signal: AbortSignal.timeout(3e3)
|
|
2366
2469
|
});
|
|
2367
2470
|
if (!res.ok) throw new Error("Daemon unreachable");
|
|
2368
|
-
const { id } = await res.json();
|
|
2369
|
-
return id;
|
|
2471
|
+
const { id, allowCount } = await res.json();
|
|
2472
|
+
return { id, allowCount: allowCount ?? 1 };
|
|
2370
2473
|
}
|
|
2371
|
-
async function resolveViaDaemon(id, decision, internalToken) {
|
|
2474
|
+
async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
2372
2475
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2373
2476
|
await fetch(`${base}/resolve/${id}`, {
|
|
2374
2477
|
method: "POST",
|
|
2375
2478
|
headers: { "Content-Type": "application/json", "X-Node9-Internal": internalToken },
|
|
2376
|
-
body: JSON.stringify({ decision }),
|
|
2479
|
+
body: JSON.stringify({ decision, ...source && { source } }),
|
|
2377
2480
|
signal: AbortSignal.timeout(3e3)
|
|
2378
2481
|
});
|
|
2379
2482
|
}
|
|
@@ -2387,7 +2490,7 @@ var init_daemon = __esm({
|
|
|
2387
2490
|
});
|
|
2388
2491
|
|
|
2389
2492
|
// src/context-sniper.ts
|
|
2390
|
-
import
|
|
2493
|
+
import path11 from "path";
|
|
2391
2494
|
function smartTruncate(str, maxLen = 500) {
|
|
2392
2495
|
if (str.length <= maxLen) return str;
|
|
2393
2496
|
const edge = Math.floor(maxLen / 2) - 3;
|
|
@@ -2439,7 +2542,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
|
|
|
2439
2542
|
intent = "EDIT";
|
|
2440
2543
|
if (obj.file_path) {
|
|
2441
2544
|
editFilePath = String(obj.file_path);
|
|
2442
|
-
editFileName =
|
|
2545
|
+
editFileName = path11.basename(editFilePath);
|
|
2443
2546
|
}
|
|
2444
2547
|
const result = extractContext(String(obj.new_string), matchedWord);
|
|
2445
2548
|
contextSnippet = result.snippet;
|
|
@@ -2496,7 +2599,7 @@ var init_context_sniper = __esm({
|
|
|
2496
2599
|
|
|
2497
2600
|
// src/ui/native.ts
|
|
2498
2601
|
import { spawn } from "child_process";
|
|
2499
|
-
import
|
|
2602
|
+
import path12 from "path";
|
|
2500
2603
|
function formatArgs(args, matchedField, matchedWord) {
|
|
2501
2604
|
if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
|
|
2502
2605
|
let parsed = args;
|
|
@@ -2515,7 +2618,7 @@ function formatArgs(args, matchedField, matchedWord) {
|
|
|
2515
2618
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2516
2619
|
const obj = parsed;
|
|
2517
2620
|
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
2518
|
-
const file = obj.file_path ?
|
|
2621
|
+
const file = obj.file_path ? path12.basename(String(obj.file_path)) : "file";
|
|
2519
2622
|
const oldPreview = smartTruncate(String(obj.old_string), 120);
|
|
2520
2623
|
const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
|
|
2521
2624
|
return {
|
|
@@ -2578,20 +2681,24 @@ ${smartTruncate(str, 500)}`
|
|
|
2578
2681
|
function escapePango(text) {
|
|
2579
2682
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2580
2683
|
}
|
|
2581
|
-
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
2684
|
+
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
|
|
2582
2685
|
const lines = [];
|
|
2583
2686
|
if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
|
|
2584
2687
|
lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
|
|
2585
2688
|
lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
|
|
2586
2689
|
lines.push("");
|
|
2587
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
|
+
}
|
|
2588
2695
|
if (!locked) {
|
|
2589
2696
|
lines.push("");
|
|
2590
2697
|
lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
|
|
2591
2698
|
}
|
|
2592
2699
|
return lines.join("\n");
|
|
2593
2700
|
}
|
|
2594
|
-
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
2701
|
+
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
|
|
2595
2702
|
const lines = [];
|
|
2596
2703
|
if (locked) {
|
|
2597
2704
|
lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
|
|
@@ -2603,6 +2710,12 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
2603
2710
|
lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
|
|
2604
2711
|
lines.push("");
|
|
2605
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
|
+
}
|
|
2606
2719
|
if (!locked) {
|
|
2607
2720
|
lines.push("");
|
|
2608
2721
|
lines.push(
|
|
@@ -2611,12 +2724,19 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
2611
2724
|
}
|
|
2612
2725
|
return lines.join("\n");
|
|
2613
2726
|
}
|
|
2614
|
-
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) {
|
|
2615
2728
|
if (isTestEnv()) return "deny";
|
|
2616
2729
|
const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
|
|
2617
2730
|
const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
|
|
2618
2731
|
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
|
|
2619
|
-
const message = buildPlainMessage(
|
|
2732
|
+
const message = buildPlainMessage(
|
|
2733
|
+
toolName,
|
|
2734
|
+
formattedArgs,
|
|
2735
|
+
agent,
|
|
2736
|
+
explainableLabel,
|
|
2737
|
+
locked,
|
|
2738
|
+
allowCount
|
|
2739
|
+
);
|
|
2620
2740
|
return new Promise((resolve) => {
|
|
2621
2741
|
let childProcess = null;
|
|
2622
2742
|
const onAbort = () => {
|
|
@@ -2648,7 +2768,8 @@ end run`;
|
|
|
2648
2768
|
formattedArgs,
|
|
2649
2769
|
agent,
|
|
2650
2770
|
explainableLabel,
|
|
2651
|
-
locked
|
|
2771
|
+
locked,
|
|
2772
|
+
allowCount
|
|
2652
2773
|
);
|
|
2653
2774
|
const argsList = [
|
|
2654
2775
|
locked ? "--info" : "--question",
|
|
@@ -2700,8 +2821,8 @@ var init_native = __esm({
|
|
|
2700
2821
|
});
|
|
2701
2822
|
|
|
2702
2823
|
// src/auth/cloud.ts
|
|
2703
|
-
import
|
|
2704
|
-
import
|
|
2824
|
+
import fs10 from "fs";
|
|
2825
|
+
import os9 from "os";
|
|
2705
2826
|
function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
2706
2827
|
return fetch(`${creds.apiUrl}/audit`, {
|
|
2707
2828
|
method: "POST",
|
|
@@ -2713,9 +2834,9 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
2713
2834
|
context: {
|
|
2714
2835
|
agent: meta?.agent,
|
|
2715
2836
|
mcpServer: meta?.mcpServer,
|
|
2716
|
-
hostname:
|
|
2837
|
+
hostname: os9.hostname(),
|
|
2717
2838
|
cwd: process.cwd(),
|
|
2718
|
-
platform:
|
|
2839
|
+
platform: os9.platform()
|
|
2719
2840
|
}
|
|
2720
2841
|
}),
|
|
2721
2842
|
signal: AbortSignal.timeout(5e3)
|
|
@@ -2736,9 +2857,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
|
2736
2857
|
context: {
|
|
2737
2858
|
agent: meta?.agent,
|
|
2738
2859
|
mcpServer: meta?.mcpServer,
|
|
2739
|
-
hostname:
|
|
2860
|
+
hostname: os9.hostname(),
|
|
2740
2861
|
cwd: process.cwd(),
|
|
2741
|
-
platform:
|
|
2862
|
+
platform: os9.platform()
|
|
2742
2863
|
},
|
|
2743
2864
|
...riskMetadata && { riskMetadata }
|
|
2744
2865
|
}),
|
|
@@ -2794,14 +2915,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
2794
2915
|
});
|
|
2795
2916
|
clearTimeout(timer);
|
|
2796
2917
|
if (!res.ok) {
|
|
2797
|
-
|
|
2918
|
+
fs10.appendFileSync(
|
|
2798
2919
|
HOOK_DEBUG_LOG,
|
|
2799
2920
|
`[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
|
|
2800
2921
|
`
|
|
2801
2922
|
);
|
|
2802
2923
|
}
|
|
2803
2924
|
} catch (err) {
|
|
2804
|
-
|
|
2925
|
+
fs10.appendFileSync(
|
|
2805
2926
|
HOOK_DEBUG_LOG,
|
|
2806
2927
|
`[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
|
|
2807
2928
|
`
|
|
@@ -2817,8 +2938,8 @@ var init_cloud = __esm({
|
|
|
2817
2938
|
|
|
2818
2939
|
// src/auth/orchestrator.ts
|
|
2819
2940
|
import net from "net";
|
|
2820
|
-
import
|
|
2821
|
-
import
|
|
2941
|
+
import path13 from "path";
|
|
2942
|
+
import os10 from "os";
|
|
2822
2943
|
import { randomUUID } from "crypto";
|
|
2823
2944
|
function notifyActivity(data) {
|
|
2824
2945
|
return new Promise((resolve) => {
|
|
@@ -3012,13 +3133,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3012
3133
|
let viewerId = null;
|
|
3013
3134
|
const internalToken = getInternalToken();
|
|
3014
3135
|
let daemonEntryId = null;
|
|
3136
|
+
let daemonAllowCount = 1;
|
|
3015
3137
|
if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
|
|
3016
3138
|
if (cloudEnforced && cloudRequestId) {
|
|
3017
|
-
|
|
3139
|
+
const viewer = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
|
|
3140
|
+
viewerId = viewer?.id ?? null;
|
|
3018
3141
|
daemonEntryId = viewerId;
|
|
3142
|
+
if (viewer) daemonAllowCount = viewer.allowCount;
|
|
3019
3143
|
} else {
|
|
3020
3144
|
try {
|
|
3021
|
-
|
|
3145
|
+
const entry = await registerDaemonEntry(
|
|
3022
3146
|
toolName,
|
|
3023
3147
|
args,
|
|
3024
3148
|
meta,
|
|
@@ -3026,6 +3150,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3026
3150
|
options?.activityId,
|
|
3027
3151
|
options?.cwd
|
|
3028
3152
|
);
|
|
3153
|
+
daemonEntryId = entry.id;
|
|
3154
|
+
daemonAllowCount = entry.allowCount;
|
|
3029
3155
|
} catch {
|
|
3030
3156
|
}
|
|
3031
3157
|
}
|
|
@@ -3061,7 +3187,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3061
3187
|
false,
|
|
3062
3188
|
signal,
|
|
3063
3189
|
policyMatchedField,
|
|
3064
|
-
policyMatchedWord
|
|
3190
|
+
policyMatchedWord,
|
|
3191
|
+
daemonAllowCount
|
|
3065
3192
|
);
|
|
3066
3193
|
if (decision === "always_allow") {
|
|
3067
3194
|
writeTrustSession(toolName, 36e5);
|
|
@@ -3119,10 +3246,13 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
3119
3246
|
if (!resolved) {
|
|
3120
3247
|
resolved = true;
|
|
3121
3248
|
abortController.abort();
|
|
3122
|
-
if (
|
|
3123
|
-
resolveViaDaemon(
|
|
3124
|
-
|
|
3125
|
-
|
|
3249
|
+
if (daemonEntryId && internalToken) {
|
|
3250
|
+
resolveViaDaemon(
|
|
3251
|
+
daemonEntryId,
|
|
3252
|
+
res.approved ? "allow" : "deny",
|
|
3253
|
+
internalToken,
|
|
3254
|
+
res.decisionSource
|
|
3255
|
+
).catch(() => null);
|
|
3126
3256
|
}
|
|
3127
3257
|
resolve(res);
|
|
3128
3258
|
}
|
|
@@ -3179,7 +3309,7 @@ var init_orchestrator = __esm({
|
|
|
3179
3309
|
init_state();
|
|
3180
3310
|
init_daemon();
|
|
3181
3311
|
init_cloud();
|
|
3182
|
-
ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
3312
|
+
ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path13.join(os10.tmpdir(), "node9-activity.sock");
|
|
3183
3313
|
}
|
|
3184
3314
|
});
|
|
3185
3315
|
|
|
@@ -3475,6 +3605,15 @@ var init_ui = __esm({
|
|
|
3475
3605
|
padding: 5px 10px;
|
|
3476
3606
|
margin-bottom: 14px;
|
|
3477
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
|
+
}
|
|
3478
3617
|
pre {
|
|
3479
3618
|
background: #0d1117;
|
|
3480
3619
|
padding: 14px 16px;
|
|
@@ -3947,6 +4086,78 @@ var init_ui = __esm({
|
|
|
3947
4086
|
color: var(--danger);
|
|
3948
4087
|
}
|
|
3949
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
|
+
|
|
3950
4161
|
.modal-overlay {
|
|
3951
4162
|
display: none;
|
|
3952
4163
|
position: fixed;
|
|
@@ -4128,6 +4339,11 @@ var init_ui = __esm({
|
|
|
4128
4339
|
<div class="panel-title">\u{1F4CB} Persistent Decisions</div>
|
|
4129
4340
|
<div id="decisionsList"><span class="decisions-empty">None yet.</span></div>
|
|
4130
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>
|
|
4131
4347
|
</div>
|
|
4132
4348
|
</div>
|
|
4133
4349
|
</div>
|
|
@@ -4317,6 +4533,7 @@ var init_ui = __esm({
|
|
|
4317
4533
|
</div>
|
|
4318
4534
|
<div class="tool-chip">\${esc(req.toolName)}</div>
|
|
4319
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>\` : ''}
|
|
4320
4537
|
\${renderPayload(req)}
|
|
4321
4538
|
<div class="actions" id="act-\${req.id}">
|
|
4322
4539
|
<button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>\u2705 Allow this Action</button>
|
|
@@ -4383,6 +4600,14 @@ var init_ui = __esm({
|
|
|
4383
4600
|
ev.addEventListener('shields-status', (e) => {
|
|
4384
4601
|
renderShields(JSON.parse(e.data).shields);
|
|
4385
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
|
+
});
|
|
4386
4611
|
|
|
4387
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
|
|
4388
4613
|
ev.addEventListener('activity', (e) => {
|
|
@@ -4632,6 +4857,74 @@ var init_ui = __esm({
|
|
|
4632
4857
|
.then((r) => r.json())
|
|
4633
4858
|
.then(renderDecisions)
|
|
4634
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(() => {});
|
|
4635
4928
|
</script>
|
|
4636
4929
|
</body>
|
|
4637
4930
|
</html>
|
|
@@ -4649,13 +4942,123 @@ var init_ui2 = __esm({
|
|
|
4649
4942
|
}
|
|
4650
4943
|
});
|
|
4651
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
|
+
|
|
4652
5035
|
// src/daemon/state.ts
|
|
4653
5036
|
import net2 from "net";
|
|
4654
|
-
import
|
|
4655
|
-
import
|
|
4656
|
-
import
|
|
5037
|
+
import fs12 from "fs";
|
|
5038
|
+
import path15 from "path";
|
|
5039
|
+
import os12 from "os";
|
|
4657
5040
|
import { spawn as spawn2 } from "child_process";
|
|
4658
|
-
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
|
+
}
|
|
4659
5062
|
function getAbandonTimer() {
|
|
4660
5063
|
return _abandonTimer;
|
|
4661
5064
|
}
|
|
@@ -4678,11 +5081,27 @@ function markRejectionHandlerRegistered() {
|
|
|
4678
5081
|
daemonRejectionHandlerRegistered = true;
|
|
4679
5082
|
}
|
|
4680
5083
|
function atomicWriteSync2(filePath, data, options) {
|
|
4681
|
-
const dir =
|
|
4682
|
-
if (!
|
|
4683
|
-
const tmpPath = `${filePath}.${
|
|
4684
|
-
|
|
4685
|
-
|
|
5084
|
+
const dir = path15.dirname(filePath);
|
|
5085
|
+
if (!fs12.existsSync(dir)) fs12.mkdirSync(dir, { recursive: true });
|
|
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
|
+
}
|
|
4686
5105
|
}
|
|
4687
5106
|
function redactArgs(value) {
|
|
4688
5107
|
if (!value || typeof value !== "object") return value;
|
|
@@ -4702,16 +5121,16 @@ function appendAuditLog(data) {
|
|
|
4702
5121
|
decision: data.decision,
|
|
4703
5122
|
source: "daemon"
|
|
4704
5123
|
};
|
|
4705
|
-
const dir =
|
|
4706
|
-
if (!
|
|
4707
|
-
|
|
5124
|
+
const dir = path15.dirname(AUDIT_LOG_FILE);
|
|
5125
|
+
if (!fs12.existsSync(dir)) fs12.mkdirSync(dir, { recursive: true });
|
|
5126
|
+
fs12.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
|
|
4708
5127
|
} catch {
|
|
4709
5128
|
}
|
|
4710
5129
|
}
|
|
4711
5130
|
function getAuditHistory(limit = 20) {
|
|
4712
5131
|
try {
|
|
4713
|
-
if (!
|
|
4714
|
-
const lines =
|
|
5132
|
+
if (!fs12.existsSync(AUDIT_LOG_FILE)) return [];
|
|
5133
|
+
const lines = fs12.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
|
|
4715
5134
|
if (lines.length === 1 && lines[0] === "") return [];
|
|
4716
5135
|
return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
|
|
4717
5136
|
} catch {
|
|
@@ -4720,19 +5139,19 @@ function getAuditHistory(limit = 20) {
|
|
|
4720
5139
|
}
|
|
4721
5140
|
function getOrgName() {
|
|
4722
5141
|
try {
|
|
4723
|
-
if (
|
|
5142
|
+
if (fs12.existsSync(CREDENTIALS_FILE)) return "Node9 Cloud";
|
|
4724
5143
|
} catch {
|
|
4725
5144
|
}
|
|
4726
5145
|
return null;
|
|
4727
5146
|
}
|
|
4728
5147
|
function hasStoredSlackKey() {
|
|
4729
|
-
return
|
|
5148
|
+
return fs12.existsSync(CREDENTIALS_FILE);
|
|
4730
5149
|
}
|
|
4731
5150
|
function writeGlobalSetting(key, value) {
|
|
4732
5151
|
let config = {};
|
|
4733
5152
|
try {
|
|
4734
|
-
if (
|
|
4735
|
-
config = JSON.parse(
|
|
5153
|
+
if (fs12.existsSync(GLOBAL_CONFIG_FILE)) {
|
|
5154
|
+
config = JSON.parse(fs12.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
|
|
4736
5155
|
}
|
|
4737
5156
|
} catch {
|
|
4738
5157
|
}
|
|
@@ -4744,8 +5163,8 @@ function writeTrustEntry(toolName, durationMs) {
|
|
|
4744
5163
|
try {
|
|
4745
5164
|
let trust = { entries: [] };
|
|
4746
5165
|
try {
|
|
4747
|
-
if (
|
|
4748
|
-
trust = JSON.parse(
|
|
5166
|
+
if (fs12.existsSync(TRUST_FILE2))
|
|
5167
|
+
trust = JSON.parse(fs12.readFileSync(TRUST_FILE2, "utf-8"));
|
|
4749
5168
|
} catch {
|
|
4750
5169
|
}
|
|
4751
5170
|
trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
|
|
@@ -4756,8 +5175,8 @@ function writeTrustEntry(toolName, durationMs) {
|
|
|
4756
5175
|
}
|
|
4757
5176
|
function readPersistentDecisions() {
|
|
4758
5177
|
try {
|
|
4759
|
-
if (
|
|
4760
|
-
return JSON.parse(
|
|
5178
|
+
if (fs12.existsSync(DECISIONS_FILE)) {
|
|
5179
|
+
return JSON.parse(fs12.readFileSync(DECISIONS_FILE, "utf-8"));
|
|
4761
5180
|
}
|
|
4762
5181
|
} catch {
|
|
4763
5182
|
}
|
|
@@ -4822,7 +5241,7 @@ function abandonPending() {
|
|
|
4822
5241
|
});
|
|
4823
5242
|
if (autoStarted) {
|
|
4824
5243
|
try {
|
|
4825
|
-
|
|
5244
|
+
fs12.unlinkSync(DAEMON_PID_FILE);
|
|
4826
5245
|
} catch {
|
|
4827
5246
|
}
|
|
4828
5247
|
setTimeout(() => {
|
|
@@ -4833,7 +5252,7 @@ function abandonPending() {
|
|
|
4833
5252
|
}
|
|
4834
5253
|
function startActivitySocket() {
|
|
4835
5254
|
try {
|
|
4836
|
-
|
|
5255
|
+
fs12.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
4837
5256
|
} catch {
|
|
4838
5257
|
}
|
|
4839
5258
|
const ACTIVITY_MAX_BYTES = 1024 * 1024;
|
|
@@ -4875,25 +5294,30 @@ function startActivitySocket() {
|
|
|
4875
5294
|
unixServer.listen(ACTIVITY_SOCKET_PATH2);
|
|
4876
5295
|
process.on("exit", () => {
|
|
4877
5296
|
try {
|
|
4878
|
-
|
|
5297
|
+
fs12.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
4879
5298
|
} catch {
|
|
4880
5299
|
}
|
|
4881
5300
|
});
|
|
4882
5301
|
}
|
|
4883
|
-
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;
|
|
4884
5303
|
var init_state2 = __esm({
|
|
4885
5304
|
"src/daemon/state.ts"() {
|
|
4886
5305
|
"use strict";
|
|
4887
5306
|
init_daemon();
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
5307
|
+
init_suggestion_tracker();
|
|
5308
|
+
homeDir = os12.homedir();
|
|
5309
|
+
DAEMON_PID_FILE = path15.join(homeDir, ".node9", "daemon.pid");
|
|
5310
|
+
DECISIONS_FILE = path15.join(homeDir, ".node9", "decisions.json");
|
|
5311
|
+
AUDIT_LOG_FILE = path15.join(homeDir, ".node9", "audit.log");
|
|
5312
|
+
TRUST_FILE2 = path15.join(homeDir, ".node9", "trust.json");
|
|
5313
|
+
GLOBAL_CONFIG_FILE = path15.join(homeDir, ".node9", "config.json");
|
|
5314
|
+
CREDENTIALS_FILE = path15.join(homeDir, ".node9", "credentials.json");
|
|
5315
|
+
INSIGHT_COUNTS_FILE = path15.join(homeDir, ".node9", "insight-counts.json");
|
|
4895
5316
|
pending = /* @__PURE__ */ new Map();
|
|
4896
5317
|
sseClients = /* @__PURE__ */ new Set();
|
|
5318
|
+
suggestionTracker = new SuggestionTracker(3);
|
|
5319
|
+
suggestions = /* @__PURE__ */ new Map();
|
|
5320
|
+
insightCounts = /* @__PURE__ */ new Map();
|
|
4897
5321
|
_abandonTimer = null;
|
|
4898
5322
|
_hadBrowserClient = false;
|
|
4899
5323
|
_daemonServer = null;
|
|
@@ -4905,23 +5329,81 @@ var init_state2 = __esm({
|
|
|
4905
5329
|
"2h": 2 * 60 * 6e4
|
|
4906
5330
|
};
|
|
4907
5331
|
autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
4908
|
-
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
5332
|
+
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path15.join(os12.tmpdir(), "node9-activity.sock");
|
|
4909
5333
|
ACTIVITY_RING_SIZE = 100;
|
|
4910
5334
|
activityRing = [];
|
|
4911
5335
|
SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
4912
5336
|
}
|
|
4913
5337
|
});
|
|
4914
5338
|
|
|
5339
|
+
// src/config/patch.ts
|
|
5340
|
+
import fs13 from "fs";
|
|
5341
|
+
import path16 from "path";
|
|
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
|
+
|
|
4915
5396
|
// src/daemon/server.ts
|
|
4916
5397
|
import http from "http";
|
|
4917
|
-
import
|
|
4918
|
-
import
|
|
4919
|
-
import { randomUUID as
|
|
5398
|
+
import fs14 from "fs";
|
|
5399
|
+
import path17 from "path";
|
|
5400
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
4920
5401
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
4921
5402
|
import chalk2 from "chalk";
|
|
4922
5403
|
function startDaemon() {
|
|
4923
|
-
|
|
4924
|
-
const
|
|
5404
|
+
loadInsightCounts();
|
|
5405
|
+
const csrfToken = randomUUID4();
|
|
5406
|
+
const internalToken = randomUUID4();
|
|
4925
5407
|
const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
|
|
4926
5408
|
const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
|
|
4927
5409
|
const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
|
|
@@ -4934,7 +5416,7 @@ function startDaemon() {
|
|
|
4934
5416
|
idleTimer = setTimeout(() => {
|
|
4935
5417
|
if (autoStarted) {
|
|
4936
5418
|
try {
|
|
4937
|
-
|
|
5419
|
+
fs14.unlinkSync(DAEMON_PID_FILE);
|
|
4938
5420
|
} catch {
|
|
4939
5421
|
}
|
|
4940
5422
|
}
|
|
@@ -4943,8 +5425,14 @@ function startDaemon() {
|
|
|
4943
5425
|
idleTimer.unref();
|
|
4944
5426
|
}
|
|
4945
5427
|
resetIdleTimer();
|
|
5428
|
+
const allowedHosts = /* @__PURE__ */ new Set([`127.0.0.1:${DAEMON_PORT}`, `localhost:${DAEMON_PORT}`]);
|
|
4946
5429
|
const server = http.createServer(async (req, res) => {
|
|
4947
|
-
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}`);
|
|
4948
5436
|
const { pathname } = reqUrl;
|
|
4949
5437
|
if (req.method === "GET" && pathname === "/") {
|
|
4950
5438
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
@@ -4977,7 +5465,8 @@ data: ${JSON.stringify({
|
|
|
4977
5465
|
slackDelegated: e.slackDelegated,
|
|
4978
5466
|
timestamp: e.timestamp,
|
|
4979
5467
|
agent: e.agent,
|
|
4980
|
-
mcpServer: e.mcpServer
|
|
5468
|
+
mcpServer: e.mcpServer,
|
|
5469
|
+
allowCount: (insightCounts.get(e.toolName) ?? 0) + 1
|
|
4981
5470
|
})),
|
|
4982
5471
|
orgName: getOrgName(),
|
|
4983
5472
|
autoDenyMs: getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS
|
|
@@ -5019,6 +5508,12 @@ data: ${JSON.stringify(item.data)}
|
|
|
5019
5508
|
}
|
|
5020
5509
|
});
|
|
5021
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
|
+
}
|
|
5022
5517
|
if (req.method === "POST" && pathname === "/check") {
|
|
5023
5518
|
try {
|
|
5024
5519
|
resetIdleTimer();
|
|
@@ -5036,7 +5531,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5036
5531
|
activityId,
|
|
5037
5532
|
cwd
|
|
5038
5533
|
} = JSON.parse(body);
|
|
5039
|
-
const id = fromCLI && typeof activityId === "string" && activityId ||
|
|
5534
|
+
const id = fromCLI && typeof activityId === "string" && activityId || randomUUID4();
|
|
5040
5535
|
const entry = {
|
|
5041
5536
|
id,
|
|
5042
5537
|
toolName,
|
|
@@ -5062,7 +5557,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5062
5557
|
e.earlyReason = "No response \u2014 auto-denied after timeout";
|
|
5063
5558
|
}
|
|
5064
5559
|
pending.delete(id);
|
|
5065
|
-
broadcast("remove", { id });
|
|
5560
|
+
broadcast("remove", { id, decision: "deny" });
|
|
5066
5561
|
}
|
|
5067
5562
|
}, getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS)
|
|
5068
5563
|
};
|
|
@@ -5076,7 +5571,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5076
5571
|
status: "pending"
|
|
5077
5572
|
});
|
|
5078
5573
|
}
|
|
5079
|
-
const projectCwd = typeof cwd === "string" &&
|
|
5574
|
+
const projectCwd = typeof cwd === "string" && path17.isAbsolute(cwd) ? cwd : void 0;
|
|
5080
5575
|
const projectConfig = getConfig(projectCwd);
|
|
5081
5576
|
const browserEnabled = projectConfig.settings.approvers?.browser !== false;
|
|
5082
5577
|
const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
|
|
@@ -5089,7 +5584,10 @@ data: ${JSON.stringify(item.data)}
|
|
|
5089
5584
|
slackDelegated: entry.slackDelegated,
|
|
5090
5585
|
agent: entry.agent,
|
|
5091
5586
|
mcpServer: entry.mcpServer,
|
|
5092
|
-
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
|
|
5093
5591
|
});
|
|
5094
5592
|
const browserAlreadyOpened = process.env.NODE9_BROWSER_OPENED === "1";
|
|
5095
5593
|
if (browserEnabled && !browserOpened && !browserAlreadyOpened) {
|
|
@@ -5098,7 +5596,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5098
5596
|
}
|
|
5099
5597
|
}
|
|
5100
5598
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5101
|
-
res.end(JSON.stringify({ id }));
|
|
5599
|
+
res.end(JSON.stringify({ id, allowCount: (insightCounts.get(toolName) ?? 0) + 1 }));
|
|
5102
5600
|
if (slackDelegated) return;
|
|
5103
5601
|
authorizeHeadless(
|
|
5104
5602
|
toolName,
|
|
@@ -5125,7 +5623,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5125
5623
|
if (e.waiter) {
|
|
5126
5624
|
e.waiter(decision, result.reason);
|
|
5127
5625
|
pending.delete(id);
|
|
5128
|
-
broadcast("remove", { id });
|
|
5626
|
+
broadcast("remove", { id, decision });
|
|
5129
5627
|
} else {
|
|
5130
5628
|
e.earlyDecision = decision;
|
|
5131
5629
|
e.earlyReason = result.reason;
|
|
@@ -5141,7 +5639,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5141
5639
|
e.earlyReason = reason;
|
|
5142
5640
|
}
|
|
5143
5641
|
pending.delete(id);
|
|
5144
|
-
broadcast("remove", { id });
|
|
5642
|
+
broadcast("remove", { id, decision: "deny" });
|
|
5145
5643
|
});
|
|
5146
5644
|
return;
|
|
5147
5645
|
} catch {
|
|
@@ -5172,12 +5670,14 @@ data: ${JSON.stringify(item.data)}
|
|
|
5172
5670
|
res.end(JSON.stringify(body));
|
|
5173
5671
|
};
|
|
5174
5672
|
req.on("close", () => {
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
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);
|
|
5181
5681
|
});
|
|
5182
5682
|
return;
|
|
5183
5683
|
}
|
|
@@ -5206,10 +5706,10 @@ data: ${JSON.stringify(item.data)}
|
|
|
5206
5706
|
if (entry.waiter) {
|
|
5207
5707
|
entry.waiter("allow");
|
|
5208
5708
|
pending.delete(id);
|
|
5209
|
-
broadcast("remove", { id });
|
|
5709
|
+
broadcast("remove", { id, decision: "allow" });
|
|
5210
5710
|
} else {
|
|
5211
5711
|
entry.earlyDecision = "allow";
|
|
5212
|
-
broadcast("remove", { id });
|
|
5712
|
+
broadcast("remove", { id, decision: "allow" });
|
|
5213
5713
|
entry.timer = setTimeout(() => pending.delete(id), 3e4);
|
|
5214
5714
|
}
|
|
5215
5715
|
res.writeHead(200);
|
|
@@ -5223,16 +5723,29 @@ data: ${JSON.stringify(item.data)}
|
|
|
5223
5723
|
decision: resolvedDecision
|
|
5224
5724
|
});
|
|
5225
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
|
+
}
|
|
5226
5739
|
const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
|
|
5227
5740
|
if (source && VALID_SOURCES.has(source)) entry.decisionSource = source;
|
|
5228
5741
|
if (entry.waiter) {
|
|
5229
5742
|
entry.waiter(resolvedDecision, reason);
|
|
5230
5743
|
pending.delete(id);
|
|
5231
|
-
broadcast("remove", { id });
|
|
5744
|
+
broadcast("remove", { id, decision: resolvedDecision });
|
|
5232
5745
|
} else {
|
|
5233
5746
|
entry.earlyDecision = resolvedDecision;
|
|
5234
5747
|
entry.earlyReason = reason;
|
|
5235
|
-
broadcast("remove", { id });
|
|
5748
|
+
broadcast("remove", { id, decision: resolvedDecision });
|
|
5236
5749
|
entry.timer = setTimeout(() => pending.delete(id), 3e4);
|
|
5237
5750
|
}
|
|
5238
5751
|
res.writeHead(200);
|
|
@@ -5320,13 +5833,38 @@ data: ${JSON.stringify(item.data)}
|
|
|
5320
5833
|
const id = pathname.split("/").pop();
|
|
5321
5834
|
const entry = pending.get(id);
|
|
5322
5835
|
if (!entry) return res.writeHead(404).end();
|
|
5323
|
-
const { decision } = JSON.parse(await readBody(req));
|
|
5324
|
-
|
|
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
|
+
});
|
|
5325
5843
|
clearTimeout(entry.timer);
|
|
5326
|
-
if (
|
|
5327
|
-
|
|
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;
|
|
5328
5866
|
pending.delete(id);
|
|
5329
|
-
broadcast("remove", { id });
|
|
5867
|
+
broadcast("remove", { id, decision: resolvedResolveDecision });
|
|
5330
5868
|
res.writeHead(200);
|
|
5331
5869
|
return res.end(JSON.stringify({ ok: true }));
|
|
5332
5870
|
} catch {
|
|
@@ -5374,20 +5912,79 @@ data: ${JSON.stringify(item.data)}
|
|
|
5374
5912
|
res.writeHead(400).end();
|
|
5375
5913
|
}
|
|
5376
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
|
+
}
|
|
5377
5974
|
res.writeHead(404).end();
|
|
5378
5975
|
});
|
|
5379
5976
|
setDaemonServer(server);
|
|
5380
5977
|
server.on("error", (e) => {
|
|
5381
5978
|
if (e.code === "EADDRINUSE") {
|
|
5382
5979
|
try {
|
|
5383
|
-
if (
|
|
5384
|
-
const { pid } = JSON.parse(
|
|
5980
|
+
if (fs14.existsSync(DAEMON_PID_FILE)) {
|
|
5981
|
+
const { pid } = JSON.parse(fs14.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
5385
5982
|
process.kill(pid, 0);
|
|
5386
5983
|
return process.exit(0);
|
|
5387
5984
|
}
|
|
5388
5985
|
} catch {
|
|
5389
5986
|
try {
|
|
5390
|
-
|
|
5987
|
+
fs14.unlinkSync(DAEMON_PID_FILE);
|
|
5391
5988
|
} catch {
|
|
5392
5989
|
}
|
|
5393
5990
|
server.listen(DAEMON_PORT, DAEMON_HOST);
|
|
@@ -5453,32 +6050,34 @@ var init_server = __esm({
|
|
|
5453
6050
|
init_shields();
|
|
5454
6051
|
init_ui2();
|
|
5455
6052
|
init_state2();
|
|
6053
|
+
init_patch();
|
|
6054
|
+
init_config_schema();
|
|
5456
6055
|
}
|
|
5457
6056
|
});
|
|
5458
6057
|
|
|
5459
6058
|
// src/daemon/index.ts
|
|
5460
|
-
import
|
|
6059
|
+
import fs15 from "fs";
|
|
5461
6060
|
import chalk3 from "chalk";
|
|
5462
6061
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
5463
6062
|
function stopDaemon() {
|
|
5464
|
-
if (!
|
|
6063
|
+
if (!fs15.existsSync(DAEMON_PID_FILE)) return console.log(chalk3.yellow("Not running."));
|
|
5465
6064
|
try {
|
|
5466
|
-
const { pid } = JSON.parse(
|
|
6065
|
+
const { pid } = JSON.parse(fs15.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
5467
6066
|
process.kill(pid, "SIGTERM");
|
|
5468
6067
|
console.log(chalk3.green("\u2705 Stopped."));
|
|
5469
6068
|
} catch {
|
|
5470
6069
|
console.log(chalk3.gray("Cleaned up stale PID file."));
|
|
5471
6070
|
} finally {
|
|
5472
6071
|
try {
|
|
5473
|
-
|
|
6072
|
+
fs15.unlinkSync(DAEMON_PID_FILE);
|
|
5474
6073
|
} catch {
|
|
5475
6074
|
}
|
|
5476
6075
|
}
|
|
5477
6076
|
}
|
|
5478
6077
|
function daemonStatus() {
|
|
5479
|
-
if (
|
|
6078
|
+
if (fs15.existsSync(DAEMON_PID_FILE)) {
|
|
5480
6079
|
try {
|
|
5481
|
-
const { pid } = JSON.parse(
|
|
6080
|
+
const { pid } = JSON.parse(fs15.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
5482
6081
|
process.kill(pid, 0);
|
|
5483
6082
|
console.log(chalk3.green("Node9 daemon: running"));
|
|
5484
6083
|
return;
|
|
@@ -5512,10 +6111,10 @@ __export(tail_exports, {
|
|
|
5512
6111
|
startTail: () => startTail
|
|
5513
6112
|
});
|
|
5514
6113
|
import http2 from "http";
|
|
5515
|
-
import
|
|
5516
|
-
import
|
|
5517
|
-
import
|
|
5518
|
-
import
|
|
6114
|
+
import chalk16 from "chalk";
|
|
6115
|
+
import fs23 from "fs";
|
|
6116
|
+
import os21 from "os";
|
|
6117
|
+
import path25 from "path";
|
|
5519
6118
|
import readline3 from "readline";
|
|
5520
6119
|
import { spawn as spawn9, execSync as execSync3 } from "child_process";
|
|
5521
6120
|
function getIcon(tool) {
|
|
@@ -5531,17 +6130,17 @@ function formatBase(activity) {
|
|
|
5531
6130
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
5532
6131
|
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
|
|
5533
6132
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
5534
|
-
return `${
|
|
6133
|
+
return `${chalk16.gray(time)} ${icon} ${chalk16.white.bold(toolName)} ${chalk16.dim(argsPreview)}`;
|
|
5535
6134
|
}
|
|
5536
6135
|
function renderResult(activity, result) {
|
|
5537
6136
|
const base = formatBase(activity);
|
|
5538
6137
|
let status;
|
|
5539
6138
|
if (result.status === "allow") {
|
|
5540
|
-
status =
|
|
6139
|
+
status = chalk16.green("\u2713 ALLOW");
|
|
5541
6140
|
} else if (result.status === "dlp") {
|
|
5542
|
-
status =
|
|
6141
|
+
status = chalk16.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
5543
6142
|
} else {
|
|
5544
|
-
status =
|
|
6143
|
+
status = chalk16.red("\u2717 BLOCK");
|
|
5545
6144
|
}
|
|
5546
6145
|
if (process.stdout.isTTY) {
|
|
5547
6146
|
readline3.clearLine(process.stdout, 0);
|
|
@@ -5551,16 +6150,16 @@ function renderResult(activity, result) {
|
|
|
5551
6150
|
}
|
|
5552
6151
|
function renderPending(activity) {
|
|
5553
6152
|
if (!process.stdout.isTTY) return;
|
|
5554
|
-
process.stdout.write(`${formatBase(activity)} ${
|
|
6153
|
+
process.stdout.write(`${formatBase(activity)} ${chalk16.yellow("\u25CF \u2026")}\r`);
|
|
5555
6154
|
}
|
|
5556
6155
|
async function ensureDaemon() {
|
|
5557
6156
|
let pidPort = null;
|
|
5558
|
-
if (
|
|
6157
|
+
if (fs23.existsSync(PID_FILE)) {
|
|
5559
6158
|
try {
|
|
5560
|
-
const { port } = JSON.parse(
|
|
6159
|
+
const { port } = JSON.parse(fs23.readFileSync(PID_FILE, "utf-8"));
|
|
5561
6160
|
pidPort = port;
|
|
5562
6161
|
} catch {
|
|
5563
|
-
console.error(
|
|
6162
|
+
console.error(chalk16.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
5564
6163
|
}
|
|
5565
6164
|
}
|
|
5566
6165
|
const checkPort = pidPort ?? DAEMON_PORT;
|
|
@@ -5571,7 +6170,7 @@ async function ensureDaemon() {
|
|
|
5571
6170
|
if (res.ok) return checkPort;
|
|
5572
6171
|
} catch {
|
|
5573
6172
|
}
|
|
5574
|
-
console.log(
|
|
6173
|
+
console.log(chalk16.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
5575
6174
|
const child = spawn9(process.execPath, [process.argv[1], "daemon"], {
|
|
5576
6175
|
detached: true,
|
|
5577
6176
|
stdio: "ignore",
|
|
@@ -5588,12 +6187,15 @@ async function ensureDaemon() {
|
|
|
5588
6187
|
} catch {
|
|
5589
6188
|
}
|
|
5590
6189
|
}
|
|
5591
|
-
console.error(
|
|
6190
|
+
console.error(chalk16.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
5592
6191
|
process.exit(1);
|
|
5593
6192
|
}
|
|
5594
|
-
function postDecisionHttp(id, decision, csrfToken, port) {
|
|
6193
|
+
function postDecisionHttp(id, decision, csrfToken, port, opts) {
|
|
5595
6194
|
return new Promise((resolve, reject) => {
|
|
5596
|
-
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);
|
|
5597
6199
|
const req = http2.request(
|
|
5598
6200
|
{
|
|
5599
6201
|
hostname: "127.0.0.1",
|
|
@@ -5616,22 +6218,30 @@ function postDecisionHttp(id, decision, csrfToken, port) {
|
|
|
5616
6218
|
req.end(body);
|
|
5617
6219
|
});
|
|
5618
6220
|
}
|
|
5619
|
-
function buildCardLines(req) {
|
|
6221
|
+
function buildCardLines(req, localCount = 0) {
|
|
5620
6222
|
const argsStr = JSON.stringify(req.args ?? {}).replace(/\s+/g, " ");
|
|
5621
6223
|
const argsPreview = argsStr.length > 60 ? argsStr.slice(0, 60) + "\u2026" : argsStr;
|
|
5622
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`;
|
|
5623
6225
|
const blockedBy = req.riskMetadata?.blockedByLabel ?? "Policy rule";
|
|
5624
|
-
|
|
6226
|
+
const lines = [
|
|
5625
6227
|
``,
|
|
5626
6228
|
`${BOLD}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET}`,
|
|
5627
6229
|
`${CYAN}\u2551${RESET} Tool: ${BOLD}${req.toolName}${RESET}`,
|
|
5628
6230
|
`${CYAN}\u2551${RESET} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET}`,
|
|
5629
|
-
`${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(
|
|
5630
6239
|
`${CYAN}\u255A${RESET}`,
|
|
5631
6240
|
``,
|
|
5632
|
-
` ${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`,
|
|
5633
6242
|
``
|
|
5634
|
-
|
|
6243
|
+
);
|
|
6244
|
+
return lines;
|
|
5635
6245
|
}
|
|
5636
6246
|
async function startTail(options = {}) {
|
|
5637
6247
|
const port = await ensureDaemon();
|
|
@@ -5659,7 +6269,7 @@ async function startTail(options = {}) {
|
|
|
5659
6269
|
req2.end();
|
|
5660
6270
|
});
|
|
5661
6271
|
if (result.ok) {
|
|
5662
|
-
console.log(
|
|
6272
|
+
console.log(chalk16.green("\u2713 Flight Recorder buffer cleared."));
|
|
5663
6273
|
} else if (result.code === "ECONNREFUSED") {
|
|
5664
6274
|
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
5665
6275
|
} else if (result.code === "ETIMEDOUT") {
|
|
@@ -5676,6 +6286,7 @@ async function startTail(options = {}) {
|
|
|
5676
6286
|
let cardActive = false;
|
|
5677
6287
|
let cardLineCount = 0;
|
|
5678
6288
|
let cancelActiveCard = null;
|
|
6289
|
+
const localAllowCounts = /* @__PURE__ */ new Map();
|
|
5679
6290
|
const canApprove = process.stdout.isTTY && process.stdin.isTTY;
|
|
5680
6291
|
if (canApprove) readline3.emitKeypressEvents(process.stdin);
|
|
5681
6292
|
function clearCard() {
|
|
@@ -5686,7 +6297,10 @@ async function startTail(options = {}) {
|
|
|
5686
6297
|
}
|
|
5687
6298
|
function printCard(req2) {
|
|
5688
6299
|
process.stdout.write(HIDE_CURSOR + SAVE_CURSOR);
|
|
5689
|
-
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);
|
|
5690
6304
|
for (const line of lines) process.stdout.write(line + "\n");
|
|
5691
6305
|
cardLineCount = lines.length;
|
|
5692
6306
|
}
|
|
@@ -5714,34 +6328,70 @@ async function startTail(options = {}) {
|
|
|
5714
6328
|
process.stdin.pause();
|
|
5715
6329
|
cancelActiveCard = null;
|
|
5716
6330
|
};
|
|
5717
|
-
const settle = (
|
|
6331
|
+
const settle = (action) => {
|
|
5718
6332
|
if (settled) return;
|
|
5719
6333
|
settled = true;
|
|
5720
6334
|
cleanup();
|
|
5721
|
-
|
|
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");
|
|
5722
6346
|
process.stdout.write(SHOW_CURSOR);
|
|
5723
|
-
|
|
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) => {
|
|
5724
6365
|
try {
|
|
5725
|
-
|
|
5726
|
-
|
|
6366
|
+
fs23.appendFileSync(
|
|
6367
|
+
path25.join(os21.homedir(), ".node9", "hook-debug.log"),
|
|
5727
6368
|
`[tail] POST /decision failed: ${String(err)}
|
|
5728
6369
|
`
|
|
5729
6370
|
);
|
|
5730
6371
|
} catch {
|
|
5731
6372
|
}
|
|
5732
6373
|
});
|
|
5733
|
-
const decisionLabel = decision === "allow" ? chalk14.green("\u2713 ALLOWED (terminal)") : chalk14.red("\u2717 DENIED (terminal)");
|
|
5734
|
-
console.log(`${chalk14.cyan("\u25C6")} ${chalk14.bold(req2.toolName.padEnd(16))} ${decisionLabel}`);
|
|
5735
6374
|
approvalQueue.shift();
|
|
5736
6375
|
cardActive = false;
|
|
5737
6376
|
showNextCard();
|
|
5738
6377
|
};
|
|
5739
|
-
cancelActiveCard = () => {
|
|
6378
|
+
cancelActiveCard = (externalDecision) => {
|
|
5740
6379
|
if (settled) return;
|
|
5741
6380
|
settled = true;
|
|
5742
6381
|
cleanup();
|
|
5743
|
-
|
|
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");
|
|
5744
6393
|
process.stdout.write(SHOW_CURSOR);
|
|
6394
|
+
cardLineCount = 0;
|
|
5745
6395
|
approvalQueue.shift();
|
|
5746
6396
|
cardActive = false;
|
|
5747
6397
|
showNextCard();
|
|
@@ -5749,10 +6399,14 @@ async function startTail(options = {}) {
|
|
|
5749
6399
|
process.stdin.resume();
|
|
5750
6400
|
onKeypress = (_str, key) => {
|
|
5751
6401
|
const name = key?.name ?? "";
|
|
5752
|
-
if (name === "
|
|
6402
|
+
if (name === "y" || name === "return") {
|
|
5753
6403
|
settle("allow");
|
|
5754
|
-
} else if (name === "
|
|
6404
|
+
} else if (name === "n" || name === "d" || key?.ctrl && name === "c") {
|
|
5755
6405
|
settle("deny");
|
|
6406
|
+
} else if (name === "a") {
|
|
6407
|
+
settle("always-allow");
|
|
6408
|
+
} else if (name === "t") {
|
|
6409
|
+
settle("trust");
|
|
5756
6410
|
}
|
|
5757
6411
|
};
|
|
5758
6412
|
process.stdin.on("keypress", onKeypress);
|
|
@@ -5765,19 +6419,27 @@ async function startTail(options = {}) {
|
|
|
5765
6419
|
else if (process.platform === "win32")
|
|
5766
6420
|
execSync3(`cmd /c start "" "${dashboardUrl}"`, { stdio: "ignore" });
|
|
5767
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
|
+
});
|
|
5768
6428
|
}
|
|
5769
6429
|
} catch {
|
|
5770
6430
|
}
|
|
5771
|
-
console.log(
|
|
5772
|
-
\u{1F6F0}\uFE0F Node9 tail `) +
|
|
6431
|
+
console.log(chalk16.cyan.bold(`
|
|
6432
|
+
\u{1F6F0}\uFE0F Node9 tail `) + chalk16.dim(`\u2192 ${dashboardUrl}`));
|
|
5773
6433
|
if (canApprove) {
|
|
5774
|
-
console.log(
|
|
6434
|
+
console.log(
|
|
6435
|
+
chalk16.dim("Interactive approvals: [\u21B5/y] Allow [n] Deny [a] Always Allow [t] Trust 30m")
|
|
6436
|
+
);
|
|
5775
6437
|
}
|
|
5776
6438
|
if (options.history) {
|
|
5777
|
-
console.log(
|
|
6439
|
+
console.log(chalk16.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
|
|
5778
6440
|
} else {
|
|
5779
6441
|
console.log(
|
|
5780
|
-
|
|
6442
|
+
chalk16.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
|
|
5781
6443
|
);
|
|
5782
6444
|
}
|
|
5783
6445
|
process.on("SIGINT", () => {
|
|
@@ -5787,13 +6449,13 @@ async function startTail(options = {}) {
|
|
|
5787
6449
|
readline3.clearLine(process.stdout, 0);
|
|
5788
6450
|
readline3.cursorTo(process.stdout, 0);
|
|
5789
6451
|
}
|
|
5790
|
-
console.log(
|
|
6452
|
+
console.log(chalk16.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
5791
6453
|
process.exit(0);
|
|
5792
6454
|
});
|
|
5793
6455
|
const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
|
|
5794
6456
|
const req = http2.get(sseUrl, (res) => {
|
|
5795
6457
|
if (res.statusCode !== 200) {
|
|
5796
|
-
console.error(
|
|
6458
|
+
console.error(chalk16.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
5797
6459
|
process.exit(1);
|
|
5798
6460
|
}
|
|
5799
6461
|
let currentEvent = "";
|
|
@@ -5823,7 +6485,7 @@ async function startTail(options = {}) {
|
|
|
5823
6485
|
readline3.clearLine(process.stdout, 0);
|
|
5824
6486
|
readline3.cursorTo(process.stdout, 0);
|
|
5825
6487
|
}
|
|
5826
|
-
console.log(
|
|
6488
|
+
console.log(chalk16.red("\n\u274C Daemon disconnected."));
|
|
5827
6489
|
process.exit(1);
|
|
5828
6490
|
});
|
|
5829
6491
|
});
|
|
@@ -5864,11 +6526,17 @@ async function startTail(options = {}) {
|
|
|
5864
6526
|
}
|
|
5865
6527
|
if (event === "remove") {
|
|
5866
6528
|
try {
|
|
5867
|
-
const { id } = JSON.parse(rawData);
|
|
6529
|
+
const { id, decision } = JSON.parse(rawData);
|
|
5868
6530
|
const idx = approvalQueue.findIndex((r) => r.id === id);
|
|
5869
6531
|
if (idx !== -1) {
|
|
5870
6532
|
if (idx === 0 && cardActive && cancelActiveCard) {
|
|
5871
|
-
|
|
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);
|
|
5872
6540
|
} else {
|
|
5873
6541
|
approvalQueue.splice(idx, 1);
|
|
5874
6542
|
}
|
|
@@ -5903,7 +6571,7 @@ async function startTail(options = {}) {
|
|
|
5903
6571
|
}
|
|
5904
6572
|
req.on("error", (err) => {
|
|
5905
6573
|
const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
|
|
5906
|
-
console.error(
|
|
6574
|
+
console.error(chalk16.red(`
|
|
5907
6575
|
\u274C ${msg}`));
|
|
5908
6576
|
process.exit(1);
|
|
5909
6577
|
});
|
|
@@ -5913,8 +6581,9 @@ var init_tail = __esm({
|
|
|
5913
6581
|
"src/tui/tail.ts"() {
|
|
5914
6582
|
"use strict";
|
|
5915
6583
|
init_daemon2();
|
|
6584
|
+
init_daemon();
|
|
5916
6585
|
init_core();
|
|
5917
|
-
PID_FILE =
|
|
6586
|
+
PID_FILE = path25.join(os21.homedir(), ".node9", "daemon.pid");
|
|
5918
6587
|
ICONS = {
|
|
5919
6588
|
bash: "\u{1F4BB}",
|
|
5920
6589
|
shell: "\u{1F4BB}",
|
|
@@ -5952,9 +6621,9 @@ init_core();
|
|
|
5952
6621
|
import { Command } from "commander";
|
|
5953
6622
|
|
|
5954
6623
|
// src/setup.ts
|
|
5955
|
-
import
|
|
5956
|
-
import
|
|
5957
|
-
import
|
|
6624
|
+
import fs11 from "fs";
|
|
6625
|
+
import path14 from "path";
|
|
6626
|
+
import os11 from "os";
|
|
5958
6627
|
import chalk from "chalk";
|
|
5959
6628
|
import { confirm } from "@inquirer/prompts";
|
|
5960
6629
|
function printDaemonTip() {
|
|
@@ -5971,26 +6640,26 @@ function fullPathCommand(subcommand) {
|
|
|
5971
6640
|
}
|
|
5972
6641
|
function readJson(filePath) {
|
|
5973
6642
|
try {
|
|
5974
|
-
if (
|
|
5975
|
-
return JSON.parse(
|
|
6643
|
+
if (fs11.existsSync(filePath)) {
|
|
6644
|
+
return JSON.parse(fs11.readFileSync(filePath, "utf-8"));
|
|
5976
6645
|
}
|
|
5977
6646
|
} catch {
|
|
5978
6647
|
}
|
|
5979
6648
|
return null;
|
|
5980
6649
|
}
|
|
5981
6650
|
function writeJson(filePath, data) {
|
|
5982
|
-
const dir =
|
|
5983
|
-
if (!
|
|
5984
|
-
|
|
6651
|
+
const dir = path14.dirname(filePath);
|
|
6652
|
+
if (!fs11.existsSync(dir)) fs11.mkdirSync(dir, { recursive: true });
|
|
6653
|
+
fs11.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
5985
6654
|
}
|
|
5986
6655
|
function isNode9Hook(cmd) {
|
|
5987
6656
|
if (!cmd) return false;
|
|
5988
6657
|
return /(?:^|[\s/\\])node9 (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js (?:check|log)/.test(cmd);
|
|
5989
6658
|
}
|
|
5990
6659
|
function teardownClaude() {
|
|
5991
|
-
const homeDir2 =
|
|
5992
|
-
const hooksPath =
|
|
5993
|
-
const mcpPath =
|
|
6660
|
+
const homeDir2 = os11.homedir();
|
|
6661
|
+
const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
|
|
6662
|
+
const mcpPath = path14.join(homeDir2, ".claude.json");
|
|
5994
6663
|
let changed = false;
|
|
5995
6664
|
const settings = readJson(hooksPath);
|
|
5996
6665
|
if (settings?.hooks) {
|
|
@@ -6038,8 +6707,8 @@ function teardownClaude() {
|
|
|
6038
6707
|
}
|
|
6039
6708
|
}
|
|
6040
6709
|
function teardownGemini() {
|
|
6041
|
-
const homeDir2 =
|
|
6042
|
-
const settingsPath =
|
|
6710
|
+
const homeDir2 = os11.homedir();
|
|
6711
|
+
const settingsPath = path14.join(homeDir2, ".gemini", "settings.json");
|
|
6043
6712
|
const settings = readJson(settingsPath);
|
|
6044
6713
|
if (!settings) {
|
|
6045
6714
|
console.log(chalk.blue(" \u2139\uFE0F ~/.gemini/settings.json not found \u2014 nothing to remove"));
|
|
@@ -6077,8 +6746,8 @@ function teardownGemini() {
|
|
|
6077
6746
|
}
|
|
6078
6747
|
}
|
|
6079
6748
|
function teardownCursor() {
|
|
6080
|
-
const homeDir2 =
|
|
6081
|
-
const mcpPath =
|
|
6749
|
+
const homeDir2 = os11.homedir();
|
|
6750
|
+
const mcpPath = path14.join(homeDir2, ".cursor", "mcp.json");
|
|
6082
6751
|
const mcpConfig = readJson(mcpPath);
|
|
6083
6752
|
if (!mcpConfig?.mcpServers) {
|
|
6084
6753
|
console.log(chalk.blue(" \u2139\uFE0F ~/.cursor/mcp.json not found \u2014 nothing to remove"));
|
|
@@ -6104,9 +6773,9 @@ function teardownCursor() {
|
|
|
6104
6773
|
}
|
|
6105
6774
|
}
|
|
6106
6775
|
async function setupClaude() {
|
|
6107
|
-
const homeDir2 =
|
|
6108
|
-
const mcpPath =
|
|
6109
|
-
const hooksPath =
|
|
6776
|
+
const homeDir2 = os11.homedir();
|
|
6777
|
+
const mcpPath = path14.join(homeDir2, ".claude.json");
|
|
6778
|
+
const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
|
|
6110
6779
|
const claudeConfig = readJson(mcpPath) ?? {};
|
|
6111
6780
|
const settings = readJson(hooksPath) ?? {};
|
|
6112
6781
|
const servers = claudeConfig.mcpServers ?? {};
|
|
@@ -6180,8 +6849,8 @@ async function setupClaude() {
|
|
|
6180
6849
|
}
|
|
6181
6850
|
}
|
|
6182
6851
|
async function setupGemini() {
|
|
6183
|
-
const homeDir2 =
|
|
6184
|
-
const settingsPath =
|
|
6852
|
+
const homeDir2 = os11.homedir();
|
|
6853
|
+
const settingsPath = path14.join(homeDir2, ".gemini", "settings.json");
|
|
6185
6854
|
const settings = readJson(settingsPath) ?? {};
|
|
6186
6855
|
const servers = settings.mcpServers ?? {};
|
|
6187
6856
|
let anythingChanged = false;
|
|
@@ -6262,9 +6931,28 @@ async function setupGemini() {
|
|
|
6262
6931
|
printDaemonTip();
|
|
6263
6932
|
}
|
|
6264
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
|
+
}
|
|
6265
6953
|
async function setupCursor() {
|
|
6266
|
-
const homeDir2 =
|
|
6267
|
-
const mcpPath =
|
|
6954
|
+
const homeDir2 = os11.homedir();
|
|
6955
|
+
const mcpPath = path14.join(homeDir2, ".cursor", "mcp.json");
|
|
6268
6956
|
const mcpConfig = readJson(mcpPath) ?? {};
|
|
6269
6957
|
const servers = mcpConfig.mcpServers ?? {};
|
|
6270
6958
|
let anythingChanged = false;
|
|
@@ -6320,10 +7008,10 @@ async function setupCursor() {
|
|
|
6320
7008
|
|
|
6321
7009
|
// src/cli.ts
|
|
6322
7010
|
init_daemon2();
|
|
6323
|
-
import
|
|
6324
|
-
import
|
|
6325
|
-
import
|
|
6326
|
-
import
|
|
7011
|
+
import chalk17 from "chalk";
|
|
7012
|
+
import fs24 from "fs";
|
|
7013
|
+
import path26 from "path";
|
|
7014
|
+
import os22 from "os";
|
|
6327
7015
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
6328
7016
|
|
|
6329
7017
|
// src/utils/duration.ts
|
|
@@ -6548,32 +7236,32 @@ init_daemon();
|
|
|
6548
7236
|
init_config();
|
|
6549
7237
|
init_policy();
|
|
6550
7238
|
import chalk5 from "chalk";
|
|
6551
|
-
import
|
|
6552
|
-
import
|
|
6553
|
-
import
|
|
7239
|
+
import fs17 from "fs";
|
|
7240
|
+
import path19 from "path";
|
|
7241
|
+
import os15 from "os";
|
|
6554
7242
|
|
|
6555
7243
|
// src/undo.ts
|
|
6556
7244
|
import { spawnSync as spawnSync4, spawn as spawn5 } from "child_process";
|
|
6557
7245
|
import crypto2 from "crypto";
|
|
6558
|
-
import
|
|
6559
|
-
import
|
|
6560
|
-
import
|
|
6561
|
-
var SNAPSHOT_STACK_PATH =
|
|
6562
|
-
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");
|
|
6563
7251
|
var MAX_SNAPSHOTS = 10;
|
|
6564
7252
|
var GIT_TIMEOUT = 15e3;
|
|
6565
7253
|
function readStack() {
|
|
6566
7254
|
try {
|
|
6567
|
-
if (
|
|
6568
|
-
return JSON.parse(
|
|
7255
|
+
if (fs16.existsSync(SNAPSHOT_STACK_PATH))
|
|
7256
|
+
return JSON.parse(fs16.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
|
|
6569
7257
|
} catch {
|
|
6570
7258
|
}
|
|
6571
7259
|
return [];
|
|
6572
7260
|
}
|
|
6573
7261
|
function writeStack(stack) {
|
|
6574
|
-
const dir =
|
|
6575
|
-
if (!
|
|
6576
|
-
|
|
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));
|
|
6577
7265
|
}
|
|
6578
7266
|
function buildArgsSummary(tool, args) {
|
|
6579
7267
|
if (!args || typeof args !== "object") return "";
|
|
@@ -6589,7 +7277,7 @@ function buildArgsSummary(tool, args) {
|
|
|
6589
7277
|
function normalizeCwdForHash(cwd) {
|
|
6590
7278
|
let normalized;
|
|
6591
7279
|
try {
|
|
6592
|
-
normalized =
|
|
7280
|
+
normalized = fs16.realpathSync(cwd);
|
|
6593
7281
|
} catch {
|
|
6594
7282
|
normalized = cwd;
|
|
6595
7283
|
}
|
|
@@ -6599,16 +7287,16 @@ function normalizeCwdForHash(cwd) {
|
|
|
6599
7287
|
}
|
|
6600
7288
|
function getShadowRepoDir(cwd) {
|
|
6601
7289
|
const hash = crypto2.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
|
|
6602
|
-
return
|
|
7290
|
+
return path18.join(os14.homedir(), ".node9", "snapshots", hash);
|
|
6603
7291
|
}
|
|
6604
7292
|
function cleanOrphanedIndexFiles(shadowDir) {
|
|
6605
7293
|
try {
|
|
6606
7294
|
const cutoff = Date.now() - 6e4;
|
|
6607
|
-
for (const f of
|
|
7295
|
+
for (const f of fs16.readdirSync(shadowDir)) {
|
|
6608
7296
|
if (f.startsWith("index_")) {
|
|
6609
|
-
const fp =
|
|
7297
|
+
const fp = path18.join(shadowDir, f);
|
|
6610
7298
|
try {
|
|
6611
|
-
if (
|
|
7299
|
+
if (fs16.statSync(fp).mtimeMs < cutoff) fs16.unlinkSync(fp);
|
|
6612
7300
|
} catch {
|
|
6613
7301
|
}
|
|
6614
7302
|
}
|
|
@@ -6620,7 +7308,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
|
|
|
6620
7308
|
const hardcoded = [".git", ".node9"];
|
|
6621
7309
|
const lines = [...hardcoded, ...ignorePaths].join("\n");
|
|
6622
7310
|
try {
|
|
6623
|
-
|
|
7311
|
+
fs16.writeFileSync(path18.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
|
|
6624
7312
|
} catch {
|
|
6625
7313
|
}
|
|
6626
7314
|
}
|
|
@@ -6633,25 +7321,25 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
6633
7321
|
timeout: 3e3
|
|
6634
7322
|
});
|
|
6635
7323
|
if (check.status === 0) {
|
|
6636
|
-
const ptPath =
|
|
7324
|
+
const ptPath = path18.join(shadowDir, "project-path.txt");
|
|
6637
7325
|
try {
|
|
6638
|
-
const stored =
|
|
7326
|
+
const stored = fs16.readFileSync(ptPath, "utf8").trim();
|
|
6639
7327
|
if (stored === normalizedCwd) return true;
|
|
6640
7328
|
if (process.env.NODE9_DEBUG === "1")
|
|
6641
7329
|
console.error(
|
|
6642
7330
|
`[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
|
|
6643
7331
|
);
|
|
6644
|
-
|
|
7332
|
+
fs16.rmSync(shadowDir, { recursive: true, force: true });
|
|
6645
7333
|
} catch {
|
|
6646
7334
|
try {
|
|
6647
|
-
|
|
7335
|
+
fs16.writeFileSync(ptPath, normalizedCwd, "utf8");
|
|
6648
7336
|
} catch {
|
|
6649
7337
|
}
|
|
6650
7338
|
return true;
|
|
6651
7339
|
}
|
|
6652
7340
|
}
|
|
6653
7341
|
try {
|
|
6654
|
-
|
|
7342
|
+
fs16.mkdirSync(shadowDir, { recursive: true });
|
|
6655
7343
|
} catch {
|
|
6656
7344
|
}
|
|
6657
7345
|
const init = spawnSync4("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
|
|
@@ -6660,7 +7348,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
6660
7348
|
console.error("[Node9] git init --bare failed:", init.stderr?.toString());
|
|
6661
7349
|
return false;
|
|
6662
7350
|
}
|
|
6663
|
-
const configFile =
|
|
7351
|
+
const configFile = path18.join(shadowDir, "config");
|
|
6664
7352
|
spawnSync4("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
|
|
6665
7353
|
timeout: 3e3
|
|
6666
7354
|
});
|
|
@@ -6668,7 +7356,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
6668
7356
|
timeout: 3e3
|
|
6669
7357
|
});
|
|
6670
7358
|
try {
|
|
6671
|
-
|
|
7359
|
+
fs16.writeFileSync(path18.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
|
|
6672
7360
|
} catch {
|
|
6673
7361
|
}
|
|
6674
7362
|
return true;
|
|
@@ -6691,7 +7379,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
6691
7379
|
const shadowDir = getShadowRepoDir(cwd);
|
|
6692
7380
|
if (!ensureShadowRepo(shadowDir, cwd)) return null;
|
|
6693
7381
|
writeShadowExcludes(shadowDir, ignorePaths);
|
|
6694
|
-
indexFile =
|
|
7382
|
+
indexFile = path18.join(shadowDir, `index_${process.pid}_${Date.now()}`);
|
|
6695
7383
|
const shadowEnv = {
|
|
6696
7384
|
...process.env,
|
|
6697
7385
|
GIT_DIR: shadowDir,
|
|
@@ -6720,7 +7408,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
6720
7408
|
const shouldGc = stack.length % 5 === 0;
|
|
6721
7409
|
if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
|
|
6722
7410
|
writeStack(stack);
|
|
6723
|
-
|
|
7411
|
+
fs16.writeFileSync(UNDO_LATEST_PATH, commitHash);
|
|
6724
7412
|
if (shouldGc) {
|
|
6725
7413
|
spawn5("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
|
|
6726
7414
|
}
|
|
@@ -6731,7 +7419,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
6731
7419
|
} finally {
|
|
6732
7420
|
if (indexFile) {
|
|
6733
7421
|
try {
|
|
6734
|
-
|
|
7422
|
+
fs16.unlinkSync(indexFile);
|
|
6735
7423
|
} catch {
|
|
6736
7424
|
}
|
|
6737
7425
|
}
|
|
@@ -6800,9 +7488,9 @@ function applyUndo(hash, cwd) {
|
|
|
6800
7488
|
timeout: GIT_TIMEOUT
|
|
6801
7489
|
}).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
6802
7490
|
for (const file of [...tracked, ...untracked]) {
|
|
6803
|
-
const fullPath =
|
|
6804
|
-
if (!snapshotFiles.has(file) &&
|
|
6805
|
-
|
|
7491
|
+
const fullPath = path18.join(dir, file);
|
|
7492
|
+
if (!snapshotFiles.has(file) && fs16.existsSync(fullPath)) {
|
|
7493
|
+
fs16.unlinkSync(fullPath);
|
|
6806
7494
|
}
|
|
6807
7495
|
}
|
|
6808
7496
|
return true;
|
|
@@ -6826,9 +7514,9 @@ function registerCheckCommand(program2) {
|
|
|
6826
7514
|
} catch (err) {
|
|
6827
7515
|
const tempConfig = getConfig();
|
|
6828
7516
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
6829
|
-
const logPath =
|
|
7517
|
+
const logPath = path19.join(os15.homedir(), ".node9", "hook-debug.log");
|
|
6830
7518
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
6831
|
-
|
|
7519
|
+
fs17.appendFileSync(
|
|
6832
7520
|
logPath,
|
|
6833
7521
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
6834
7522
|
RAW: ${raw}
|
|
@@ -6839,10 +7527,10 @@ RAW: ${raw}
|
|
|
6839
7527
|
}
|
|
6840
7528
|
const config = getConfig(payload.cwd || void 0);
|
|
6841
7529
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
6842
|
-
const logPath =
|
|
6843
|
-
if (!
|
|
6844
|
-
|
|
6845
|
-
|
|
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}
|
|
6846
7534
|
`);
|
|
6847
7535
|
}
|
|
6848
7536
|
const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
|
|
@@ -6855,8 +7543,8 @@ RAW: ${raw}
|
|
|
6855
7543
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
6856
7544
|
let ttyFd = null;
|
|
6857
7545
|
try {
|
|
6858
|
-
ttyFd =
|
|
6859
|
-
const writeTty = (line) =>
|
|
7546
|
+
ttyFd = fs17.openSync("/dev/tty", "w");
|
|
7547
|
+
const writeTty = (line) => fs17.writeSync(ttyFd, line + "\n");
|
|
6860
7548
|
if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
|
|
6861
7549
|
writeTty(chalk5.bgRed.white.bold(`
|
|
6862
7550
|
\u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
@@ -6872,7 +7560,7 @@ RAW: ${raw}
|
|
|
6872
7560
|
} finally {
|
|
6873
7561
|
if (ttyFd !== null)
|
|
6874
7562
|
try {
|
|
6875
|
-
|
|
7563
|
+
fs17.closeSync(ttyFd);
|
|
6876
7564
|
} catch {
|
|
6877
7565
|
}
|
|
6878
7566
|
}
|
|
@@ -6903,7 +7591,7 @@ RAW: ${raw}
|
|
|
6903
7591
|
if (shouldSnapshot(toolName, toolInput, config)) {
|
|
6904
7592
|
await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
|
|
6905
7593
|
}
|
|
6906
|
-
const safeCwdForAuth = typeof payload.cwd === "string" &&
|
|
7594
|
+
const safeCwdForAuth = typeof payload.cwd === "string" && path19.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
6907
7595
|
const result = await authorizeHeadless(toolName, toolInput, meta, {
|
|
6908
7596
|
cwd: safeCwdForAuth
|
|
6909
7597
|
});
|
|
@@ -6915,12 +7603,12 @@ RAW: ${raw}
|
|
|
6915
7603
|
}
|
|
6916
7604
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
6917
7605
|
try {
|
|
6918
|
-
const tty =
|
|
6919
|
-
|
|
7606
|
+
const tty = fs17.openSync("/dev/tty", "w");
|
|
7607
|
+
fs17.writeSync(
|
|
6920
7608
|
tty,
|
|
6921
7609
|
chalk5.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
|
|
6922
7610
|
);
|
|
6923
|
-
|
|
7611
|
+
fs17.closeSync(tty);
|
|
6924
7612
|
} catch {
|
|
6925
7613
|
}
|
|
6926
7614
|
const daemonReady = await autoStartDaemonAndWait();
|
|
@@ -6947,9 +7635,9 @@ RAW: ${raw}
|
|
|
6947
7635
|
});
|
|
6948
7636
|
} catch (err) {
|
|
6949
7637
|
if (process.env.NODE9_DEBUG === "1") {
|
|
6950
|
-
const logPath =
|
|
7638
|
+
const logPath = path19.join(os15.homedir(), ".node9", "hook-debug.log");
|
|
6951
7639
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
6952
|
-
|
|
7640
|
+
fs17.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
6953
7641
|
`);
|
|
6954
7642
|
}
|
|
6955
7643
|
process.exit(0);
|
|
@@ -6986,9 +7674,9 @@ RAW: ${raw}
|
|
|
6986
7674
|
init_audit();
|
|
6987
7675
|
init_config();
|
|
6988
7676
|
init_policy();
|
|
6989
|
-
import
|
|
6990
|
-
import
|
|
6991
|
-
import
|
|
7677
|
+
import fs18 from "fs";
|
|
7678
|
+
import path20 from "path";
|
|
7679
|
+
import os16 from "os";
|
|
6992
7680
|
function sanitize3(value) {
|
|
6993
7681
|
return value.replace(/[\x00-\x1F\x7F]/g, "");
|
|
6994
7682
|
}
|
|
@@ -7007,11 +7695,11 @@ function registerLogCommand(program2) {
|
|
|
7007
7695
|
decision: "allowed",
|
|
7008
7696
|
source: "post-hook"
|
|
7009
7697
|
};
|
|
7010
|
-
const logPath =
|
|
7011
|
-
if (!
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
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;
|
|
7015
7703
|
const config = getConfig(safeCwd);
|
|
7016
7704
|
if (shouldSnapshot(tool, {}, config)) {
|
|
7017
7705
|
await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
|
|
@@ -7020,9 +7708,9 @@ function registerLogCommand(program2) {
|
|
|
7020
7708
|
const msg = err instanceof Error ? err.message : String(err);
|
|
7021
7709
|
process.stderr.write(`[Node9] audit log error: ${msg}
|
|
7022
7710
|
`);
|
|
7023
|
-
const debugPath =
|
|
7711
|
+
const debugPath = path20.join(os16.homedir(), ".node9", "hook-debug.log");
|
|
7024
7712
|
try {
|
|
7025
|
-
|
|
7713
|
+
fs18.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
|
|
7026
7714
|
`);
|
|
7027
7715
|
} catch {
|
|
7028
7716
|
}
|
|
@@ -7327,13 +8015,13 @@ function registerConfigShowCommand(program2) {
|
|
|
7327
8015
|
// src/cli/commands/doctor.ts
|
|
7328
8016
|
init_daemon();
|
|
7329
8017
|
import chalk7 from "chalk";
|
|
7330
|
-
import
|
|
7331
|
-
import
|
|
7332
|
-
import
|
|
8018
|
+
import fs19 from "fs";
|
|
8019
|
+
import path21 from "path";
|
|
8020
|
+
import os17 from "os";
|
|
7333
8021
|
import { execSync as execSync2 } from "child_process";
|
|
7334
8022
|
function registerDoctorCommand(program2, version2) {
|
|
7335
8023
|
program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
7336
|
-
const homeDir2 =
|
|
8024
|
+
const homeDir2 = os17.homedir();
|
|
7337
8025
|
let failures = 0;
|
|
7338
8026
|
function pass(msg) {
|
|
7339
8027
|
console.log(chalk7.green(" \u2705 ") + msg);
|
|
@@ -7382,10 +8070,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7382
8070
|
);
|
|
7383
8071
|
}
|
|
7384
8072
|
section("Configuration");
|
|
7385
|
-
const globalConfigPath =
|
|
7386
|
-
if (
|
|
8073
|
+
const globalConfigPath = path21.join(homeDir2, ".node9", "config.json");
|
|
8074
|
+
if (fs19.existsSync(globalConfigPath)) {
|
|
7387
8075
|
try {
|
|
7388
|
-
JSON.parse(
|
|
8076
|
+
JSON.parse(fs19.readFileSync(globalConfigPath, "utf-8"));
|
|
7389
8077
|
pass("~/.node9/config.json found and valid");
|
|
7390
8078
|
} catch {
|
|
7391
8079
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -7393,10 +8081,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7393
8081
|
} else {
|
|
7394
8082
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
7395
8083
|
}
|
|
7396
|
-
const projectConfigPath =
|
|
7397
|
-
if (
|
|
8084
|
+
const projectConfigPath = path21.join(process.cwd(), "node9.config.json");
|
|
8085
|
+
if (fs19.existsSync(projectConfigPath)) {
|
|
7398
8086
|
try {
|
|
7399
|
-
JSON.parse(
|
|
8087
|
+
JSON.parse(fs19.readFileSync(projectConfigPath, "utf-8"));
|
|
7400
8088
|
pass("node9.config.json found and valid (project)");
|
|
7401
8089
|
} catch {
|
|
7402
8090
|
fail(
|
|
@@ -7405,8 +8093,8 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7405
8093
|
);
|
|
7406
8094
|
}
|
|
7407
8095
|
}
|
|
7408
|
-
const credsPath =
|
|
7409
|
-
if (
|
|
8096
|
+
const credsPath = path21.join(homeDir2, ".node9", "credentials.json");
|
|
8097
|
+
if (fs19.existsSync(credsPath)) {
|
|
7410
8098
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
7411
8099
|
} else {
|
|
7412
8100
|
warn(
|
|
@@ -7415,10 +8103,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7415
8103
|
);
|
|
7416
8104
|
}
|
|
7417
8105
|
section("Agent Hooks");
|
|
7418
|
-
const claudeSettingsPath =
|
|
7419
|
-
if (
|
|
8106
|
+
const claudeSettingsPath = path21.join(homeDir2, ".claude", "settings.json");
|
|
8107
|
+
if (fs19.existsSync(claudeSettingsPath)) {
|
|
7420
8108
|
try {
|
|
7421
|
-
const cs = JSON.parse(
|
|
8109
|
+
const cs = JSON.parse(fs19.readFileSync(claudeSettingsPath, "utf-8"));
|
|
7422
8110
|
const hasHook = cs.hooks?.PreToolUse?.some(
|
|
7423
8111
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
7424
8112
|
);
|
|
@@ -7434,10 +8122,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7434
8122
|
} else {
|
|
7435
8123
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
7436
8124
|
}
|
|
7437
|
-
const geminiSettingsPath =
|
|
7438
|
-
if (
|
|
8125
|
+
const geminiSettingsPath = path21.join(homeDir2, ".gemini", "settings.json");
|
|
8126
|
+
if (fs19.existsSync(geminiSettingsPath)) {
|
|
7439
8127
|
try {
|
|
7440
|
-
const gs = JSON.parse(
|
|
8128
|
+
const gs = JSON.parse(fs19.readFileSync(geminiSettingsPath, "utf-8"));
|
|
7441
8129
|
const hasHook = gs.hooks?.BeforeTool?.some(
|
|
7442
8130
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
7443
8131
|
);
|
|
@@ -7453,10 +8141,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7453
8141
|
} else {
|
|
7454
8142
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
7455
8143
|
}
|
|
7456
|
-
const cursorHooksPath =
|
|
7457
|
-
if (
|
|
8144
|
+
const cursorHooksPath = path21.join(homeDir2, ".cursor", "hooks.json");
|
|
8145
|
+
if (fs19.existsSync(cursorHooksPath)) {
|
|
7458
8146
|
try {
|
|
7459
|
-
const cur = JSON.parse(
|
|
8147
|
+
const cur = JSON.parse(fs19.readFileSync(cursorHooksPath, "utf-8"));
|
|
7460
8148
|
const hasHook = cur.hooks?.preToolUse?.some(
|
|
7461
8149
|
(h) => h.command?.includes("node9") || h.command?.includes("cli.js")
|
|
7462
8150
|
);
|
|
@@ -7494,9 +8182,9 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7494
8182
|
|
|
7495
8183
|
// src/cli/commands/audit.ts
|
|
7496
8184
|
import chalk8 from "chalk";
|
|
7497
|
-
import
|
|
7498
|
-
import
|
|
7499
|
-
import
|
|
8185
|
+
import fs20 from "fs";
|
|
8186
|
+
import path22 from "path";
|
|
8187
|
+
import os18 from "os";
|
|
7500
8188
|
function formatRelativeTime(timestamp) {
|
|
7501
8189
|
const diff = Date.now() - new Date(timestamp).getTime();
|
|
7502
8190
|
const sec = Math.floor(diff / 1e3);
|
|
@@ -7509,14 +8197,14 @@ function formatRelativeTime(timestamp) {
|
|
|
7509
8197
|
}
|
|
7510
8198
|
function registerAuditCommand(program2) {
|
|
7511
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) => {
|
|
7512
|
-
const logPath =
|
|
7513
|
-
if (!
|
|
8200
|
+
const logPath = path22.join(os18.homedir(), ".node9", "audit.log");
|
|
8201
|
+
if (!fs20.existsSync(logPath)) {
|
|
7514
8202
|
console.log(
|
|
7515
8203
|
chalk8.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
7516
8204
|
);
|
|
7517
8205
|
return;
|
|
7518
8206
|
}
|
|
7519
|
-
const raw =
|
|
8207
|
+
const raw = fs20.readFileSync(logPath, "utf-8");
|
|
7520
8208
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
7521
8209
|
let entries = lines.flatMap((line) => {
|
|
7522
8210
|
try {
|
|
@@ -7636,9 +8324,42 @@ function registerDaemonCommand(program2) {
|
|
|
7636
8324
|
init_core();
|
|
7637
8325
|
init_daemon();
|
|
7638
8326
|
import chalk10 from "chalk";
|
|
7639
|
-
import
|
|
7640
|
-
import
|
|
7641
|
-
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
|
+
}
|
|
7642
8363
|
function registerStatusCommand(program2) {
|
|
7643
8364
|
program2.command("status").description("Show current Node9 mode, policy source, and persistent decisions").action(() => {
|
|
7644
8365
|
const creds = getCredentials();
|
|
@@ -7673,19 +8394,72 @@ function registerStatusCommand(program2) {
|
|
|
7673
8394
|
console.log("");
|
|
7674
8395
|
const modeLabel = settings.mode === "audit" ? chalk10.blue("audit") : settings.mode === "strict" ? chalk10.red("strict") : chalk10.white("standard");
|
|
7675
8396
|
console.log(` Mode: ${modeLabel}`);
|
|
7676
|
-
const projectConfig =
|
|
7677
|
-
const globalConfig =
|
|
8397
|
+
const projectConfig = path23.join(process.cwd(), "node9.config.json");
|
|
8398
|
+
const globalConfig = path23.join(os19.homedir(), ".node9", "config.json");
|
|
7678
8399
|
console.log(
|
|
7679
|
-
` Local: ${
|
|
8400
|
+
` Local: ${fs21.existsSync(projectConfig) ? chalk10.green("Active (node9.config.json)") : chalk10.gray("Not present")}`
|
|
7680
8401
|
);
|
|
7681
8402
|
console.log(
|
|
7682
|
-
` Global: ${
|
|
8403
|
+
` Global: ${fs21.existsSync(globalConfig) ? chalk10.green("Active (~/.node9/config.json)") : chalk10.gray("Not present")}`
|
|
7683
8404
|
);
|
|
7684
8405
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
7685
8406
|
console.log(
|
|
7686
8407
|
` Sandbox: ${chalk10.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
7687
8408
|
);
|
|
7688
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
|
+
}
|
|
7689
8463
|
const pauseState = checkPause();
|
|
7690
8464
|
if (pauseState.paused) {
|
|
7691
8465
|
const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
|
|
@@ -7698,8 +8472,63 @@ function registerStatusCommand(program2) {
|
|
|
7698
8472
|
});
|
|
7699
8473
|
}
|
|
7700
8474
|
|
|
7701
|
-
// src/cli/commands/
|
|
8475
|
+
// src/cli/commands/init.ts
|
|
8476
|
+
init_core();
|
|
7702
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";
|
|
7703
8532
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
7704
8533
|
function registerUndoCommand(program2) {
|
|
7705
8534
|
program2.command("undo").description(
|
|
@@ -7711,22 +8540,22 @@ function registerUndoCommand(program2) {
|
|
|
7711
8540
|
if (history.length === 0) {
|
|
7712
8541
|
if (!options.all && allHistory.length > 0) {
|
|
7713
8542
|
console.log(
|
|
7714
|
-
|
|
8543
|
+
chalk12.yellow(
|
|
7715
8544
|
`
|
|
7716
8545
|
\u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
|
|
7717
|
-
Run ${
|
|
8546
|
+
Run ${chalk12.cyan("node9 undo --all")} to see snapshots from all projects.
|
|
7718
8547
|
`
|
|
7719
8548
|
)
|
|
7720
8549
|
);
|
|
7721
8550
|
} else {
|
|
7722
|
-
console.log(
|
|
8551
|
+
console.log(chalk12.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
|
|
7723
8552
|
}
|
|
7724
8553
|
return;
|
|
7725
8554
|
}
|
|
7726
8555
|
const idx = history.length - steps;
|
|
7727
8556
|
if (idx < 0) {
|
|
7728
8557
|
console.log(
|
|
7729
|
-
|
|
8558
|
+
chalk12.yellow(
|
|
7730
8559
|
`
|
|
7731
8560
|
\u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
|
|
7732
8561
|
`
|
|
@@ -7738,19 +8567,19 @@ function registerUndoCommand(program2) {
|
|
|
7738
8567
|
const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
|
|
7739
8568
|
const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
|
|
7740
8569
|
console.log(
|
|
7741
|
-
|
|
8570
|
+
chalk12.magenta.bold(`
|
|
7742
8571
|
\u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`)
|
|
7743
8572
|
);
|
|
7744
8573
|
console.log(
|
|
7745
|
-
|
|
7746
|
-
` Tool: ${
|
|
8574
|
+
chalk12.white(
|
|
8575
|
+
` Tool: ${chalk12.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk12.gray(" \u2192 " + snapshot.argsSummary) : ""}`
|
|
7747
8576
|
)
|
|
7748
8577
|
);
|
|
7749
|
-
console.log(
|
|
7750
|
-
console.log(
|
|
8578
|
+
console.log(chalk12.white(` When: ${chalk12.gray(ageStr)}`));
|
|
8579
|
+
console.log(chalk12.white(` Dir: ${chalk12.gray(snapshot.cwd)}`));
|
|
7751
8580
|
if (steps > 1)
|
|
7752
8581
|
console.log(
|
|
7753
|
-
|
|
8582
|
+
chalk12.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
|
|
7754
8583
|
);
|
|
7755
8584
|
console.log("");
|
|
7756
8585
|
const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
|
|
@@ -7758,21 +8587,21 @@ function registerUndoCommand(program2) {
|
|
|
7758
8587
|
const lines = diff.split("\n");
|
|
7759
8588
|
for (const line of lines) {
|
|
7760
8589
|
if (line.startsWith("+++") || line.startsWith("---")) {
|
|
7761
|
-
console.log(
|
|
8590
|
+
console.log(chalk12.bold(line));
|
|
7762
8591
|
} else if (line.startsWith("+")) {
|
|
7763
|
-
console.log(
|
|
8592
|
+
console.log(chalk12.green(line));
|
|
7764
8593
|
} else if (line.startsWith("-")) {
|
|
7765
|
-
console.log(
|
|
8594
|
+
console.log(chalk12.red(line));
|
|
7766
8595
|
} else if (line.startsWith("@@")) {
|
|
7767
|
-
console.log(
|
|
8596
|
+
console.log(chalk12.cyan(line));
|
|
7768
8597
|
} else {
|
|
7769
|
-
console.log(
|
|
8598
|
+
console.log(chalk12.gray(line));
|
|
7770
8599
|
}
|
|
7771
8600
|
}
|
|
7772
8601
|
console.log("");
|
|
7773
8602
|
} else {
|
|
7774
8603
|
console.log(
|
|
7775
|
-
|
|
8604
|
+
chalk12.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
|
|
7776
8605
|
);
|
|
7777
8606
|
}
|
|
7778
8607
|
const proceed = await confirm2({
|
|
@@ -7781,19 +8610,19 @@ function registerUndoCommand(program2) {
|
|
|
7781
8610
|
});
|
|
7782
8611
|
if (proceed) {
|
|
7783
8612
|
if (applyUndo(snapshot.hash, snapshot.cwd)) {
|
|
7784
|
-
console.log(
|
|
8613
|
+
console.log(chalk12.green("\n\u2705 Reverted successfully.\n"));
|
|
7785
8614
|
} else {
|
|
7786
|
-
console.error(
|
|
8615
|
+
console.error(chalk12.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
|
|
7787
8616
|
}
|
|
7788
8617
|
} else {
|
|
7789
|
-
console.log(
|
|
8618
|
+
console.log(chalk12.gray("\nCancelled.\n"));
|
|
7790
8619
|
}
|
|
7791
8620
|
});
|
|
7792
8621
|
}
|
|
7793
8622
|
|
|
7794
8623
|
// src/cli/commands/watch.ts
|
|
7795
8624
|
init_daemon();
|
|
7796
|
-
import
|
|
8625
|
+
import chalk13 from "chalk";
|
|
7797
8626
|
import { spawn as spawn7, spawnSync as spawnSync5 } from "child_process";
|
|
7798
8627
|
function registerWatchCommand(program2) {
|
|
7799
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) => {
|
|
@@ -7809,7 +8638,7 @@ function registerWatchCommand(program2) {
|
|
|
7809
8638
|
throw new Error("not running");
|
|
7810
8639
|
}
|
|
7811
8640
|
} catch {
|
|
7812
|
-
console.error(
|
|
8641
|
+
console.error(chalk13.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
|
|
7813
8642
|
const child = spawn7(process.execPath, [process.argv[1], "daemon"], {
|
|
7814
8643
|
detached: true,
|
|
7815
8644
|
stdio: "ignore",
|
|
@@ -7831,12 +8660,12 @@ function registerWatchCommand(program2) {
|
|
|
7831
8660
|
}
|
|
7832
8661
|
}
|
|
7833
8662
|
if (!ready) {
|
|
7834
|
-
console.error(
|
|
8663
|
+
console.error(chalk13.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
7835
8664
|
process.exit(1);
|
|
7836
8665
|
}
|
|
7837
8666
|
}
|
|
7838
8667
|
console.error(
|
|
7839
|
-
|
|
8668
|
+
chalk13.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + chalk13.dim(` \u2192 localhost:${port}`) + chalk13.dim(
|
|
7840
8669
|
"\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
|
|
7841
8670
|
)
|
|
7842
8671
|
);
|
|
@@ -7845,7 +8674,7 @@ function registerWatchCommand(program2) {
|
|
|
7845
8674
|
env: { ...process.env, NODE9_WATCH_MODE: "1" }
|
|
7846
8675
|
});
|
|
7847
8676
|
if (result.error) {
|
|
7848
|
-
console.error(
|
|
8677
|
+
console.error(chalk13.red(`\u274C Failed to run command: ${result.error.message}`));
|
|
7849
8678
|
process.exit(1);
|
|
7850
8679
|
}
|
|
7851
8680
|
process.exit(result.status ?? 0);
|
|
@@ -7855,7 +8684,7 @@ function registerWatchCommand(program2) {
|
|
|
7855
8684
|
// src/mcp-gateway/index.ts
|
|
7856
8685
|
init_orchestrator();
|
|
7857
8686
|
import readline2 from "readline";
|
|
7858
|
-
import
|
|
8687
|
+
import chalk14 from "chalk";
|
|
7859
8688
|
import { spawn as spawn8 } from "child_process";
|
|
7860
8689
|
import { execa as execa2 } from "execa";
|
|
7861
8690
|
init_provenance();
|
|
@@ -7918,13 +8747,13 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
7918
8747
|
const prov = checkProvenance(executable);
|
|
7919
8748
|
if (prov.trustLevel === "suspect") {
|
|
7920
8749
|
console.error(
|
|
7921
|
-
|
|
8750
|
+
chalk14.red(
|
|
7922
8751
|
`\u26A0\uFE0F Node9: Upstream MCP server binary is suspect \u2014 ${prov.reason} (${prov.resolvedPath})`
|
|
7923
8752
|
)
|
|
7924
8753
|
);
|
|
7925
|
-
console.error(
|
|
8754
|
+
console.error(chalk14.red(" Verify this binary is trusted before proceeding."));
|
|
7926
8755
|
}
|
|
7927
|
-
console.error(
|
|
8756
|
+
console.error(chalk14.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
|
|
7928
8757
|
const UPSTREAM_INJECTOR_VARS = /* @__PURE__ */ new Set([
|
|
7929
8758
|
"NODE_OPTIONS",
|
|
7930
8759
|
"NODE_PATH",
|
|
@@ -7988,10 +8817,10 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
7988
8817
|
mcpServer
|
|
7989
8818
|
});
|
|
7990
8819
|
if (!result.approved) {
|
|
7991
|
-
console.error(
|
|
8820
|
+
console.error(chalk14.red(`
|
|
7992
8821
|
\u{1F6D1} Node9 MCP Gateway: Action Blocked`));
|
|
7993
|
-
console.error(
|
|
7994
|
-
console.error(
|
|
8822
|
+
console.error(chalk14.gray(` Tool: ${toolName}`));
|
|
8823
|
+
console.error(chalk14.gray(` Reason: ${result.reason ?? "Security Policy"}
|
|
7995
8824
|
`));
|
|
7996
8825
|
const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
|
|
7997
8826
|
const isHumanDecision = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
|
|
@@ -8063,22 +8892,77 @@ function registerMcpGatewayCommand(program2) {
|
|
|
8063
8892
|
});
|
|
8064
8893
|
}
|
|
8065
8894
|
|
|
8895
|
+
// src/cli/commands/trust.ts
|
|
8896
|
+
init_trusted_hosts();
|
|
8897
|
+
import chalk15 from "chalk";
|
|
8898
|
+
function isValidHost(host) {
|
|
8899
|
+
return /^(\*\.)?[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/.test(host);
|
|
8900
|
+
}
|
|
8901
|
+
function registerTrustCommand(program2) {
|
|
8902
|
+
const trustCmd = program2.command("trust").description("Manage trusted network hosts (reduces approval friction for known destinations)");
|
|
8903
|
+
trustCmd.command("add <host>").description("Add a trusted host \u2014 pipe-chain blocks targeting this host are downgraded").action((host) => {
|
|
8904
|
+
const normalized = normalizeHost(host.trim());
|
|
8905
|
+
if (!isValidHost(normalized)) {
|
|
8906
|
+
console.error(
|
|
8907
|
+
chalk15.red(`
|
|
8908
|
+
\u274C Invalid host: "${host}"
|
|
8909
|
+
`) + chalk15.gray(" Use an FQDN like api.mycompany.com or *.mycompany.com\n")
|
|
8910
|
+
);
|
|
8911
|
+
process.exit(1);
|
|
8912
|
+
}
|
|
8913
|
+
addTrustedHost(normalized);
|
|
8914
|
+
console.log(chalk15.green(`
|
|
8915
|
+
\u2705 ${normalized} added to trusted hosts.`));
|
|
8916
|
+
console.log(
|
|
8917
|
+
chalk15.gray(" Pipe-chain blocks to this host: critical \u2192 review, high \u2192 allow\n")
|
|
8918
|
+
);
|
|
8919
|
+
});
|
|
8920
|
+
trustCmd.command("remove <host>").description("Remove a trusted host").action((host) => {
|
|
8921
|
+
const normalized = normalizeHost(host.trim());
|
|
8922
|
+
const removed = removeTrustedHost(normalized);
|
|
8923
|
+
if (!removed) {
|
|
8924
|
+
console.error(chalk15.yellow(`
|
|
8925
|
+
\u26A0\uFE0F "${normalized}" is not in the trusted hosts list.
|
|
8926
|
+
`));
|
|
8927
|
+
process.exit(1);
|
|
8928
|
+
}
|
|
8929
|
+
console.log(chalk15.green(`
|
|
8930
|
+
\u2705 ${normalized} removed from trusted hosts.
|
|
8931
|
+
`));
|
|
8932
|
+
});
|
|
8933
|
+
trustCmd.command("list").description("Show all trusted hosts").action(() => {
|
|
8934
|
+
const hosts = readTrustedHosts();
|
|
8935
|
+
if (hosts.length === 0) {
|
|
8936
|
+
console.log(chalk15.gray("\n No trusted hosts configured.\n"));
|
|
8937
|
+
console.log(` Add one: ${chalk15.cyan("node9 trust add api.mycompany.com")}
|
|
8938
|
+
`);
|
|
8939
|
+
return;
|
|
8940
|
+
}
|
|
8941
|
+
console.log(chalk15.bold("\n\u{1F513} Trusted Hosts\n"));
|
|
8942
|
+
for (const entry of hosts) {
|
|
8943
|
+
const date = new Date(entry.addedAt).toLocaleDateString();
|
|
8944
|
+
console.log(` ${chalk15.cyan(entry.host.padEnd(40))} ${chalk15.gray(`added ${date}`)}`);
|
|
8945
|
+
}
|
|
8946
|
+
console.log("");
|
|
8947
|
+
});
|
|
8948
|
+
}
|
|
8949
|
+
|
|
8066
8950
|
// src/cli.ts
|
|
8067
8951
|
var { version } = JSON.parse(
|
|
8068
|
-
|
|
8952
|
+
fs24.readFileSync(path26.join(__dirname, "../package.json"), "utf-8")
|
|
8069
8953
|
);
|
|
8070
8954
|
var program = new Command();
|
|
8071
8955
|
program.name("node9").description("The Sudo Command for AI Agents").version(version);
|
|
8072
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) => {
|
|
8073
8957
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
8074
|
-
const credPath =
|
|
8075
|
-
if (!
|
|
8076
|
-
|
|
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 });
|
|
8077
8961
|
const profileName = options.profile || "default";
|
|
8078
8962
|
let existingCreds = {};
|
|
8079
8963
|
try {
|
|
8080
|
-
if (
|
|
8081
|
-
const raw = JSON.parse(
|
|
8964
|
+
if (fs24.existsSync(credPath)) {
|
|
8965
|
+
const raw = JSON.parse(fs24.readFileSync(credPath, "utf-8"));
|
|
8082
8966
|
if (raw.apiKey) {
|
|
8083
8967
|
existingCreds = {
|
|
8084
8968
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
|
|
@@ -8090,13 +8974,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
8090
8974
|
} catch {
|
|
8091
8975
|
}
|
|
8092
8976
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
8093
|
-
|
|
8977
|
+
fs24.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
8094
8978
|
if (profileName === "default") {
|
|
8095
|
-
const configPath =
|
|
8979
|
+
const configPath = path26.join(os22.homedir(), ".node9", "config.json");
|
|
8096
8980
|
let config = {};
|
|
8097
8981
|
try {
|
|
8098
|
-
if (
|
|
8099
|
-
config = JSON.parse(
|
|
8982
|
+
if (fs24.existsSync(configPath))
|
|
8983
|
+
config = JSON.parse(fs24.readFileSync(configPath, "utf-8"));
|
|
8100
8984
|
} catch {
|
|
8101
8985
|
}
|
|
8102
8986
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -8111,36 +8995,36 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
8111
8995
|
approvers.cloud = false;
|
|
8112
8996
|
}
|
|
8113
8997
|
s.approvers = approvers;
|
|
8114
|
-
if (!
|
|
8115
|
-
|
|
8116
|
-
|
|
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 });
|
|
8117
9001
|
}
|
|
8118
9002
|
if (options.profile && profileName !== "default") {
|
|
8119
|
-
console.log(
|
|
8120
|
-
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`));
|
|
8121
9005
|
} else if (options.local) {
|
|
8122
|
-
console.log(
|
|
8123
|
-
console.log(
|
|
9006
|
+
console.log(chalk17.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
9007
|
+
console.log(chalk17.gray(` All decisions stay on this machine.`));
|
|
8124
9008
|
} else {
|
|
8125
|
-
console.log(
|
|
8126
|
-
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.`));
|
|
8127
9011
|
}
|
|
8128
9012
|
});
|
|
8129
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) => {
|
|
8130
9014
|
if (target === "gemini") return await setupGemini();
|
|
8131
9015
|
if (target === "claude") return await setupClaude();
|
|
8132
9016
|
if (target === "cursor") return await setupCursor();
|
|
8133
|
-
console.error(
|
|
9017
|
+
console.error(chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
8134
9018
|
process.exit(1);
|
|
8135
9019
|
});
|
|
8136
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) => {
|
|
8137
9021
|
if (!target) {
|
|
8138
|
-
console.log(
|
|
8139
|
-
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");
|
|
8140
9024
|
console.log(" Targets:");
|
|
8141
|
-
console.log(" " +
|
|
8142
|
-
console.log(" " +
|
|
8143
|
-
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)");
|
|
8144
9028
|
console.log("");
|
|
8145
9029
|
return;
|
|
8146
9030
|
}
|
|
@@ -8148,7 +9032,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
8148
9032
|
if (t === "gemini") return await setupGemini();
|
|
8149
9033
|
if (t === "claude") return await setupClaude();
|
|
8150
9034
|
if (t === "cursor") return await setupCursor();
|
|
8151
|
-
console.error(
|
|
9035
|
+
console.error(chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
8152
9036
|
process.exit(1);
|
|
8153
9037
|
});
|
|
8154
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) => {
|
|
@@ -8157,30 +9041,30 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
|
|
|
8157
9041
|
else if (target === "gemini") fn = teardownGemini;
|
|
8158
9042
|
else if (target === "cursor") fn = teardownCursor;
|
|
8159
9043
|
else {
|
|
8160
|
-
console.error(
|
|
9044
|
+
console.error(chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
8161
9045
|
process.exit(1);
|
|
8162
9046
|
}
|
|
8163
|
-
console.log(
|
|
9047
|
+
console.log(chalk17.cyan(`
|
|
8164
9048
|
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
8165
9049
|
`));
|
|
8166
9050
|
try {
|
|
8167
9051
|
fn();
|
|
8168
9052
|
} catch (err) {
|
|
8169
|
-
console.error(
|
|
9053
|
+
console.error(chalk17.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
8170
9054
|
process.exit(1);
|
|
8171
9055
|
}
|
|
8172
|
-
console.log(
|
|
9056
|
+
console.log(chalk17.gray("\n Restart the agent for changes to take effect."));
|
|
8173
9057
|
});
|
|
8174
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) => {
|
|
8175
|
-
console.log(
|
|
8176
|
-
console.log(
|
|
9059
|
+
console.log(chalk17.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
9060
|
+
console.log(chalk17.bold("Stopping daemon..."));
|
|
8177
9061
|
try {
|
|
8178
9062
|
stopDaemon();
|
|
8179
|
-
console.log(
|
|
9063
|
+
console.log(chalk17.green(" \u2705 Daemon stopped"));
|
|
8180
9064
|
} catch {
|
|
8181
|
-
console.log(
|
|
9065
|
+
console.log(chalk17.blue(" \u2139\uFE0F Daemon was not running"));
|
|
8182
9066
|
}
|
|
8183
|
-
console.log(
|
|
9067
|
+
console.log(chalk17.bold("\nRemoving hooks..."));
|
|
8184
9068
|
let teardownFailed = false;
|
|
8185
9069
|
for (const [label, fn] of [
|
|
8186
9070
|
["Claude", teardownClaude],
|
|
@@ -8192,45 +9076,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
8192
9076
|
} catch (err) {
|
|
8193
9077
|
teardownFailed = true;
|
|
8194
9078
|
console.error(
|
|
8195
|
-
|
|
9079
|
+
chalk17.red(
|
|
8196
9080
|
` \u26A0\uFE0F Failed to remove ${label} hooks: ${err instanceof Error ? err.message : String(err)}`
|
|
8197
9081
|
)
|
|
8198
9082
|
);
|
|
8199
9083
|
}
|
|
8200
9084
|
}
|
|
8201
9085
|
if (options.purge) {
|
|
8202
|
-
const node9Dir =
|
|
8203
|
-
if (
|
|
9086
|
+
const node9Dir = path26.join(os22.homedir(), ".node9");
|
|
9087
|
+
if (fs24.existsSync(node9Dir)) {
|
|
8204
9088
|
const confirmed = await confirm3({
|
|
8205
9089
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
8206
9090
|
default: false
|
|
8207
9091
|
});
|
|
8208
9092
|
if (confirmed) {
|
|
8209
|
-
|
|
8210
|
-
if (
|
|
9093
|
+
fs24.rmSync(node9Dir, { recursive: true });
|
|
9094
|
+
if (fs24.existsSync(node9Dir)) {
|
|
8211
9095
|
console.error(
|
|
8212
|
-
|
|
9096
|
+
chalk17.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
8213
9097
|
);
|
|
8214
9098
|
} else {
|
|
8215
|
-
console.log(
|
|
9099
|
+
console.log(chalk17.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
8216
9100
|
}
|
|
8217
9101
|
} else {
|
|
8218
|
-
console.log(
|
|
9102
|
+
console.log(chalk17.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
8219
9103
|
}
|
|
8220
9104
|
} else {
|
|
8221
|
-
console.log(
|
|
9105
|
+
console.log(chalk17.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
8222
9106
|
}
|
|
8223
9107
|
} else {
|
|
8224
9108
|
console.log(
|
|
8225
|
-
|
|
9109
|
+
chalk17.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
8226
9110
|
);
|
|
8227
9111
|
}
|
|
8228
9112
|
if (teardownFailed) {
|
|
8229
|
-
console.error(
|
|
9113
|
+
console.error(chalk17.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
8230
9114
|
process.exit(1);
|
|
8231
9115
|
}
|
|
8232
|
-
console.log(
|
|
8233
|
-
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"));
|
|
8234
9118
|
});
|
|
8235
9119
|
registerDoctorCommand(program, version);
|
|
8236
9120
|
program.command("explain").description(
|
|
@@ -8243,7 +9127,7 @@ program.command("explain").description(
|
|
|
8243
9127
|
try {
|
|
8244
9128
|
args = JSON.parse(trimmed);
|
|
8245
9129
|
} catch {
|
|
8246
|
-
console.error(
|
|
9130
|
+
console.error(chalk17.red(`
|
|
8247
9131
|
\u274C Invalid JSON: ${trimmed}
|
|
8248
9132
|
`));
|
|
8249
9133
|
process.exit(1);
|
|
@@ -8254,83 +9138,59 @@ program.command("explain").description(
|
|
|
8254
9138
|
}
|
|
8255
9139
|
const result = await explainPolicy(tool, args);
|
|
8256
9140
|
console.log("");
|
|
8257
|
-
console.log(
|
|
9141
|
+
console.log(chalk17.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
8258
9142
|
console.log("");
|
|
8259
|
-
console.log(` ${
|
|
9143
|
+
console.log(` ${chalk17.bold("Tool:")} ${chalk17.white(result.tool)}`);
|
|
8260
9144
|
if (argsRaw) {
|
|
8261
9145
|
const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
8262
|
-
console.log(` ${
|
|
9146
|
+
console.log(` ${chalk17.bold("Input:")} ${chalk17.gray(preview)}`);
|
|
8263
9147
|
}
|
|
8264
9148
|
console.log("");
|
|
8265
|
-
console.log(
|
|
9149
|
+
console.log(chalk17.bold("Config Sources (Waterfall):"));
|
|
8266
9150
|
for (const tier of result.waterfall) {
|
|
8267
|
-
const num =
|
|
9151
|
+
const num = chalk17.gray(` ${tier.tier}.`);
|
|
8268
9152
|
const label = tier.label.padEnd(16);
|
|
8269
9153
|
let statusStr;
|
|
8270
9154
|
if (tier.tier === 1) {
|
|
8271
|
-
statusStr =
|
|
9155
|
+
statusStr = chalk17.gray(tier.note ?? "");
|
|
8272
9156
|
} else if (tier.status === "active") {
|
|
8273
|
-
const loc = tier.path ?
|
|
8274
|
-
const note = tier.note ?
|
|
8275
|
-
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 : "");
|
|
8276
9160
|
} else {
|
|
8277
|
-
statusStr =
|
|
9161
|
+
statusStr = chalk17.gray("\u25CB " + (tier.note ?? "not found"));
|
|
8278
9162
|
}
|
|
8279
|
-
console.log(`${num} ${
|
|
9163
|
+
console.log(`${num} ${chalk17.white(label)} ${statusStr}`);
|
|
8280
9164
|
}
|
|
8281
9165
|
console.log("");
|
|
8282
|
-
console.log(
|
|
9166
|
+
console.log(chalk17.bold("Policy Evaluation:"));
|
|
8283
9167
|
for (const step of result.steps) {
|
|
8284
9168
|
const isFinal = step.isFinal;
|
|
8285
9169
|
let icon;
|
|
8286
|
-
if (step.outcome === "allow") icon =
|
|
8287
|
-
else if (step.outcome === "review") icon =
|
|
8288
|
-
else if (step.outcome === "skip") icon =
|
|
8289
|
-
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 ");
|
|
8290
9174
|
const name = step.name.padEnd(18);
|
|
8291
|
-
const nameStr = isFinal ?
|
|
8292
|
-
const detail = isFinal ?
|
|
8293
|
-
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") : "";
|
|
8294
9178
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
8295
9179
|
}
|
|
8296
9180
|
console.log("");
|
|
8297
9181
|
if (result.decision === "allow") {
|
|
8298
|
-
console.log(
|
|
9182
|
+
console.log(chalk17.green.bold(" Decision: \u2705 ALLOW") + chalk17.gray(" \u2014 no approval needed"));
|
|
8299
9183
|
} else {
|
|
8300
9184
|
console.log(
|
|
8301
|
-
|
|
9185
|
+
chalk17.red.bold(" Decision: \u{1F534} REVIEW") + chalk17.gray(" \u2014 human approval required")
|
|
8302
9186
|
);
|
|
8303
9187
|
if (result.blockedByLabel) {
|
|
8304
|
-
console.log(
|
|
9188
|
+
console.log(chalk17.gray(` Reason: ${result.blockedByLabel}`));
|
|
8305
9189
|
}
|
|
8306
9190
|
}
|
|
8307
9191
|
console.log("");
|
|
8308
9192
|
});
|
|
8309
|
-
program
|
|
8310
|
-
const configPath = path23.join(os19.homedir(), ".node9", "config.json");
|
|
8311
|
-
if (fs21.existsSync(configPath) && !options.force) {
|
|
8312
|
-
console.log(chalk15.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
8313
|
-
console.log(chalk15.gray(` Run with --force to overwrite.`));
|
|
8314
|
-
return;
|
|
8315
|
-
}
|
|
8316
|
-
const requestedMode = options.mode.toLowerCase();
|
|
8317
|
-
const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
|
|
8318
|
-
const configToSave = {
|
|
8319
|
-
...DEFAULT_CONFIG,
|
|
8320
|
-
settings: {
|
|
8321
|
-
...DEFAULT_CONFIG.settings,
|
|
8322
|
-
mode: safeMode
|
|
8323
|
-
}
|
|
8324
|
-
};
|
|
8325
|
-
const dir = path23.dirname(configPath);
|
|
8326
|
-
if (!fs21.existsSync(dir)) fs21.mkdirSync(dir, { recursive: true });
|
|
8327
|
-
fs21.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
8328
|
-
console.log(chalk15.green(`\u2705 Global config created: ${configPath}`));
|
|
8329
|
-
console.log(chalk15.cyan(` Mode set to: ${safeMode}`));
|
|
8330
|
-
console.log(
|
|
8331
|
-
chalk15.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
|
|
8332
|
-
);
|
|
8333
|
-
});
|
|
9193
|
+
registerInitCommand(program);
|
|
8334
9194
|
registerAuditCommand(program);
|
|
8335
9195
|
registerStatusCommand(program);
|
|
8336
9196
|
registerDaemonCommand(program);
|
|
@@ -8339,7 +9199,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
|
|
|
8339
9199
|
try {
|
|
8340
9200
|
await startTail2(options);
|
|
8341
9201
|
} catch (err) {
|
|
8342
|
-
console.error(
|
|
9202
|
+
console.error(chalk17.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
|
|
8343
9203
|
process.exit(1);
|
|
8344
9204
|
}
|
|
8345
9205
|
});
|
|
@@ -8351,7 +9211,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
8351
9211
|
const ms = parseDuration(options.duration);
|
|
8352
9212
|
if (ms === null) {
|
|
8353
9213
|
console.error(
|
|
8354
|
-
|
|
9214
|
+
chalk17.red(`
|
|
8355
9215
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
8356
9216
|
`)
|
|
8357
9217
|
);
|
|
@@ -8359,20 +9219,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
8359
9219
|
}
|
|
8360
9220
|
pauseNode9(ms, options.duration);
|
|
8361
9221
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
8362
|
-
console.log(
|
|
9222
|
+
console.log(chalk17.yellow(`
|
|
8363
9223
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
8364
|
-
console.log(
|
|
8365
|
-
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.
|
|
8366
9226
|
`));
|
|
8367
9227
|
});
|
|
8368
9228
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
8369
9229
|
const { paused } = checkPause();
|
|
8370
9230
|
if (!paused) {
|
|
8371
|
-
console.log(
|
|
9231
|
+
console.log(chalk17.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
8372
9232
|
return;
|
|
8373
9233
|
}
|
|
8374
9234
|
resumeNode9();
|
|
8375
|
-
console.log(
|
|
9235
|
+
console.log(chalk17.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
8376
9236
|
});
|
|
8377
9237
|
var HOOK_BASED_AGENTS = {
|
|
8378
9238
|
claude: "claude",
|
|
@@ -8385,15 +9245,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
8385
9245
|
if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
|
|
8386
9246
|
const target = HOOK_BASED_AGENTS[firstArg2];
|
|
8387
9247
|
console.error(
|
|
8388
|
-
|
|
9248
|
+
chalk17.yellow(`
|
|
8389
9249
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
8390
9250
|
);
|
|
8391
|
-
console.error(
|
|
9251
|
+
console.error(chalk17.white(`
|
|
8392
9252
|
"${target}" uses its own hook system. Use:`));
|
|
8393
9253
|
console.error(
|
|
8394
|
-
|
|
9254
|
+
chalk17.green(` node9 addto ${target} `) + chalk17.gray("# one-time setup")
|
|
8395
9255
|
);
|
|
8396
|
-
console.error(
|
|
9256
|
+
console.error(chalk17.green(` ${target} `) + chalk17.gray("# run normally"));
|
|
8397
9257
|
process.exit(1);
|
|
8398
9258
|
}
|
|
8399
9259
|
const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
|
|
@@ -8410,7 +9270,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
8410
9270
|
}
|
|
8411
9271
|
);
|
|
8412
9272
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
8413
|
-
console.error(
|
|
9273
|
+
console.error(chalk17.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
8414
9274
|
const daemonReady = await autoStartDaemonAndWait();
|
|
8415
9275
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
8416
9276
|
}
|
|
@@ -8423,12 +9283,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
8423
9283
|
}
|
|
8424
9284
|
if (!result.approved) {
|
|
8425
9285
|
console.error(
|
|
8426
|
-
|
|
9286
|
+
chalk17.red(`
|
|
8427
9287
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
8428
9288
|
);
|
|
8429
9289
|
process.exit(1);
|
|
8430
9290
|
}
|
|
8431
|
-
console.error(
|
|
9291
|
+
console.error(chalk17.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
8432
9292
|
await runProxy(fullCommand);
|
|
8433
9293
|
} else {
|
|
8434
9294
|
program.help();
|
|
@@ -8437,14 +9297,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
8437
9297
|
registerUndoCommand(program);
|
|
8438
9298
|
registerShieldCommand(program);
|
|
8439
9299
|
registerConfigShowCommand(program);
|
|
9300
|
+
registerTrustCommand(program);
|
|
8440
9301
|
if (process.argv[2] !== "daemon") {
|
|
8441
9302
|
process.on("unhandledRejection", (reason) => {
|
|
8442
9303
|
const isCheckHook = process.argv[2] === "check";
|
|
8443
9304
|
if (isCheckHook) {
|
|
8444
9305
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
8445
|
-
const logPath =
|
|
9306
|
+
const logPath = path26.join(os22.homedir(), ".node9", "hook-debug.log");
|
|
8446
9307
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
8447
|
-
|
|
9308
|
+
fs24.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
8448
9309
|
`);
|
|
8449
9310
|
}
|
|
8450
9311
|
process.exit(0);
|