@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.js
CHANGED
|
@@ -114,8 +114,8 @@ function sanitizeConfig(raw) {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
const lines = result.error.issues.map((issue) => {
|
|
117
|
-
const
|
|
118
|
-
return ` \u2022 ${
|
|
117
|
+
const path27 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
118
|
+
return ` \u2022 ${path27}: ${issue.message}`;
|
|
119
119
|
});
|
|
120
120
|
return {
|
|
121
121
|
sanitized,
|
|
@@ -1610,6 +1610,92 @@ var init_ssh_parser = __esm({
|
|
|
1610
1610
|
}
|
|
1611
1611
|
});
|
|
1612
1612
|
|
|
1613
|
+
// src/auth/trusted-hosts.ts
|
|
1614
|
+
function getTrustedHostsPath() {
|
|
1615
|
+
return import_path7.default.join(import_os5.default.homedir(), ".node9", "trusted-hosts.json");
|
|
1616
|
+
}
|
|
1617
|
+
function readTrustedHosts() {
|
|
1618
|
+
try {
|
|
1619
|
+
const raw = import_fs6.default.readFileSync(getTrustedHostsPath(), "utf8");
|
|
1620
|
+
const parsed = JSON.parse(raw);
|
|
1621
|
+
return Array.isArray(parsed.hosts) ? parsed.hosts : [];
|
|
1622
|
+
} catch {
|
|
1623
|
+
return [];
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
function getFileMtime() {
|
|
1627
|
+
try {
|
|
1628
|
+
return import_fs6.default.statSync(getTrustedHostsPath()).mtimeMs;
|
|
1629
|
+
} catch {
|
|
1630
|
+
return 0;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
function getCachedHosts() {
|
|
1634
|
+
const now = Date.now();
|
|
1635
|
+
if (_cache && now < _cache.expiry) {
|
|
1636
|
+
const mtime = getFileMtime();
|
|
1637
|
+
if (mtime === _cache.mtime) return _cache.hosts;
|
|
1638
|
+
}
|
|
1639
|
+
const hosts = readTrustedHosts();
|
|
1640
|
+
_cache = { hosts, expiry: now + CACHE_TTL_MS, mtime: getFileMtime() };
|
|
1641
|
+
return hosts;
|
|
1642
|
+
}
|
|
1643
|
+
function writeTrustedHosts(hosts) {
|
|
1644
|
+
const filePath = getTrustedHostsPath();
|
|
1645
|
+
import_fs6.default.mkdirSync(import_path7.default.dirname(filePath), { recursive: true });
|
|
1646
|
+
const tmp = filePath + ".node9-tmp";
|
|
1647
|
+
import_fs6.default.writeFileSync(tmp, JSON.stringify({ hosts }, null, 2), { mode: 384 });
|
|
1648
|
+
import_fs6.default.renameSync(tmp, filePath);
|
|
1649
|
+
_cache = { hosts, expiry: Date.now() + CACHE_TTL_MS, mtime: getFileMtime() };
|
|
1650
|
+
}
|
|
1651
|
+
function addTrustedHost(host) {
|
|
1652
|
+
const normalized = normalizeHost(host);
|
|
1653
|
+
if (normalized.startsWith("*.")) {
|
|
1654
|
+
const base = normalized.slice(2);
|
|
1655
|
+
if (!base.includes(".")) {
|
|
1656
|
+
throw new Error(
|
|
1657
|
+
`Wildcard pattern '${normalized}' is too broad \u2014 the base domain must have at least one dot (e.g. '*.mycompany.com', not '*.com').`
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
const hosts = readTrustedHosts();
|
|
1662
|
+
if (hosts.some((h) => h.host === normalized)) return;
|
|
1663
|
+
hosts.push({ host: normalized, addedAt: Date.now(), addedBy: "user" });
|
|
1664
|
+
writeTrustedHosts(hosts);
|
|
1665
|
+
}
|
|
1666
|
+
function removeTrustedHost(host) {
|
|
1667
|
+
const hosts = readTrustedHosts();
|
|
1668
|
+
const filtered = hosts.filter((h) => h.host !== host);
|
|
1669
|
+
if (filtered.length === hosts.length) return false;
|
|
1670
|
+
writeTrustedHosts(filtered);
|
|
1671
|
+
return true;
|
|
1672
|
+
}
|
|
1673
|
+
function normalizeHost(raw) {
|
|
1674
|
+
return raw.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^[^@]+@/, "").replace(/:\d+$/, "");
|
|
1675
|
+
}
|
|
1676
|
+
function isTrustedHost(host) {
|
|
1677
|
+
const normalized = normalizeHost(host);
|
|
1678
|
+
return getCachedHosts().some((entry) => {
|
|
1679
|
+
const entryHost = entry.host.toLowerCase();
|
|
1680
|
+
if (entryHost.startsWith("*.")) {
|
|
1681
|
+
const domain = entryHost.slice(2);
|
|
1682
|
+
return normalized.endsWith("." + domain);
|
|
1683
|
+
}
|
|
1684
|
+
return normalized === entryHost;
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
var import_fs6, import_path7, import_os5, _cache, CACHE_TTL_MS;
|
|
1688
|
+
var init_trusted_hosts = __esm({
|
|
1689
|
+
"src/auth/trusted-hosts.ts"() {
|
|
1690
|
+
"use strict";
|
|
1691
|
+
import_fs6 = __toESM(require("fs"));
|
|
1692
|
+
import_path7 = __toESM(require("path"));
|
|
1693
|
+
import_os5 = __toESM(require("os"));
|
|
1694
|
+
_cache = null;
|
|
1695
|
+
CACHE_TTL_MS = 5e3;
|
|
1696
|
+
}
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1613
1699
|
// src/policy/index.ts
|
|
1614
1700
|
function tokenize2(toolName) {
|
|
1615
1701
|
return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
|
|
@@ -1624,9 +1710,9 @@ function matchesPattern(text, patterns) {
|
|
|
1624
1710
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1625
1711
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1626
1712
|
}
|
|
1627
|
-
function getNestedValue(obj,
|
|
1713
|
+
function getNestedValue(obj, path27) {
|
|
1628
1714
|
if (!obj || typeof obj !== "object") return null;
|
|
1629
|
-
return
|
|
1715
|
+
return path27.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1630
1716
|
}
|
|
1631
1717
|
function shouldSnapshot(toolName, args, config) {
|
|
1632
1718
|
if (!config.settings.enableUndo) return false;
|
|
@@ -1792,23 +1878,39 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1792
1878
|
return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
|
|
1793
1879
|
}
|
|
1794
1880
|
const pipeAnalysis = analyzePipeChain(shellCommand);
|
|
1795
|
-
if (pipeAnalysis.isPipeline) {
|
|
1881
|
+
if (pipeAnalysis.isPipeline && (pipeAnalysis.risk === "critical" || pipeAnalysis.risk === "high")) {
|
|
1882
|
+
const sinks = pipeAnalysis.sinkTargets;
|
|
1883
|
+
const allTrusted = sinks.length > 0 && sinks.every(isTrustedHost);
|
|
1796
1884
|
if (pipeAnalysis.risk === "critical") {
|
|
1885
|
+
if (allTrusted) {
|
|
1886
|
+
return {
|
|
1887
|
+
decision: "review",
|
|
1888
|
+
blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
|
|
1889
|
+
reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
|
|
1890
|
+
tier: 3
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1797
1893
|
return {
|
|
1798
1894
|
decision: "block",
|
|
1799
1895
|
blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
|
|
1800
|
-
reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${
|
|
1896
|
+
reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
1801
1897
|
tier: 3
|
|
1802
1898
|
};
|
|
1803
1899
|
}
|
|
1804
|
-
if (
|
|
1900
|
+
if (allTrusted) {
|
|
1805
1901
|
return {
|
|
1806
|
-
decision: "
|
|
1807
|
-
blockedByLabel: "Node9: Pipe-Chain
|
|
1808
|
-
reason: `Sensitive file piped to
|
|
1902
|
+
decision: "allow",
|
|
1903
|
+
blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
|
|
1904
|
+
reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
|
|
1809
1905
|
tier: 3
|
|
1810
1906
|
};
|
|
1811
1907
|
}
|
|
1908
|
+
return {
|
|
1909
|
+
decision: "review",
|
|
1910
|
+
blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
|
|
1911
|
+
reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
1912
|
+
tier: 3
|
|
1913
|
+
};
|
|
1812
1914
|
}
|
|
1813
1915
|
const firstToken = analyzed.actions[0] ?? "";
|
|
1814
1916
|
if (["ssh", "scp", "rsync"].includes(firstToken)) {
|
|
@@ -1816,7 +1918,7 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1816
1918
|
const sshHosts = extractAllSshHosts(rawTokens.slice(1));
|
|
1817
1919
|
allTokens.push(...sshHosts);
|
|
1818
1920
|
}
|
|
1819
|
-
if (firstToken &&
|
|
1921
|
+
if (firstToken && import_path8.default.posix.isAbsolute(firstToken)) {
|
|
1820
1922
|
const prov = checkProvenance(firstToken, cwd);
|
|
1821
1923
|
if (prov.trustLevel === "suspect") {
|
|
1822
1924
|
return {
|
|
@@ -1913,9 +2015,9 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1913
2015
|
}
|
|
1914
2016
|
async function explainPolicy(toolName, args) {
|
|
1915
2017
|
const steps = [];
|
|
1916
|
-
const globalPath =
|
|
1917
|
-
const projectPath =
|
|
1918
|
-
const credsPath =
|
|
2018
|
+
const globalPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "config.json");
|
|
2019
|
+
const projectPath = import_path8.default.join(process.cwd(), "node9.config.json");
|
|
2020
|
+
const credsPath = import_path8.default.join(import_os6.default.homedir(), ".node9", "credentials.json");
|
|
1919
2021
|
const waterfall = [
|
|
1920
2022
|
{
|
|
1921
2023
|
tier: 1,
|
|
@@ -1926,19 +2028,19 @@ async function explainPolicy(toolName, args) {
|
|
|
1926
2028
|
{
|
|
1927
2029
|
tier: 2,
|
|
1928
2030
|
label: "Cloud policy",
|
|
1929
|
-
status:
|
|
1930
|
-
note:
|
|
2031
|
+
status: import_fs7.default.existsSync(credsPath) ? "active" : "missing",
|
|
2032
|
+
note: import_fs7.default.existsSync(credsPath) ? "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
|
|
1931
2033
|
},
|
|
1932
2034
|
{
|
|
1933
2035
|
tier: 3,
|
|
1934
2036
|
label: "Project config",
|
|
1935
|
-
status:
|
|
2037
|
+
status: import_fs7.default.existsSync(projectPath) ? "active" : "missing",
|
|
1936
2038
|
path: projectPath
|
|
1937
2039
|
},
|
|
1938
2040
|
{
|
|
1939
2041
|
tier: 4,
|
|
1940
2042
|
label: "Global config",
|
|
1941
|
-
status:
|
|
2043
|
+
status: import_fs7.default.existsSync(globalPath) ? "active" : "missing",
|
|
1942
2044
|
path: globalPath
|
|
1943
2045
|
},
|
|
1944
2046
|
{
|
|
@@ -2175,13 +2277,13 @@ function isIgnoredTool(toolName) {
|
|
|
2175
2277
|
const config = getConfig();
|
|
2176
2278
|
return matchesPattern(toolName, config.policy.ignoredTools);
|
|
2177
2279
|
}
|
|
2178
|
-
var
|
|
2280
|
+
var import_fs7, import_path8, import_os6, import_picomatch, import_sh_syntax, SQL_DML_KEYWORDS;
|
|
2179
2281
|
var init_policy = __esm({
|
|
2180
2282
|
"src/policy/index.ts"() {
|
|
2181
2283
|
"use strict";
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2284
|
+
import_fs7 = __toESM(require("fs"));
|
|
2285
|
+
import_path8 = __toESM(require("path"));
|
|
2286
|
+
import_os6 = __toESM(require("os"));
|
|
2185
2287
|
import_picomatch = __toESM(require("picomatch"));
|
|
2186
2288
|
import_sh_syntax = require("sh-syntax");
|
|
2187
2289
|
init_dlp();
|
|
@@ -2190,6 +2292,7 @@ var init_policy = __esm({
|
|
|
2190
2292
|
init_provenance();
|
|
2191
2293
|
init_pipe_chain();
|
|
2192
2294
|
init_ssh_parser();
|
|
2295
|
+
init_trusted_hosts();
|
|
2193
2296
|
SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
2194
2297
|
}
|
|
2195
2298
|
});
|
|
@@ -2197,11 +2300,11 @@ var init_policy = __esm({
|
|
|
2197
2300
|
// src/auth/state.ts
|
|
2198
2301
|
function checkPause() {
|
|
2199
2302
|
try {
|
|
2200
|
-
if (!
|
|
2201
|
-
const state = JSON.parse(
|
|
2303
|
+
if (!import_fs8.default.existsSync(PAUSED_FILE)) return { paused: false };
|
|
2304
|
+
const state = JSON.parse(import_fs8.default.readFileSync(PAUSED_FILE, "utf-8"));
|
|
2202
2305
|
if (state.expiry > 0 && Date.now() >= state.expiry) {
|
|
2203
2306
|
try {
|
|
2204
|
-
|
|
2307
|
+
import_fs8.default.unlinkSync(PAUSED_FILE);
|
|
2205
2308
|
} catch {
|
|
2206
2309
|
}
|
|
2207
2310
|
return { paused: false };
|
|
@@ -2212,11 +2315,11 @@ function checkPause() {
|
|
|
2212
2315
|
}
|
|
2213
2316
|
}
|
|
2214
2317
|
function atomicWriteSync(filePath, data, options) {
|
|
2215
|
-
const dir =
|
|
2216
|
-
if (!
|
|
2217
|
-
const tmpPath = `${filePath}.${
|
|
2218
|
-
|
|
2219
|
-
|
|
2318
|
+
const dir = import_path9.default.dirname(filePath);
|
|
2319
|
+
if (!import_fs8.default.existsSync(dir)) import_fs8.default.mkdirSync(dir, { recursive: true });
|
|
2320
|
+
const tmpPath = `${filePath}.${import_os7.default.hostname()}.${process.pid}.tmp`;
|
|
2321
|
+
import_fs8.default.writeFileSync(tmpPath, data, options);
|
|
2322
|
+
import_fs8.default.renameSync(tmpPath, filePath);
|
|
2220
2323
|
}
|
|
2221
2324
|
function pauseNode9(durationMs, durationStr) {
|
|
2222
2325
|
const state = { expiry: Date.now() + durationMs, duration: durationStr };
|
|
@@ -2224,18 +2327,18 @@ function pauseNode9(durationMs, durationStr) {
|
|
|
2224
2327
|
}
|
|
2225
2328
|
function resumeNode9() {
|
|
2226
2329
|
try {
|
|
2227
|
-
if (
|
|
2330
|
+
if (import_fs8.default.existsSync(PAUSED_FILE)) import_fs8.default.unlinkSync(PAUSED_FILE);
|
|
2228
2331
|
} catch {
|
|
2229
2332
|
}
|
|
2230
2333
|
}
|
|
2231
2334
|
function getActiveTrustSession(toolName) {
|
|
2232
2335
|
try {
|
|
2233
|
-
if (!
|
|
2234
|
-
const trust = JSON.parse(
|
|
2336
|
+
if (!import_fs8.default.existsSync(TRUST_FILE)) return false;
|
|
2337
|
+
const trust = JSON.parse(import_fs8.default.readFileSync(TRUST_FILE, "utf-8"));
|
|
2235
2338
|
const now = Date.now();
|
|
2236
2339
|
const active = trust.entries.filter((e) => e.expiry > now);
|
|
2237
2340
|
if (active.length !== trust.entries.length) {
|
|
2238
|
-
|
|
2341
|
+
import_fs8.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
|
|
2239
2342
|
}
|
|
2240
2343
|
return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
|
|
2241
2344
|
} catch {
|
|
@@ -2246,8 +2349,8 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
2246
2349
|
try {
|
|
2247
2350
|
let trust = { entries: [] };
|
|
2248
2351
|
try {
|
|
2249
|
-
if (
|
|
2250
|
-
trust = JSON.parse(
|
|
2352
|
+
if (import_fs8.default.existsSync(TRUST_FILE)) {
|
|
2353
|
+
trust = JSON.parse(import_fs8.default.readFileSync(TRUST_FILE, "utf-8"));
|
|
2251
2354
|
}
|
|
2252
2355
|
} catch {
|
|
2253
2356
|
}
|
|
@@ -2263,34 +2366,34 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
2263
2366
|
}
|
|
2264
2367
|
function getPersistentDecision(toolName) {
|
|
2265
2368
|
try {
|
|
2266
|
-
const file =
|
|
2267
|
-
if (!
|
|
2268
|
-
const decisions = JSON.parse(
|
|
2369
|
+
const file = import_path9.default.join(import_os7.default.homedir(), ".node9", "decisions.json");
|
|
2370
|
+
if (!import_fs8.default.existsSync(file)) return null;
|
|
2371
|
+
const decisions = JSON.parse(import_fs8.default.readFileSync(file, "utf-8"));
|
|
2269
2372
|
const d = decisions[toolName];
|
|
2270
2373
|
if (d === "allow" || d === "deny") return d;
|
|
2271
2374
|
} catch {
|
|
2272
2375
|
}
|
|
2273
2376
|
return null;
|
|
2274
2377
|
}
|
|
2275
|
-
var
|
|
2378
|
+
var import_fs8, import_path9, import_os7, PAUSED_FILE, TRUST_FILE;
|
|
2276
2379
|
var init_state = __esm({
|
|
2277
2380
|
"src/auth/state.ts"() {
|
|
2278
2381
|
"use strict";
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2382
|
+
import_fs8 = __toESM(require("fs"));
|
|
2383
|
+
import_path9 = __toESM(require("path"));
|
|
2384
|
+
import_os7 = __toESM(require("os"));
|
|
2282
2385
|
init_policy();
|
|
2283
|
-
PAUSED_FILE =
|
|
2284
|
-
TRUST_FILE =
|
|
2386
|
+
PAUSED_FILE = import_path9.default.join(import_os7.default.homedir(), ".node9", "PAUSED");
|
|
2387
|
+
TRUST_FILE = import_path9.default.join(import_os7.default.homedir(), ".node9", "trust.json");
|
|
2285
2388
|
}
|
|
2286
2389
|
});
|
|
2287
2390
|
|
|
2288
2391
|
// src/auth/daemon.ts
|
|
2289
2392
|
function getInternalToken() {
|
|
2290
2393
|
try {
|
|
2291
|
-
const pidFile =
|
|
2292
|
-
if (!
|
|
2293
|
-
const data = JSON.parse(
|
|
2394
|
+
const pidFile = import_path10.default.join(import_os8.default.homedir(), ".node9", "daemon.pid");
|
|
2395
|
+
if (!import_fs9.default.existsSync(pidFile)) return null;
|
|
2396
|
+
const data = JSON.parse(import_fs9.default.readFileSync(pidFile, "utf-8"));
|
|
2294
2397
|
process.kill(data.pid, 0);
|
|
2295
2398
|
return data.internalToken ?? null;
|
|
2296
2399
|
} catch {
|
|
@@ -2298,10 +2401,10 @@ function getInternalToken() {
|
|
|
2298
2401
|
}
|
|
2299
2402
|
}
|
|
2300
2403
|
function isDaemonRunning() {
|
|
2301
|
-
const pidFile =
|
|
2302
|
-
if (
|
|
2404
|
+
const pidFile = import_path10.default.join(import_os8.default.homedir(), ".node9", "daemon.pid");
|
|
2405
|
+
if (import_fs9.default.existsSync(pidFile)) {
|
|
2303
2406
|
try {
|
|
2304
|
-
const { pid, port } = JSON.parse(
|
|
2407
|
+
const { pid, port } = JSON.parse(import_fs9.default.readFileSync(pidFile, "utf-8"));
|
|
2305
2408
|
if (port !== DAEMON_PORT) return false;
|
|
2306
2409
|
process.kill(pid, 0);
|
|
2307
2410
|
return true;
|
|
@@ -2342,8 +2445,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
2342
2445
|
signal: ctrl.signal
|
|
2343
2446
|
});
|
|
2344
2447
|
if (!res.ok) throw new Error("Daemon fail");
|
|
2345
|
-
const { id } = await res.json();
|
|
2346
|
-
return id;
|
|
2448
|
+
const { id, allowCount } = await res.json();
|
|
2449
|
+
return { id, allowCount: allowCount ?? 1 };
|
|
2347
2450
|
} finally {
|
|
2348
2451
|
clearTimeout(timer);
|
|
2349
2452
|
}
|
|
@@ -2382,25 +2485,25 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
|
|
|
2382
2485
|
signal: AbortSignal.timeout(3e3)
|
|
2383
2486
|
});
|
|
2384
2487
|
if (!res.ok) throw new Error("Daemon unreachable");
|
|
2385
|
-
const { id } = await res.json();
|
|
2386
|
-
return id;
|
|
2488
|
+
const { id, allowCount } = await res.json();
|
|
2489
|
+
return { id, allowCount: allowCount ?? 1 };
|
|
2387
2490
|
}
|
|
2388
|
-
async function resolveViaDaemon(id, decision, internalToken) {
|
|
2491
|
+
async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
2389
2492
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2390
2493
|
await fetch(`${base}/resolve/${id}`, {
|
|
2391
2494
|
method: "POST",
|
|
2392
2495
|
headers: { "Content-Type": "application/json", "X-Node9-Internal": internalToken },
|
|
2393
|
-
body: JSON.stringify({ decision }),
|
|
2496
|
+
body: JSON.stringify({ decision, ...source && { source } }),
|
|
2394
2497
|
signal: AbortSignal.timeout(3e3)
|
|
2395
2498
|
});
|
|
2396
2499
|
}
|
|
2397
|
-
var
|
|
2500
|
+
var import_fs9, import_path10, import_os8, import_child_process, DAEMON_PORT, DAEMON_HOST;
|
|
2398
2501
|
var init_daemon = __esm({
|
|
2399
2502
|
"src/auth/daemon.ts"() {
|
|
2400
2503
|
"use strict";
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2504
|
+
import_fs9 = __toESM(require("fs"));
|
|
2505
|
+
import_path10 = __toESM(require("path"));
|
|
2506
|
+
import_os8 = __toESM(require("os"));
|
|
2404
2507
|
import_child_process = require("child_process");
|
|
2405
2508
|
DAEMON_PORT = 7391;
|
|
2406
2509
|
DAEMON_HOST = "127.0.0.1";
|
|
@@ -2459,7 +2562,7 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
|
|
|
2459
2562
|
intent = "EDIT";
|
|
2460
2563
|
if (obj.file_path) {
|
|
2461
2564
|
editFilePath = String(obj.file_path);
|
|
2462
|
-
editFileName =
|
|
2565
|
+
editFileName = import_path11.default.basename(editFilePath);
|
|
2463
2566
|
}
|
|
2464
2567
|
const result = extractContext(String(obj.new_string), matchedWord);
|
|
2465
2568
|
contextSnippet = result.snippet;
|
|
@@ -2491,11 +2594,11 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
|
|
|
2491
2594
|
...ruleName && { ruleName }
|
|
2492
2595
|
};
|
|
2493
2596
|
}
|
|
2494
|
-
var
|
|
2597
|
+
var import_path11, CODE_KEYS;
|
|
2495
2598
|
var init_context_sniper = __esm({
|
|
2496
2599
|
"src/context-sniper.ts"() {
|
|
2497
2600
|
"use strict";
|
|
2498
|
-
|
|
2601
|
+
import_path11 = __toESM(require("path"));
|
|
2499
2602
|
CODE_KEYS = [
|
|
2500
2603
|
"command",
|
|
2501
2604
|
"cmd",
|
|
@@ -2534,7 +2637,7 @@ function formatArgs(args, matchedField, matchedWord) {
|
|
|
2534
2637
|
if (typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2535
2638
|
const obj = parsed;
|
|
2536
2639
|
if (obj.old_string !== void 0 && obj.new_string !== void 0) {
|
|
2537
|
-
const file = obj.file_path ?
|
|
2640
|
+
const file = obj.file_path ? import_path12.default.basename(String(obj.file_path)) : "file";
|
|
2538
2641
|
const oldPreview = smartTruncate(String(obj.old_string), 120);
|
|
2539
2642
|
const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
|
|
2540
2643
|
return {
|
|
@@ -2597,20 +2700,24 @@ ${smartTruncate(str, 500)}`
|
|
|
2597
2700
|
function escapePango(text) {
|
|
2598
2701
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2599
2702
|
}
|
|
2600
|
-
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
2703
|
+
function buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
|
|
2601
2704
|
const lines = [];
|
|
2602
2705
|
if (locked) lines.push("\u26A0\uFE0F LOCKED BY ADMIN POLICY\n");
|
|
2603
2706
|
lines.push(`\u{1F916} ${agent || "AI Agent"} | \u{1F527} ${toolName}`);
|
|
2604
2707
|
lines.push(`\u{1F6E1}\uFE0F ${explainableLabel || "Security Policy"}`);
|
|
2605
2708
|
lines.push("");
|
|
2606
2709
|
lines.push(formattedArgs);
|
|
2710
|
+
if (allowCount >= 3) {
|
|
2711
|
+
lines.push("");
|
|
2712
|
+
lines.push(`\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule`);
|
|
2713
|
+
}
|
|
2607
2714
|
if (!locked) {
|
|
2608
2715
|
lines.push("");
|
|
2609
2716
|
lines.push('\u21B5 Enter = Allow \u21B5 | \u238B Esc = Block \u238B | "Always Allow" = never ask again');
|
|
2610
2717
|
}
|
|
2611
2718
|
return lines.join("\n");
|
|
2612
2719
|
}
|
|
2613
|
-
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked) {
|
|
2720
|
+
function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, locked, allowCount = 1) {
|
|
2614
2721
|
const lines = [];
|
|
2615
2722
|
if (locked) {
|
|
2616
2723
|
lines.push('<span foreground="red" weight="bold">\u26A0\uFE0F LOCKED BY ADMIN POLICY</span>');
|
|
@@ -2622,6 +2729,12 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
2622
2729
|
lines.push(`<i>\u{1F6E1}\uFE0F ${escapePango(explainableLabel || "Security Policy")}</i>`);
|
|
2623
2730
|
lines.push("");
|
|
2624
2731
|
lines.push(`<tt>${escapePango(formattedArgs)}</tt>`);
|
|
2732
|
+
if (allowCount >= 3) {
|
|
2733
|
+
lines.push("");
|
|
2734
|
+
lines.push(
|
|
2735
|
+
`<span foreground="#f0c040">\u{1F4A1} Approved ${allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</span>`
|
|
2736
|
+
);
|
|
2737
|
+
}
|
|
2625
2738
|
if (!locked) {
|
|
2626
2739
|
lines.push("");
|
|
2627
2740
|
lines.push(
|
|
@@ -2630,12 +2743,19 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
|
|
|
2630
2743
|
}
|
|
2631
2744
|
return lines.join("\n");
|
|
2632
2745
|
}
|
|
2633
|
-
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord) {
|
|
2746
|
+
async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord, allowCount = 1) {
|
|
2634
2747
|
if (isTestEnv()) return "deny";
|
|
2635
2748
|
const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
|
|
2636
2749
|
const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
|
|
2637
2750
|
const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
|
|
2638
|
-
const message = buildPlainMessage(
|
|
2751
|
+
const message = buildPlainMessage(
|
|
2752
|
+
toolName,
|
|
2753
|
+
formattedArgs,
|
|
2754
|
+
agent,
|
|
2755
|
+
explainableLabel,
|
|
2756
|
+
locked,
|
|
2757
|
+
allowCount
|
|
2758
|
+
);
|
|
2639
2759
|
return new Promise((resolve) => {
|
|
2640
2760
|
let childProcess = null;
|
|
2641
2761
|
const onAbort = () => {
|
|
@@ -2667,7 +2787,8 @@ end run`;
|
|
|
2667
2787
|
formattedArgs,
|
|
2668
2788
|
agent,
|
|
2669
2789
|
explainableLabel,
|
|
2670
|
-
locked
|
|
2790
|
+
locked,
|
|
2791
|
+
allowCount
|
|
2671
2792
|
);
|
|
2672
2793
|
const argsList = [
|
|
2673
2794
|
locked ? "--info" : "--question",
|
|
@@ -2707,12 +2828,12 @@ end run`;
|
|
|
2707
2828
|
}
|
|
2708
2829
|
});
|
|
2709
2830
|
}
|
|
2710
|
-
var import_child_process2,
|
|
2831
|
+
var import_child_process2, import_path12, isTestEnv;
|
|
2711
2832
|
var init_native = __esm({
|
|
2712
2833
|
"src/ui/native.ts"() {
|
|
2713
2834
|
"use strict";
|
|
2714
2835
|
import_child_process2 = require("child_process");
|
|
2715
|
-
|
|
2836
|
+
import_path12 = __toESM(require("path"));
|
|
2716
2837
|
init_context_sniper();
|
|
2717
2838
|
isTestEnv = () => {
|
|
2718
2839
|
return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
|
|
@@ -2732,9 +2853,9 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
2732
2853
|
context: {
|
|
2733
2854
|
agent: meta?.agent,
|
|
2734
2855
|
mcpServer: meta?.mcpServer,
|
|
2735
|
-
hostname:
|
|
2856
|
+
hostname: import_os9.default.hostname(),
|
|
2736
2857
|
cwd: process.cwd(),
|
|
2737
|
-
platform:
|
|
2858
|
+
platform: import_os9.default.platform()
|
|
2738
2859
|
}
|
|
2739
2860
|
}),
|
|
2740
2861
|
signal: AbortSignal.timeout(5e3)
|
|
@@ -2755,9 +2876,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
|
2755
2876
|
context: {
|
|
2756
2877
|
agent: meta?.agent,
|
|
2757
2878
|
mcpServer: meta?.mcpServer,
|
|
2758
|
-
hostname:
|
|
2879
|
+
hostname: import_os9.default.hostname(),
|
|
2759
2880
|
cwd: process.cwd(),
|
|
2760
|
-
platform:
|
|
2881
|
+
platform: import_os9.default.platform()
|
|
2761
2882
|
},
|
|
2762
2883
|
...riskMetadata && { riskMetadata }
|
|
2763
2884
|
}),
|
|
@@ -2813,26 +2934,26 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
2813
2934
|
});
|
|
2814
2935
|
clearTimeout(timer);
|
|
2815
2936
|
if (!res.ok) {
|
|
2816
|
-
|
|
2937
|
+
import_fs10.default.appendFileSync(
|
|
2817
2938
|
HOOK_DEBUG_LOG,
|
|
2818
2939
|
`[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
|
|
2819
2940
|
`
|
|
2820
2941
|
);
|
|
2821
2942
|
}
|
|
2822
2943
|
} catch (err) {
|
|
2823
|
-
|
|
2944
|
+
import_fs10.default.appendFileSync(
|
|
2824
2945
|
HOOK_DEBUG_LOG,
|
|
2825
2946
|
`[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
|
|
2826
2947
|
`
|
|
2827
2948
|
);
|
|
2828
2949
|
}
|
|
2829
2950
|
}
|
|
2830
|
-
var
|
|
2951
|
+
var import_fs10, import_os9;
|
|
2831
2952
|
var init_cloud = __esm({
|
|
2832
2953
|
"src/auth/cloud.ts"() {
|
|
2833
2954
|
"use strict";
|
|
2834
|
-
|
|
2835
|
-
|
|
2955
|
+
import_fs10 = __toESM(require("fs"));
|
|
2956
|
+
import_os9 = __toESM(require("os"));
|
|
2836
2957
|
init_audit();
|
|
2837
2958
|
}
|
|
2838
2959
|
});
|
|
@@ -3030,13 +3151,16 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3030
3151
|
let viewerId = null;
|
|
3031
3152
|
const internalToken = getInternalToken();
|
|
3032
3153
|
let daemonEntryId = null;
|
|
3154
|
+
let daemonAllowCount = 1;
|
|
3033
3155
|
if ((approvers.browser || approvers.terminal) && isDaemonRunning() && !options?.calledFromDaemon) {
|
|
3034
3156
|
if (cloudEnforced && cloudRequestId) {
|
|
3035
|
-
|
|
3157
|
+
const viewer = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
|
|
3158
|
+
viewerId = viewer?.id ?? null;
|
|
3036
3159
|
daemonEntryId = viewerId;
|
|
3160
|
+
if (viewer) daemonAllowCount = viewer.allowCount;
|
|
3037
3161
|
} else {
|
|
3038
3162
|
try {
|
|
3039
|
-
|
|
3163
|
+
const entry = await registerDaemonEntry(
|
|
3040
3164
|
toolName,
|
|
3041
3165
|
args,
|
|
3042
3166
|
meta,
|
|
@@ -3044,6 +3168,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3044
3168
|
options?.activityId,
|
|
3045
3169
|
options?.cwd
|
|
3046
3170
|
);
|
|
3171
|
+
daemonEntryId = entry.id;
|
|
3172
|
+
daemonAllowCount = entry.allowCount;
|
|
3047
3173
|
} catch {
|
|
3048
3174
|
}
|
|
3049
3175
|
}
|
|
@@ -3079,7 +3205,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3079
3205
|
false,
|
|
3080
3206
|
signal,
|
|
3081
3207
|
policyMatchedField,
|
|
3082
|
-
policyMatchedWord
|
|
3208
|
+
policyMatchedWord,
|
|
3209
|
+
daemonAllowCount
|
|
3083
3210
|
);
|
|
3084
3211
|
if (decision === "always_allow") {
|
|
3085
3212
|
writeTrustSession(toolName, 36e5);
|
|
@@ -3137,10 +3264,13 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
3137
3264
|
if (!resolved) {
|
|
3138
3265
|
resolved = true;
|
|
3139
3266
|
abortController.abort();
|
|
3140
|
-
if (
|
|
3141
|
-
resolveViaDaemon(
|
|
3142
|
-
|
|
3143
|
-
|
|
3267
|
+
if (daemonEntryId && internalToken) {
|
|
3268
|
+
resolveViaDaemon(
|
|
3269
|
+
daemonEntryId,
|
|
3270
|
+
res.approved ? "allow" : "deny",
|
|
3271
|
+
internalToken,
|
|
3272
|
+
res.decisionSource
|
|
3273
|
+
).catch(() => null);
|
|
3144
3274
|
}
|
|
3145
3275
|
resolve(res);
|
|
3146
3276
|
}
|
|
@@ -3184,13 +3314,13 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
3184
3314
|
}
|
|
3185
3315
|
return finalResult;
|
|
3186
3316
|
}
|
|
3187
|
-
var import_net,
|
|
3317
|
+
var import_net, import_path13, import_os10, import_crypto2, ACTIVITY_SOCKET_PATH;
|
|
3188
3318
|
var init_orchestrator = __esm({
|
|
3189
3319
|
"src/auth/orchestrator.ts"() {
|
|
3190
3320
|
"use strict";
|
|
3191
3321
|
import_net = __toESM(require("net"));
|
|
3192
|
-
|
|
3193
|
-
|
|
3322
|
+
import_path13 = __toESM(require("path"));
|
|
3323
|
+
import_os10 = __toESM(require("os"));
|
|
3194
3324
|
import_crypto2 = require("crypto");
|
|
3195
3325
|
init_native();
|
|
3196
3326
|
init_context_sniper();
|
|
@@ -3201,7 +3331,7 @@ var init_orchestrator = __esm({
|
|
|
3201
3331
|
init_state();
|
|
3202
3332
|
init_daemon();
|
|
3203
3333
|
init_cloud();
|
|
3204
|
-
ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
3334
|
+
ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path13.default.join(import_os10.default.tmpdir(), "node9-activity.sock");
|
|
3205
3335
|
}
|
|
3206
3336
|
});
|
|
3207
3337
|
|
|
@@ -3497,6 +3627,15 @@ var init_ui = __esm({
|
|
|
3497
3627
|
padding: 5px 10px;
|
|
3498
3628
|
margin-bottom: 14px;
|
|
3499
3629
|
}
|
|
3630
|
+
.insight-hint {
|
|
3631
|
+
font-size: 12px;
|
|
3632
|
+
color: #f0c040;
|
|
3633
|
+
background: rgba(240, 192, 64, 0.08);
|
|
3634
|
+
border: 1px solid rgba(240, 192, 64, 0.25);
|
|
3635
|
+
border-radius: 6px;
|
|
3636
|
+
padding: 6px 10px;
|
|
3637
|
+
margin-bottom: 12px;
|
|
3638
|
+
}
|
|
3500
3639
|
pre {
|
|
3501
3640
|
background: #0d1117;
|
|
3502
3641
|
padding: 14px 16px;
|
|
@@ -3969,6 +4108,78 @@ var init_ui = __esm({
|
|
|
3969
4108
|
color: var(--danger);
|
|
3970
4109
|
}
|
|
3971
4110
|
|
|
4111
|
+
/* \u2500\u2500 Suggestion cards \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
4112
|
+
.suggestion-card {
|
|
4113
|
+
background: rgba(82, 130, 255, 0.06);
|
|
4114
|
+
border: 1px solid rgba(82, 130, 255, 0.25);
|
|
4115
|
+
border-radius: 8px;
|
|
4116
|
+
padding: 10px 12px;
|
|
4117
|
+
margin-bottom: 8px;
|
|
4118
|
+
}
|
|
4119
|
+
.suggestion-card:last-child {
|
|
4120
|
+
margin-bottom: 0;
|
|
4121
|
+
}
|
|
4122
|
+
.suggestion-header {
|
|
4123
|
+
display: flex;
|
|
4124
|
+
align-items: center;
|
|
4125
|
+
gap: 8px;
|
|
4126
|
+
margin-bottom: 6px;
|
|
4127
|
+
}
|
|
4128
|
+
.suggestion-tool {
|
|
4129
|
+
font-family: 'Fira Code', monospace;
|
|
4130
|
+
font-size: 11px;
|
|
4131
|
+
color: var(--text-bright);
|
|
4132
|
+
flex: 1;
|
|
4133
|
+
word-break: break-all;
|
|
4134
|
+
}
|
|
4135
|
+
.suggestion-count {
|
|
4136
|
+
font-size: 10px;
|
|
4137
|
+
color: var(--muted);
|
|
4138
|
+
white-space: nowrap;
|
|
4139
|
+
}
|
|
4140
|
+
.suggestion-rule {
|
|
4141
|
+
font-family: 'Fira Code', monospace;
|
|
4142
|
+
font-size: 10px;
|
|
4143
|
+
color: #79c0ff;
|
|
4144
|
+
background: rgba(0, 0, 0, 0.25);
|
|
4145
|
+
border-radius: 4px;
|
|
4146
|
+
padding: 4px 8px;
|
|
4147
|
+
margin-bottom: 8px;
|
|
4148
|
+
word-break: break-all;
|
|
4149
|
+
white-space: pre-wrap;
|
|
4150
|
+
}
|
|
4151
|
+
.suggestion-actions {
|
|
4152
|
+
display: flex;
|
|
4153
|
+
gap: 6px;
|
|
4154
|
+
}
|
|
4155
|
+
.btn-apply {
|
|
4156
|
+
background: rgba(52, 125, 57, 0.2);
|
|
4157
|
+
border: 1px solid rgba(87, 171, 90, 0.4);
|
|
4158
|
+
color: #57ab5a;
|
|
4159
|
+
padding: 4px 10px;
|
|
4160
|
+
font-size: 11px;
|
|
4161
|
+
border-radius: 5px;
|
|
4162
|
+
font-family: inherit;
|
|
4163
|
+
cursor: pointer;
|
|
4164
|
+
}
|
|
4165
|
+
.btn-apply:hover {
|
|
4166
|
+
background: rgba(52, 125, 57, 0.35);
|
|
4167
|
+
}
|
|
4168
|
+
.btn-dismiss-suggestion {
|
|
4169
|
+
background: transparent;
|
|
4170
|
+
border: 1px solid var(--border);
|
|
4171
|
+
color: var(--muted);
|
|
4172
|
+
padding: 4px 10px;
|
|
4173
|
+
font-size: 11px;
|
|
4174
|
+
border-radius: 5px;
|
|
4175
|
+
font-family: inherit;
|
|
4176
|
+
cursor: pointer;
|
|
4177
|
+
}
|
|
4178
|
+
.btn-dismiss-suggestion:hover {
|
|
4179
|
+
border-color: var(--danger);
|
|
4180
|
+
color: var(--danger);
|
|
4181
|
+
}
|
|
4182
|
+
|
|
3972
4183
|
.modal-overlay {
|
|
3973
4184
|
display: none;
|
|
3974
4185
|
position: fixed;
|
|
@@ -4150,6 +4361,11 @@ var init_ui = __esm({
|
|
|
4150
4361
|
<div class="panel-title">\u{1F4CB} Persistent Decisions</div>
|
|
4151
4362
|
<div id="decisionsList"><span class="decisions-empty">None yet.</span></div>
|
|
4152
4363
|
</div>
|
|
4364
|
+
|
|
4365
|
+
<div class="panel" id="suggestionsPanel" style="display: none">
|
|
4366
|
+
<div class="panel-title">\u{1F4A1} Smart Rule Suggestions</div>
|
|
4367
|
+
<div id="suggestionsList"></div>
|
|
4368
|
+
</div>
|
|
4153
4369
|
</div>
|
|
4154
4370
|
</div>
|
|
4155
4371
|
</div>
|
|
@@ -4339,6 +4555,7 @@ var init_ui = __esm({
|
|
|
4339
4555
|
</div>
|
|
4340
4556
|
<div class="tool-chip">\${esc(req.toolName)}</div>
|
|
4341
4557
|
\${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Cloud approval \u2014 view only</div>' : ''}
|
|
4558
|
+
\${req.allowCount >= 3 ? \`<div class="insight-hint">\u{1F4A1} Approved \${req.allowCount - 1}\xD7 before \u2014 "Always Allow" creates a permanent rule</div>\` : ''}
|
|
4342
4559
|
\${renderPayload(req)}
|
|
4343
4560
|
<div class="actions" id="act-\${req.id}">
|
|
4344
4561
|
<button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>\u2705 Allow this Action</button>
|
|
@@ -4405,6 +4622,14 @@ var init_ui = __esm({
|
|
|
4405
4622
|
ev.addEventListener('shields-status', (e) => {
|
|
4406
4623
|
renderShields(JSON.parse(e.data).shields);
|
|
4407
4624
|
});
|
|
4625
|
+
ev.addEventListener('suggestion:new', (e) => {
|
|
4626
|
+
const s = JSON.parse(e.data);
|
|
4627
|
+
addSuggestionCard(s);
|
|
4628
|
+
});
|
|
4629
|
+
ev.addEventListener('suggestion:resolved', (e) => {
|
|
4630
|
+
const { id } = JSON.parse(e.data);
|
|
4631
|
+
removeSuggestionCard(id);
|
|
4632
|
+
});
|
|
4408
4633
|
|
|
4409
4634
|
// \u2500\u2500 Flight Recorder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4410
4635
|
ev.addEventListener('activity', (e) => {
|
|
@@ -4654,6 +4879,74 @@ var init_ui = __esm({
|
|
|
4654
4879
|
.then((r) => r.json())
|
|
4655
4880
|
.then(renderDecisions)
|
|
4656
4881
|
.catch(() => {});
|
|
4882
|
+
|
|
4883
|
+
// \u2500\u2500 Smart Rule Suggestions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4884
|
+
function rulePreview(suggestion) {
|
|
4885
|
+
const r = suggestion.suggestedRule;
|
|
4886
|
+
if (r.type === 'ignoredTool') return \`ignoredTool: "\${r.toolName}"\`;
|
|
4887
|
+
const cond = r.rule.conditions?.[0];
|
|
4888
|
+
const condStr = cond ? \` where \${cond.field} \${cond.op} "\${cond.value}"\` : '';
|
|
4889
|
+
return \`allow \${r.rule.tool}\${condStr}\`;
|
|
4890
|
+
}
|
|
4891
|
+
|
|
4892
|
+
function addSuggestionCard(s) {
|
|
4893
|
+
const panel = document.getElementById('suggestionsPanel');
|
|
4894
|
+
const list = document.getElementById('suggestionsList');
|
|
4895
|
+
panel.style.display = '';
|
|
4896
|
+
|
|
4897
|
+
const card = document.createElement('div');
|
|
4898
|
+
card.className = 'suggestion-card';
|
|
4899
|
+
card.id = 'sg-' + s.id;
|
|
4900
|
+
card.innerHTML = \`
|
|
4901
|
+
<div class="suggestion-header">
|
|
4902
|
+
<span class="suggestion-tool">\${esc(s.toolName)}</span>
|
|
4903
|
+
<span class="suggestion-count">allowed \${s.allowCount}\xD7</span>
|
|
4904
|
+
</div>
|
|
4905
|
+
<div class="suggestion-rule">\${esc(rulePreview(s))}</div>
|
|
4906
|
+
<div class="suggestion-actions">
|
|
4907
|
+
<button class="btn-apply" onclick="applySuggestion('\${esc(s.id)}')">Apply rule</button>
|
|
4908
|
+
<button class="btn-dismiss-suggestion" onclick="dismissSuggestion('\${esc(s.id)}')">Dismiss</button>
|
|
4909
|
+
</div>
|
|
4910
|
+
\`;
|
|
4911
|
+
list.appendChild(card);
|
|
4912
|
+
}
|
|
4913
|
+
|
|
4914
|
+
function removeSuggestionCard(id) {
|
|
4915
|
+
document.getElementById('sg-' + id)?.remove();
|
|
4916
|
+
const list = document.getElementById('suggestionsList');
|
|
4917
|
+
if (!list.querySelector('.suggestion-card')) {
|
|
4918
|
+
document.getElementById('suggestionsPanel').style.display = 'none';
|
|
4919
|
+
}
|
|
4920
|
+
}
|
|
4921
|
+
|
|
4922
|
+
function applySuggestion(id) {
|
|
4923
|
+
fetch('/suggestions/' + id + '/apply', {
|
|
4924
|
+
method: 'POST',
|
|
4925
|
+
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
4926
|
+
body: JSON.stringify({}),
|
|
4927
|
+
})
|
|
4928
|
+
.then((r) => {
|
|
4929
|
+
if (r.ok) removeSuggestionCard(id);
|
|
4930
|
+
})
|
|
4931
|
+
.catch(() => {});
|
|
4932
|
+
}
|
|
4933
|
+
|
|
4934
|
+
function dismissSuggestion(id) {
|
|
4935
|
+
fetch('/suggestions/' + id + '/dismiss', {
|
|
4936
|
+
method: 'POST',
|
|
4937
|
+
headers: { 'X-Node9-Token': CSRF_TOKEN },
|
|
4938
|
+
})
|
|
4939
|
+
.then((r) => {
|
|
4940
|
+
if (r.ok) removeSuggestionCard(id);
|
|
4941
|
+
})
|
|
4942
|
+
.catch(() => {});
|
|
4943
|
+
}
|
|
4944
|
+
|
|
4945
|
+
// Load any suggestions that survived a page reload (daemon still running)
|
|
4946
|
+
fetch('/suggestions')
|
|
4947
|
+
.then((r) => r.json())
|
|
4948
|
+
.then((list) => list.filter((s) => s.status === 'pending').forEach(addSuggestionCard))
|
|
4949
|
+
.catch(() => {});
|
|
4657
4950
|
</script>
|
|
4658
4951
|
</body>
|
|
4659
4952
|
</html>
|
|
@@ -4671,7 +4964,117 @@ var init_ui2 = __esm({
|
|
|
4671
4964
|
}
|
|
4672
4965
|
});
|
|
4673
4966
|
|
|
4967
|
+
// src/daemon/suggestion-tracker.ts
|
|
4968
|
+
function extractPath(args) {
|
|
4969
|
+
if (!args || typeof args !== "object") return null;
|
|
4970
|
+
const a = args;
|
|
4971
|
+
for (const key of ["path", "file_path", "filename", "filepath", "dest", "destination"]) {
|
|
4972
|
+
if (typeof a[key] === "string" && a[key]) return a[key];
|
|
4973
|
+
}
|
|
4974
|
+
return null;
|
|
4975
|
+
}
|
|
4976
|
+
function commonPathPrefix(paths) {
|
|
4977
|
+
if (paths.length < 2) return null;
|
|
4978
|
+
const dirParts = paths.map((p) => {
|
|
4979
|
+
const lastSlash = p.lastIndexOf("/");
|
|
4980
|
+
return lastSlash > 0 ? p.slice(0, lastSlash + 1) : "/";
|
|
4981
|
+
});
|
|
4982
|
+
const first = dirParts[0].split("/");
|
|
4983
|
+
const common = [];
|
|
4984
|
+
for (let i = 0; i < first.length; i++) {
|
|
4985
|
+
if (dirParts.every((d) => d.split("/")[i] === first[i])) {
|
|
4986
|
+
common.push(first[i]);
|
|
4987
|
+
} else {
|
|
4988
|
+
break;
|
|
4989
|
+
}
|
|
4990
|
+
}
|
|
4991
|
+
const prefix = common.join("/").replace(/\/?$/, "/");
|
|
4992
|
+
return prefix.length > 1 ? prefix : null;
|
|
4993
|
+
}
|
|
4994
|
+
var import_crypto3, SuggestionTracker;
|
|
4995
|
+
var init_suggestion_tracker = __esm({
|
|
4996
|
+
"src/daemon/suggestion-tracker.ts"() {
|
|
4997
|
+
"use strict";
|
|
4998
|
+
import_crypto3 = require("crypto");
|
|
4999
|
+
SuggestionTracker = class {
|
|
5000
|
+
events = /* @__PURE__ */ new Map();
|
|
5001
|
+
threshold;
|
|
5002
|
+
constructor(threshold = 3) {
|
|
5003
|
+
this.threshold = threshold;
|
|
5004
|
+
}
|
|
5005
|
+
/**
|
|
5006
|
+
* Record a human-allowed review for a tool.
|
|
5007
|
+
* Returns a Suggestion when the threshold is reached, null otherwise.
|
|
5008
|
+
*/
|
|
5009
|
+
recordAllow(toolName, args) {
|
|
5010
|
+
const events = this.events.get(toolName) ?? [];
|
|
5011
|
+
events.push({ args, ts: Date.now() });
|
|
5012
|
+
this.events.set(toolName, events);
|
|
5013
|
+
if (events.length >= this.threshold) {
|
|
5014
|
+
this.events.delete(toolName);
|
|
5015
|
+
return this.generateSuggestion(toolName, events);
|
|
5016
|
+
}
|
|
5017
|
+
return null;
|
|
5018
|
+
}
|
|
5019
|
+
/**
|
|
5020
|
+
* Reset the counter for a tool (e.g. when the user clicks Deny —
|
|
5021
|
+
* don't suggest allowing something they just blocked).
|
|
5022
|
+
*/
|
|
5023
|
+
resetTool(toolName) {
|
|
5024
|
+
this.events.delete(toolName);
|
|
5025
|
+
}
|
|
5026
|
+
/** Current allow count for a tool (for tests). */
|
|
5027
|
+
getCount(toolName) {
|
|
5028
|
+
return this.events.get(toolName)?.length ?? 0;
|
|
5029
|
+
}
|
|
5030
|
+
generateSuggestion(toolName, events) {
|
|
5031
|
+
const paths = events.map((e) => extractPath(e.args)).filter((p) => typeof p === "string" && p.length > 0);
|
|
5032
|
+
const prefix = commonPathPrefix(paths);
|
|
5033
|
+
const suggestedRule = prefix ? {
|
|
5034
|
+
type: "smartRule",
|
|
5035
|
+
rule: {
|
|
5036
|
+
name: `allow-${toolName}-${prefix.replace(/[^a-z0-9]/gi, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}`,
|
|
5037
|
+
tool: toolName,
|
|
5038
|
+
conditions: [{ field: "path", op: "matchesGlob", value: `${prefix}**` }],
|
|
5039
|
+
verdict: "allow",
|
|
5040
|
+
reason: `Auto-suggested: ${toolName} allowed ${events.length}\xD7 in ${prefix}`
|
|
5041
|
+
}
|
|
5042
|
+
} : { type: "ignoredTool", toolName };
|
|
5043
|
+
return {
|
|
5044
|
+
id: (0, import_crypto3.randomUUID)(),
|
|
5045
|
+
toolName,
|
|
5046
|
+
allowCount: events.length,
|
|
5047
|
+
suggestedRule,
|
|
5048
|
+
status: "pending",
|
|
5049
|
+
createdAt: Date.now(),
|
|
5050
|
+
exampleArgs: events.slice(0, 3).map((e) => e.args)
|
|
5051
|
+
};
|
|
5052
|
+
}
|
|
5053
|
+
};
|
|
5054
|
+
}
|
|
5055
|
+
});
|
|
5056
|
+
|
|
4674
5057
|
// src/daemon/state.ts
|
|
5058
|
+
function loadInsightCounts() {
|
|
5059
|
+
try {
|
|
5060
|
+
if (!import_fs12.default.existsSync(INSIGHT_COUNTS_FILE)) return;
|
|
5061
|
+
const data = JSON.parse(import_fs12.default.readFileSync(INSIGHT_COUNTS_FILE, "utf-8"));
|
|
5062
|
+
for (const [tool, count] of Object.entries(data)) {
|
|
5063
|
+
if (typeof count === "number" && count > 0) insightCounts.set(tool, count);
|
|
5064
|
+
}
|
|
5065
|
+
} catch {
|
|
5066
|
+
}
|
|
5067
|
+
}
|
|
5068
|
+
function saveInsightCounts() {
|
|
5069
|
+
try {
|
|
5070
|
+
const data = {};
|
|
5071
|
+
insightCounts.forEach((count, tool) => {
|
|
5072
|
+
data[tool] = count;
|
|
5073
|
+
});
|
|
5074
|
+
atomicWriteSync2(INSIGHT_COUNTS_FILE, JSON.stringify(data, null, 2), { mode: 384 });
|
|
5075
|
+
} catch {
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
4675
5078
|
function getAbandonTimer() {
|
|
4676
5079
|
return _abandonTimer;
|
|
4677
5080
|
}
|
|
@@ -4694,11 +5097,27 @@ function markRejectionHandlerRegistered() {
|
|
|
4694
5097
|
daemonRejectionHandlerRegistered = true;
|
|
4695
5098
|
}
|
|
4696
5099
|
function atomicWriteSync2(filePath, data, options) {
|
|
4697
|
-
const dir =
|
|
4698
|
-
if (!
|
|
4699
|
-
const tmpPath = `${filePath}.${(0,
|
|
4700
|
-
|
|
4701
|
-
|
|
5100
|
+
const dir = import_path15.default.dirname(filePath);
|
|
5101
|
+
if (!import_fs12.default.existsSync(dir)) import_fs12.default.mkdirSync(dir, { recursive: true });
|
|
5102
|
+
const tmpPath = `${filePath}.${(0, import_crypto4.randomUUID)()}.tmp`;
|
|
5103
|
+
try {
|
|
5104
|
+
import_fs12.default.writeFileSync(tmpPath, data, options);
|
|
5105
|
+
} catch (err) {
|
|
5106
|
+
try {
|
|
5107
|
+
import_fs12.default.unlinkSync(tmpPath);
|
|
5108
|
+
} catch {
|
|
5109
|
+
}
|
|
5110
|
+
throw err;
|
|
5111
|
+
}
|
|
5112
|
+
try {
|
|
5113
|
+
import_fs12.default.renameSync(tmpPath, filePath);
|
|
5114
|
+
} catch (err) {
|
|
5115
|
+
try {
|
|
5116
|
+
import_fs12.default.unlinkSync(tmpPath);
|
|
5117
|
+
} catch {
|
|
5118
|
+
}
|
|
5119
|
+
throw err;
|
|
5120
|
+
}
|
|
4702
5121
|
}
|
|
4703
5122
|
function redactArgs(value) {
|
|
4704
5123
|
if (!value || typeof value !== "object") return value;
|
|
@@ -4718,16 +5137,16 @@ function appendAuditLog(data) {
|
|
|
4718
5137
|
decision: data.decision,
|
|
4719
5138
|
source: "daemon"
|
|
4720
5139
|
};
|
|
4721
|
-
const dir =
|
|
4722
|
-
if (!
|
|
4723
|
-
|
|
5140
|
+
const dir = import_path15.default.dirname(AUDIT_LOG_FILE);
|
|
5141
|
+
if (!import_fs12.default.existsSync(dir)) import_fs12.default.mkdirSync(dir, { recursive: true });
|
|
5142
|
+
import_fs12.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
|
|
4724
5143
|
} catch {
|
|
4725
5144
|
}
|
|
4726
5145
|
}
|
|
4727
5146
|
function getAuditHistory(limit = 20) {
|
|
4728
5147
|
try {
|
|
4729
|
-
if (!
|
|
4730
|
-
const lines =
|
|
5148
|
+
if (!import_fs12.default.existsSync(AUDIT_LOG_FILE)) return [];
|
|
5149
|
+
const lines = import_fs12.default.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
|
|
4731
5150
|
if (lines.length === 1 && lines[0] === "") return [];
|
|
4732
5151
|
return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
|
|
4733
5152
|
} catch {
|
|
@@ -4736,19 +5155,19 @@ function getAuditHistory(limit = 20) {
|
|
|
4736
5155
|
}
|
|
4737
5156
|
function getOrgName() {
|
|
4738
5157
|
try {
|
|
4739
|
-
if (
|
|
5158
|
+
if (import_fs12.default.existsSync(CREDENTIALS_FILE)) return "Node9 Cloud";
|
|
4740
5159
|
} catch {
|
|
4741
5160
|
}
|
|
4742
5161
|
return null;
|
|
4743
5162
|
}
|
|
4744
5163
|
function hasStoredSlackKey() {
|
|
4745
|
-
return
|
|
5164
|
+
return import_fs12.default.existsSync(CREDENTIALS_FILE);
|
|
4746
5165
|
}
|
|
4747
5166
|
function writeGlobalSetting(key, value) {
|
|
4748
5167
|
let config = {};
|
|
4749
5168
|
try {
|
|
4750
|
-
if (
|
|
4751
|
-
config = JSON.parse(
|
|
5169
|
+
if (import_fs12.default.existsSync(GLOBAL_CONFIG_FILE)) {
|
|
5170
|
+
config = JSON.parse(import_fs12.default.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
|
|
4752
5171
|
}
|
|
4753
5172
|
} catch {
|
|
4754
5173
|
}
|
|
@@ -4760,8 +5179,8 @@ function writeTrustEntry(toolName, durationMs) {
|
|
|
4760
5179
|
try {
|
|
4761
5180
|
let trust = { entries: [] };
|
|
4762
5181
|
try {
|
|
4763
|
-
if (
|
|
4764
|
-
trust = JSON.parse(
|
|
5182
|
+
if (import_fs12.default.existsSync(TRUST_FILE2))
|
|
5183
|
+
trust = JSON.parse(import_fs12.default.readFileSync(TRUST_FILE2, "utf-8"));
|
|
4765
5184
|
} catch {
|
|
4766
5185
|
}
|
|
4767
5186
|
trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
|
|
@@ -4772,8 +5191,8 @@ function writeTrustEntry(toolName, durationMs) {
|
|
|
4772
5191
|
}
|
|
4773
5192
|
function readPersistentDecisions() {
|
|
4774
5193
|
try {
|
|
4775
|
-
if (
|
|
4776
|
-
return JSON.parse(
|
|
5194
|
+
if (import_fs12.default.existsSync(DECISIONS_FILE)) {
|
|
5195
|
+
return JSON.parse(import_fs12.default.readFileSync(DECISIONS_FILE, "utf-8"));
|
|
4777
5196
|
}
|
|
4778
5197
|
} catch {
|
|
4779
5198
|
}
|
|
@@ -4838,7 +5257,7 @@ function abandonPending() {
|
|
|
4838
5257
|
});
|
|
4839
5258
|
if (autoStarted) {
|
|
4840
5259
|
try {
|
|
4841
|
-
|
|
5260
|
+
import_fs12.default.unlinkSync(DAEMON_PID_FILE);
|
|
4842
5261
|
} catch {
|
|
4843
5262
|
}
|
|
4844
5263
|
setTimeout(() => {
|
|
@@ -4849,7 +5268,7 @@ function abandonPending() {
|
|
|
4849
5268
|
}
|
|
4850
5269
|
function startActivitySocket() {
|
|
4851
5270
|
try {
|
|
4852
|
-
|
|
5271
|
+
import_fs12.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
4853
5272
|
} catch {
|
|
4854
5273
|
}
|
|
4855
5274
|
const ACTIVITY_MAX_BYTES = 1024 * 1024;
|
|
@@ -4891,31 +5310,36 @@ function startActivitySocket() {
|
|
|
4891
5310
|
unixServer.listen(ACTIVITY_SOCKET_PATH2);
|
|
4892
5311
|
process.on("exit", () => {
|
|
4893
5312
|
try {
|
|
4894
|
-
|
|
5313
|
+
import_fs12.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
4895
5314
|
} catch {
|
|
4896
5315
|
}
|
|
4897
5316
|
});
|
|
4898
5317
|
}
|
|
4899
|
-
var import_net2,
|
|
5318
|
+
var import_net2, import_fs12, import_path15, import_os12, import_child_process3, import_crypto4, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE;
|
|
4900
5319
|
var init_state2 = __esm({
|
|
4901
5320
|
"src/daemon/state.ts"() {
|
|
4902
5321
|
"use strict";
|
|
4903
5322
|
import_net2 = __toESM(require("net"));
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
5323
|
+
import_fs12 = __toESM(require("fs"));
|
|
5324
|
+
import_path15 = __toESM(require("path"));
|
|
5325
|
+
import_os12 = __toESM(require("os"));
|
|
4907
5326
|
import_child_process3 = require("child_process");
|
|
4908
|
-
|
|
5327
|
+
import_crypto4 = require("crypto");
|
|
4909
5328
|
init_daemon();
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
5329
|
+
init_suggestion_tracker();
|
|
5330
|
+
homeDir = import_os12.default.homedir();
|
|
5331
|
+
DAEMON_PID_FILE = import_path15.default.join(homeDir, ".node9", "daemon.pid");
|
|
5332
|
+
DECISIONS_FILE = import_path15.default.join(homeDir, ".node9", "decisions.json");
|
|
5333
|
+
AUDIT_LOG_FILE = import_path15.default.join(homeDir, ".node9", "audit.log");
|
|
5334
|
+
TRUST_FILE2 = import_path15.default.join(homeDir, ".node9", "trust.json");
|
|
5335
|
+
GLOBAL_CONFIG_FILE = import_path15.default.join(homeDir, ".node9", "config.json");
|
|
5336
|
+
CREDENTIALS_FILE = import_path15.default.join(homeDir, ".node9", "credentials.json");
|
|
5337
|
+
INSIGHT_COUNTS_FILE = import_path15.default.join(homeDir, ".node9", "insight-counts.json");
|
|
4917
5338
|
pending = /* @__PURE__ */ new Map();
|
|
4918
5339
|
sseClients = /* @__PURE__ */ new Set();
|
|
5340
|
+
suggestionTracker = new SuggestionTracker(3);
|
|
5341
|
+
suggestions = /* @__PURE__ */ new Map();
|
|
5342
|
+
insightCounts = /* @__PURE__ */ new Map();
|
|
4919
5343
|
_abandonTimer = null;
|
|
4920
5344
|
_hadBrowserClient = false;
|
|
4921
5345
|
_daemonServer = null;
|
|
@@ -4927,17 +5351,75 @@ var init_state2 = __esm({
|
|
|
4927
5351
|
"2h": 2 * 60 * 6e4
|
|
4928
5352
|
};
|
|
4929
5353
|
autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
4930
|
-
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
5354
|
+
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path15.default.join(import_os12.default.tmpdir(), "node9-activity.sock");
|
|
4931
5355
|
ACTIVITY_RING_SIZE = 100;
|
|
4932
5356
|
activityRing = [];
|
|
4933
5357
|
SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
4934
5358
|
}
|
|
4935
5359
|
});
|
|
4936
5360
|
|
|
5361
|
+
// src/config/patch.ts
|
|
5362
|
+
function patchConfig(configPath, patch) {
|
|
5363
|
+
let config = {};
|
|
5364
|
+
try {
|
|
5365
|
+
if (import_fs13.default.existsSync(configPath)) {
|
|
5366
|
+
config = JSON.parse(import_fs13.default.readFileSync(configPath, "utf8"));
|
|
5367
|
+
}
|
|
5368
|
+
} catch {
|
|
5369
|
+
throw new Error(`Cannot read config at ${configPath} \u2014 file may be corrupted`);
|
|
5370
|
+
}
|
|
5371
|
+
if (!config.policy || typeof config.policy !== "object") config.policy = {};
|
|
5372
|
+
const policy = config.policy;
|
|
5373
|
+
if (patch.type === "smartRule") {
|
|
5374
|
+
if (!Array.isArray(policy.smartRules)) policy.smartRules = [];
|
|
5375
|
+
const rules = policy.smartRules;
|
|
5376
|
+
if (patch.rule.name && rules.some((r) => r.name === patch.rule.name)) return;
|
|
5377
|
+
rules.push(patch.rule);
|
|
5378
|
+
} else {
|
|
5379
|
+
if (!Array.isArray(policy.ignoredTools)) policy.ignoredTools = [];
|
|
5380
|
+
const ignored = policy.ignoredTools;
|
|
5381
|
+
if (!ignored.includes(patch.toolName)) {
|
|
5382
|
+
ignored.push(patch.toolName);
|
|
5383
|
+
}
|
|
5384
|
+
}
|
|
5385
|
+
const dir = import_path16.default.dirname(configPath);
|
|
5386
|
+
import_fs13.default.mkdirSync(dir, { recursive: true });
|
|
5387
|
+
const tmp = configPath + ".node9-tmp";
|
|
5388
|
+
try {
|
|
5389
|
+
import_fs13.default.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
|
|
5390
|
+
} catch (err) {
|
|
5391
|
+
try {
|
|
5392
|
+
import_fs13.default.unlinkSync(tmp);
|
|
5393
|
+
} catch {
|
|
5394
|
+
}
|
|
5395
|
+
throw err;
|
|
5396
|
+
}
|
|
5397
|
+
try {
|
|
5398
|
+
import_fs13.default.renameSync(tmp, configPath);
|
|
5399
|
+
} catch (err) {
|
|
5400
|
+
try {
|
|
5401
|
+
import_fs13.default.unlinkSync(tmp);
|
|
5402
|
+
} catch {
|
|
5403
|
+
}
|
|
5404
|
+
throw err;
|
|
5405
|
+
}
|
|
5406
|
+
}
|
|
5407
|
+
var import_fs13, import_path16, import_os13, GLOBAL_CONFIG_PATH;
|
|
5408
|
+
var init_patch = __esm({
|
|
5409
|
+
"src/config/patch.ts"() {
|
|
5410
|
+
"use strict";
|
|
5411
|
+
import_fs13 = __toESM(require("fs"));
|
|
5412
|
+
import_path16 = __toESM(require("path"));
|
|
5413
|
+
import_os13 = __toESM(require("os"));
|
|
5414
|
+
GLOBAL_CONFIG_PATH = import_path16.default.join(import_os13.default.homedir(), ".node9", "config.json");
|
|
5415
|
+
}
|
|
5416
|
+
});
|
|
5417
|
+
|
|
4937
5418
|
// src/daemon/server.ts
|
|
4938
5419
|
function startDaemon() {
|
|
4939
|
-
|
|
4940
|
-
const
|
|
5420
|
+
loadInsightCounts();
|
|
5421
|
+
const csrfToken = (0, import_crypto5.randomUUID)();
|
|
5422
|
+
const internalToken = (0, import_crypto5.randomUUID)();
|
|
4941
5423
|
const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
|
|
4942
5424
|
const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
|
|
4943
5425
|
const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
|
|
@@ -4950,7 +5432,7 @@ function startDaemon() {
|
|
|
4950
5432
|
idleTimer = setTimeout(() => {
|
|
4951
5433
|
if (autoStarted) {
|
|
4952
5434
|
try {
|
|
4953
|
-
|
|
5435
|
+
import_fs14.default.unlinkSync(DAEMON_PID_FILE);
|
|
4954
5436
|
} catch {
|
|
4955
5437
|
}
|
|
4956
5438
|
}
|
|
@@ -4959,8 +5441,14 @@ function startDaemon() {
|
|
|
4959
5441
|
idleTimer.unref();
|
|
4960
5442
|
}
|
|
4961
5443
|
resetIdleTimer();
|
|
5444
|
+
const allowedHosts = /* @__PURE__ */ new Set([`127.0.0.1:${DAEMON_PORT}`, `localhost:${DAEMON_PORT}`]);
|
|
4962
5445
|
const server = import_http.default.createServer(async (req, res) => {
|
|
4963
|
-
const
|
|
5446
|
+
const host = req.headers.host ?? "";
|
|
5447
|
+
if (!allowedHosts.has(host)) {
|
|
5448
|
+
res.writeHead(421, { "Content-Type": "text/plain" });
|
|
5449
|
+
return res.end("Misdirected Request");
|
|
5450
|
+
}
|
|
5451
|
+
const reqUrl = new URL(req.url || "/", `http://${host}`);
|
|
4964
5452
|
const { pathname } = reqUrl;
|
|
4965
5453
|
if (req.method === "GET" && pathname === "/") {
|
|
4966
5454
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
@@ -4993,7 +5481,8 @@ data: ${JSON.stringify({
|
|
|
4993
5481
|
slackDelegated: e.slackDelegated,
|
|
4994
5482
|
timestamp: e.timestamp,
|
|
4995
5483
|
agent: e.agent,
|
|
4996
|
-
mcpServer: e.mcpServer
|
|
5484
|
+
mcpServer: e.mcpServer,
|
|
5485
|
+
allowCount: (insightCounts.get(e.toolName) ?? 0) + 1
|
|
4997
5486
|
})),
|
|
4998
5487
|
orgName: getOrgName(),
|
|
4999
5488
|
autoDenyMs: getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS
|
|
@@ -5035,6 +5524,12 @@ data: ${JSON.stringify(item.data)}
|
|
|
5035
5524
|
}
|
|
5036
5525
|
});
|
|
5037
5526
|
}
|
|
5527
|
+
if (req.method === "POST" && pathname === "/browser-opened") {
|
|
5528
|
+
if (req.headers["x-node9-internal"] !== internalToken) return res.writeHead(403).end();
|
|
5529
|
+
browserOpened = true;
|
|
5530
|
+
res.writeHead(200).end();
|
|
5531
|
+
return;
|
|
5532
|
+
}
|
|
5038
5533
|
if (req.method === "POST" && pathname === "/check") {
|
|
5039
5534
|
try {
|
|
5040
5535
|
resetIdleTimer();
|
|
@@ -5052,7 +5547,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5052
5547
|
activityId,
|
|
5053
5548
|
cwd
|
|
5054
5549
|
} = JSON.parse(body);
|
|
5055
|
-
const id = fromCLI && typeof activityId === "string" && activityId || (0,
|
|
5550
|
+
const id = fromCLI && typeof activityId === "string" && activityId || (0, import_crypto5.randomUUID)();
|
|
5056
5551
|
const entry = {
|
|
5057
5552
|
id,
|
|
5058
5553
|
toolName,
|
|
@@ -5078,7 +5573,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5078
5573
|
e.earlyReason = "No response \u2014 auto-denied after timeout";
|
|
5079
5574
|
}
|
|
5080
5575
|
pending.delete(id);
|
|
5081
|
-
broadcast("remove", { id });
|
|
5576
|
+
broadcast("remove", { id, decision: "deny" });
|
|
5082
5577
|
}
|
|
5083
5578
|
}, getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS)
|
|
5084
5579
|
};
|
|
@@ -5092,7 +5587,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5092
5587
|
status: "pending"
|
|
5093
5588
|
});
|
|
5094
5589
|
}
|
|
5095
|
-
const projectCwd = typeof cwd === "string" &&
|
|
5590
|
+
const projectCwd = typeof cwd === "string" && import_path17.default.isAbsolute(cwd) ? cwd : void 0;
|
|
5096
5591
|
const projectConfig = getConfig(projectCwd);
|
|
5097
5592
|
const browserEnabled = projectConfig.settings.approvers?.browser !== false;
|
|
5098
5593
|
const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
|
|
@@ -5105,7 +5600,10 @@ data: ${JSON.stringify(item.data)}
|
|
|
5105
5600
|
slackDelegated: entry.slackDelegated,
|
|
5106
5601
|
agent: entry.agent,
|
|
5107
5602
|
mcpServer: entry.mcpServer,
|
|
5108
|
-
interactive: terminalEnabled
|
|
5603
|
+
interactive: terminalEnabled,
|
|
5604
|
+
// allowCount = what this count will be if the user allows.
|
|
5605
|
+
// Terminal uses this to show the 💡 insight line on the Nth consecutive approval.
|
|
5606
|
+
allowCount: (insightCounts.get(toolName) ?? 0) + 1
|
|
5109
5607
|
});
|
|
5110
5608
|
const browserAlreadyOpened = process.env.NODE9_BROWSER_OPENED === "1";
|
|
5111
5609
|
if (browserEnabled && !browserOpened && !browserAlreadyOpened) {
|
|
@@ -5114,7 +5612,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5114
5612
|
}
|
|
5115
5613
|
}
|
|
5116
5614
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5117
|
-
res.end(JSON.stringify({ id }));
|
|
5615
|
+
res.end(JSON.stringify({ id, allowCount: (insightCounts.get(toolName) ?? 0) + 1 }));
|
|
5118
5616
|
if (slackDelegated) return;
|
|
5119
5617
|
authorizeHeadless(
|
|
5120
5618
|
toolName,
|
|
@@ -5141,7 +5639,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5141
5639
|
if (e.waiter) {
|
|
5142
5640
|
e.waiter(decision, result.reason);
|
|
5143
5641
|
pending.delete(id);
|
|
5144
|
-
broadcast("remove", { id });
|
|
5642
|
+
broadcast("remove", { id, decision });
|
|
5145
5643
|
} else {
|
|
5146
5644
|
e.earlyDecision = decision;
|
|
5147
5645
|
e.earlyReason = result.reason;
|
|
@@ -5157,7 +5655,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5157
5655
|
e.earlyReason = reason;
|
|
5158
5656
|
}
|
|
5159
5657
|
pending.delete(id);
|
|
5160
|
-
broadcast("remove", { id });
|
|
5658
|
+
broadcast("remove", { id, decision: "deny" });
|
|
5161
5659
|
});
|
|
5162
5660
|
return;
|
|
5163
5661
|
} catch {
|
|
@@ -5188,12 +5686,14 @@ data: ${JSON.stringify(item.data)}
|
|
|
5188
5686
|
res.end(JSON.stringify(body));
|
|
5189
5687
|
};
|
|
5190
5688
|
req.on("close", () => {
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5689
|
+
setTimeout(() => {
|
|
5690
|
+
const e = pending.get(id);
|
|
5691
|
+
if (e && e.waiter && e.earlyDecision === null) {
|
|
5692
|
+
clearTimeout(e.timer);
|
|
5693
|
+
pending.delete(id);
|
|
5694
|
+
broadcast("remove", { id });
|
|
5695
|
+
}
|
|
5696
|
+
}, 200);
|
|
5197
5697
|
});
|
|
5198
5698
|
return;
|
|
5199
5699
|
}
|
|
@@ -5222,10 +5722,10 @@ data: ${JSON.stringify(item.data)}
|
|
|
5222
5722
|
if (entry.waiter) {
|
|
5223
5723
|
entry.waiter("allow");
|
|
5224
5724
|
pending.delete(id);
|
|
5225
|
-
broadcast("remove", { id });
|
|
5725
|
+
broadcast("remove", { id, decision: "allow" });
|
|
5226
5726
|
} else {
|
|
5227
5727
|
entry.earlyDecision = "allow";
|
|
5228
|
-
broadcast("remove", { id });
|
|
5728
|
+
broadcast("remove", { id, decision: "allow" });
|
|
5229
5729
|
entry.timer = setTimeout(() => pending.delete(id), 3e4);
|
|
5230
5730
|
}
|
|
5231
5731
|
res.writeHead(200);
|
|
@@ -5239,16 +5739,29 @@ data: ${JSON.stringify(item.data)}
|
|
|
5239
5739
|
decision: resolvedDecision
|
|
5240
5740
|
});
|
|
5241
5741
|
clearTimeout(entry.timer);
|
|
5742
|
+
if (resolvedDecision === "allow" && !persist) {
|
|
5743
|
+
insightCounts.set(entry.toolName, (insightCounts.get(entry.toolName) ?? 0) + 1);
|
|
5744
|
+
saveInsightCounts();
|
|
5745
|
+
const suggestion = suggestionTracker.recordAllow(entry.toolName, entry.args);
|
|
5746
|
+
if (suggestion) {
|
|
5747
|
+
suggestions.set(suggestion.id, suggestion);
|
|
5748
|
+
broadcast("suggestion:new", suggestion);
|
|
5749
|
+
}
|
|
5750
|
+
} else if (resolvedDecision === "deny") {
|
|
5751
|
+
insightCounts.delete(entry.toolName);
|
|
5752
|
+
saveInsightCounts();
|
|
5753
|
+
suggestionTracker.resetTool(entry.toolName);
|
|
5754
|
+
}
|
|
5242
5755
|
const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
|
|
5243
5756
|
if (source && VALID_SOURCES.has(source)) entry.decisionSource = source;
|
|
5244
5757
|
if (entry.waiter) {
|
|
5245
5758
|
entry.waiter(resolvedDecision, reason);
|
|
5246
5759
|
pending.delete(id);
|
|
5247
|
-
broadcast("remove", { id });
|
|
5760
|
+
broadcast("remove", { id, decision: resolvedDecision });
|
|
5248
5761
|
} else {
|
|
5249
5762
|
entry.earlyDecision = resolvedDecision;
|
|
5250
5763
|
entry.earlyReason = reason;
|
|
5251
|
-
broadcast("remove", { id });
|
|
5764
|
+
broadcast("remove", { id, decision: resolvedDecision });
|
|
5252
5765
|
entry.timer = setTimeout(() => pending.delete(id), 3e4);
|
|
5253
5766
|
}
|
|
5254
5767
|
res.writeHead(200);
|
|
@@ -5336,13 +5849,38 @@ data: ${JSON.stringify(item.data)}
|
|
|
5336
5849
|
const id = pathname.split("/").pop();
|
|
5337
5850
|
const entry = pending.get(id);
|
|
5338
5851
|
if (!entry) return res.writeHead(404).end();
|
|
5339
|
-
const { decision } = JSON.parse(await readBody(req));
|
|
5340
|
-
|
|
5852
|
+
const { decision, source } = JSON.parse(await readBody(req));
|
|
5853
|
+
const resolvedResolveDecision = decision === "allow" ? "allow" : "deny";
|
|
5854
|
+
appendAuditLog({
|
|
5855
|
+
toolName: entry.toolName,
|
|
5856
|
+
args: entry.args,
|
|
5857
|
+
decision: resolvedResolveDecision
|
|
5858
|
+
});
|
|
5341
5859
|
clearTimeout(entry.timer);
|
|
5342
|
-
if (
|
|
5343
|
-
|
|
5860
|
+
if (resolvedResolveDecision === "allow") {
|
|
5861
|
+
insightCounts.set(entry.toolName, (insightCounts.get(entry.toolName) ?? 0) + 1);
|
|
5862
|
+
saveInsightCounts();
|
|
5863
|
+
} else {
|
|
5864
|
+
insightCounts.delete(entry.toolName);
|
|
5865
|
+
saveInsightCounts();
|
|
5866
|
+
}
|
|
5867
|
+
if (!entry.slackDelegated) {
|
|
5868
|
+
if (resolvedResolveDecision === "allow") {
|
|
5869
|
+
const suggestion = suggestionTracker.recordAllow(entry.toolName, entry.args);
|
|
5870
|
+
if (suggestion) {
|
|
5871
|
+
suggestions.set(suggestion.id, suggestion);
|
|
5872
|
+
broadcast("suggestion:new", suggestion);
|
|
5873
|
+
}
|
|
5874
|
+
} else {
|
|
5875
|
+
suggestionTracker.resetTool(entry.toolName);
|
|
5876
|
+
}
|
|
5877
|
+
}
|
|
5878
|
+
const VALID_RESOLVE_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
|
|
5879
|
+
if (source && VALID_RESOLVE_SOURCES.has(source)) entry.decisionSource = source;
|
|
5880
|
+
if (entry.waiter) entry.waiter(resolvedResolveDecision);
|
|
5881
|
+
else entry.earlyDecision = resolvedResolveDecision;
|
|
5344
5882
|
pending.delete(id);
|
|
5345
|
-
broadcast("remove", { id });
|
|
5883
|
+
broadcast("remove", { id, decision: resolvedResolveDecision });
|
|
5346
5884
|
res.writeHead(200);
|
|
5347
5885
|
return res.end(JSON.stringify({ ok: true }));
|
|
5348
5886
|
} catch {
|
|
@@ -5390,20 +5928,79 @@ data: ${JSON.stringify(item.data)}
|
|
|
5390
5928
|
res.writeHead(400).end();
|
|
5391
5929
|
}
|
|
5392
5930
|
}
|
|
5931
|
+
if (req.method === "GET" && pathname === "/suggestions") {
|
|
5932
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5933
|
+
return res.end(JSON.stringify([...suggestions.values()]));
|
|
5934
|
+
}
|
|
5935
|
+
if (req.method === "POST" && pathname.startsWith("/suggestions/") && pathname.endsWith("/apply")) {
|
|
5936
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
5937
|
+
try {
|
|
5938
|
+
const body = await readBody(req);
|
|
5939
|
+
const data = body ? JSON.parse(body) : {};
|
|
5940
|
+
const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
|
|
5941
|
+
const node9Dir = import_path17.default.dirname(GLOBAL_CONFIG_PATH);
|
|
5942
|
+
if (!import_path17.default.resolve(configPath).startsWith(node9Dir + import_path17.default.sep)) {
|
|
5943
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
5944
|
+
return res.end(
|
|
5945
|
+
JSON.stringify({ error: "configPath must be within the node9 config directory" })
|
|
5946
|
+
);
|
|
5947
|
+
}
|
|
5948
|
+
const id = pathname.split("/")[2];
|
|
5949
|
+
const suggestion = suggestions.get(id);
|
|
5950
|
+
if (!suggestion) return res.writeHead(404).end();
|
|
5951
|
+
let patch;
|
|
5952
|
+
if (data.rule !== void 0) {
|
|
5953
|
+
const parsed = SmartRuleSchema.safeParse(data.rule);
|
|
5954
|
+
if (!parsed.success) {
|
|
5955
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
5956
|
+
return res.end(JSON.stringify({ error: parsed.error.message }));
|
|
5957
|
+
}
|
|
5958
|
+
patch = { type: "smartRule", rule: parsed.data };
|
|
5959
|
+
} else {
|
|
5960
|
+
patch = suggestion.suggestedRule;
|
|
5961
|
+
}
|
|
5962
|
+
patchConfig(configPath, patch);
|
|
5963
|
+
_resetConfigCache();
|
|
5964
|
+
insightCounts.delete(suggestion.toolName);
|
|
5965
|
+
saveInsightCounts();
|
|
5966
|
+
suggestion.status = "applied";
|
|
5967
|
+
broadcast("suggestion:resolved", { id, status: "applied" });
|
|
5968
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5969
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
5970
|
+
} catch (err) {
|
|
5971
|
+
console.error(import_chalk2.default.red("[node9 daemon] POST /suggestions/:id/apply failed:"), err);
|
|
5972
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
5973
|
+
return res.end(JSON.stringify({ error: String(err) }));
|
|
5974
|
+
}
|
|
5975
|
+
}
|
|
5976
|
+
if (req.method === "POST" && pathname.startsWith("/suggestions/") && pathname.endsWith("/dismiss")) {
|
|
5977
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
5978
|
+
try {
|
|
5979
|
+
const id = pathname.split("/")[2];
|
|
5980
|
+
const suggestion = suggestions.get(id);
|
|
5981
|
+
if (!suggestion) return res.writeHead(404).end();
|
|
5982
|
+
suggestion.status = "dismissed";
|
|
5983
|
+
broadcast("suggestion:resolved", { id, status: "dismissed" });
|
|
5984
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5985
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
5986
|
+
} catch {
|
|
5987
|
+
res.writeHead(400).end();
|
|
5988
|
+
}
|
|
5989
|
+
}
|
|
5393
5990
|
res.writeHead(404).end();
|
|
5394
5991
|
});
|
|
5395
5992
|
setDaemonServer(server);
|
|
5396
5993
|
server.on("error", (e) => {
|
|
5397
5994
|
if (e.code === "EADDRINUSE") {
|
|
5398
5995
|
try {
|
|
5399
|
-
if (
|
|
5400
|
-
const { pid } = JSON.parse(
|
|
5996
|
+
if (import_fs14.default.existsSync(DAEMON_PID_FILE)) {
|
|
5997
|
+
const { pid } = JSON.parse(import_fs14.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
5401
5998
|
process.kill(pid, 0);
|
|
5402
5999
|
return process.exit(0);
|
|
5403
6000
|
}
|
|
5404
6001
|
} catch {
|
|
5405
6002
|
try {
|
|
5406
|
-
|
|
6003
|
+
import_fs14.default.unlinkSync(DAEMON_PID_FILE);
|
|
5407
6004
|
} catch {
|
|
5408
6005
|
}
|
|
5409
6006
|
server.listen(DAEMON_PORT, DAEMON_HOST);
|
|
@@ -5462,43 +6059,45 @@ data: ${JSON.stringify(item.data)}
|
|
|
5462
6059
|
}
|
|
5463
6060
|
startActivitySocket();
|
|
5464
6061
|
}
|
|
5465
|
-
var import_http,
|
|
6062
|
+
var import_http, import_fs14, import_path17, import_crypto5, import_child_process4, import_chalk2;
|
|
5466
6063
|
var init_server = __esm({
|
|
5467
6064
|
"src/daemon/server.ts"() {
|
|
5468
6065
|
"use strict";
|
|
5469
6066
|
import_http = __toESM(require("http"));
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
6067
|
+
import_fs14 = __toESM(require("fs"));
|
|
6068
|
+
import_path17 = __toESM(require("path"));
|
|
6069
|
+
import_crypto5 = require("crypto");
|
|
5473
6070
|
import_child_process4 = require("child_process");
|
|
5474
6071
|
import_chalk2 = __toESM(require("chalk"));
|
|
5475
6072
|
init_core();
|
|
5476
6073
|
init_shields();
|
|
5477
6074
|
init_ui2();
|
|
5478
6075
|
init_state2();
|
|
6076
|
+
init_patch();
|
|
6077
|
+
init_config_schema();
|
|
5479
6078
|
}
|
|
5480
6079
|
});
|
|
5481
6080
|
|
|
5482
6081
|
// src/daemon/index.ts
|
|
5483
6082
|
function stopDaemon() {
|
|
5484
|
-
if (!
|
|
6083
|
+
if (!import_fs15.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk3.default.yellow("Not running."));
|
|
5485
6084
|
try {
|
|
5486
|
-
const { pid } = JSON.parse(
|
|
6085
|
+
const { pid } = JSON.parse(import_fs15.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
5487
6086
|
process.kill(pid, "SIGTERM");
|
|
5488
6087
|
console.log(import_chalk3.default.green("\u2705 Stopped."));
|
|
5489
6088
|
} catch {
|
|
5490
6089
|
console.log(import_chalk3.default.gray("Cleaned up stale PID file."));
|
|
5491
6090
|
} finally {
|
|
5492
6091
|
try {
|
|
5493
|
-
|
|
6092
|
+
import_fs15.default.unlinkSync(DAEMON_PID_FILE);
|
|
5494
6093
|
} catch {
|
|
5495
6094
|
}
|
|
5496
6095
|
}
|
|
5497
6096
|
}
|
|
5498
6097
|
function daemonStatus() {
|
|
5499
|
-
if (
|
|
6098
|
+
if (import_fs15.default.existsSync(DAEMON_PID_FILE)) {
|
|
5500
6099
|
try {
|
|
5501
|
-
const { pid } = JSON.parse(
|
|
6100
|
+
const { pid } = JSON.parse(import_fs15.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
5502
6101
|
process.kill(pid, 0);
|
|
5503
6102
|
console.log(import_chalk3.default.green("Node9 daemon: running"));
|
|
5504
6103
|
return;
|
|
@@ -5517,11 +6116,11 @@ function daemonStatus() {
|
|
|
5517
6116
|
console.log(import_chalk3.default.yellow("Node9 daemon: not running"));
|
|
5518
6117
|
}
|
|
5519
6118
|
}
|
|
5520
|
-
var
|
|
6119
|
+
var import_fs15, import_chalk3, import_child_process5;
|
|
5521
6120
|
var init_daemon2 = __esm({
|
|
5522
6121
|
"src/daemon/index.ts"() {
|
|
5523
6122
|
"use strict";
|
|
5524
|
-
|
|
6123
|
+
import_fs15 = __toESM(require("fs"));
|
|
5525
6124
|
import_chalk3 = __toESM(require("chalk"));
|
|
5526
6125
|
import_child_process5 = require("child_process");
|
|
5527
6126
|
init_server();
|
|
@@ -5548,17 +6147,17 @@ function formatBase(activity) {
|
|
|
5548
6147
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
5549
6148
|
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
|
|
5550
6149
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
5551
|
-
return `${
|
|
6150
|
+
return `${import_chalk16.default.gray(time)} ${icon} ${import_chalk16.default.white.bold(toolName)} ${import_chalk16.default.dim(argsPreview)}`;
|
|
5552
6151
|
}
|
|
5553
6152
|
function renderResult(activity, result) {
|
|
5554
6153
|
const base = formatBase(activity);
|
|
5555
6154
|
let status;
|
|
5556
6155
|
if (result.status === "allow") {
|
|
5557
|
-
status =
|
|
6156
|
+
status = import_chalk16.default.green("\u2713 ALLOW");
|
|
5558
6157
|
} else if (result.status === "dlp") {
|
|
5559
|
-
status =
|
|
6158
|
+
status = import_chalk16.default.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
5560
6159
|
} else {
|
|
5561
|
-
status =
|
|
6160
|
+
status = import_chalk16.default.red("\u2717 BLOCK");
|
|
5562
6161
|
}
|
|
5563
6162
|
if (process.stdout.isTTY) {
|
|
5564
6163
|
import_readline3.default.clearLine(process.stdout, 0);
|
|
@@ -5568,16 +6167,16 @@ function renderResult(activity, result) {
|
|
|
5568
6167
|
}
|
|
5569
6168
|
function renderPending(activity) {
|
|
5570
6169
|
if (!process.stdout.isTTY) return;
|
|
5571
|
-
process.stdout.write(`${formatBase(activity)} ${
|
|
6170
|
+
process.stdout.write(`${formatBase(activity)} ${import_chalk16.default.yellow("\u25CF \u2026")}\r`);
|
|
5572
6171
|
}
|
|
5573
6172
|
async function ensureDaemon() {
|
|
5574
6173
|
let pidPort = null;
|
|
5575
|
-
if (
|
|
6174
|
+
if (import_fs23.default.existsSync(PID_FILE)) {
|
|
5576
6175
|
try {
|
|
5577
|
-
const { port } = JSON.parse(
|
|
6176
|
+
const { port } = JSON.parse(import_fs23.default.readFileSync(PID_FILE, "utf-8"));
|
|
5578
6177
|
pidPort = port;
|
|
5579
6178
|
} catch {
|
|
5580
|
-
console.error(
|
|
6179
|
+
console.error(import_chalk16.default.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
5581
6180
|
}
|
|
5582
6181
|
}
|
|
5583
6182
|
const checkPort = pidPort ?? DAEMON_PORT;
|
|
@@ -5588,7 +6187,7 @@ async function ensureDaemon() {
|
|
|
5588
6187
|
if (res.ok) return checkPort;
|
|
5589
6188
|
} catch {
|
|
5590
6189
|
}
|
|
5591
|
-
console.log(
|
|
6190
|
+
console.log(import_chalk16.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
5592
6191
|
const child = (0, import_child_process13.spawn)(process.execPath, [process.argv[1], "daemon"], {
|
|
5593
6192
|
detached: true,
|
|
5594
6193
|
stdio: "ignore",
|
|
@@ -5605,12 +6204,15 @@ async function ensureDaemon() {
|
|
|
5605
6204
|
} catch {
|
|
5606
6205
|
}
|
|
5607
6206
|
}
|
|
5608
|
-
console.error(
|
|
6207
|
+
console.error(import_chalk16.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
5609
6208
|
process.exit(1);
|
|
5610
6209
|
}
|
|
5611
|
-
function postDecisionHttp(id, decision, csrfToken, port) {
|
|
6210
|
+
function postDecisionHttp(id, decision, csrfToken, port, opts) {
|
|
5612
6211
|
return new Promise((resolve, reject) => {
|
|
5613
|
-
const
|
|
6212
|
+
const bodyObj = { decision, source: "terminal" };
|
|
6213
|
+
if (opts?.persist) bodyObj.persist = true;
|
|
6214
|
+
if (opts?.trustDuration) bodyObj.trustDuration = opts.trustDuration;
|
|
6215
|
+
const body = JSON.stringify(bodyObj);
|
|
5614
6216
|
const req = import_http2.default.request(
|
|
5615
6217
|
{
|
|
5616
6218
|
hostname: "127.0.0.1",
|
|
@@ -5633,22 +6235,30 @@ function postDecisionHttp(id, decision, csrfToken, port) {
|
|
|
5633
6235
|
req.end(body);
|
|
5634
6236
|
});
|
|
5635
6237
|
}
|
|
5636
|
-
function buildCardLines(req) {
|
|
6238
|
+
function buildCardLines(req, localCount = 0) {
|
|
5637
6239
|
const argsStr = JSON.stringify(req.args ?? {}).replace(/\s+/g, " ");
|
|
5638
6240
|
const argsPreview = argsStr.length > 60 ? argsStr.slice(0, 60) + "\u2026" : argsStr;
|
|
5639
6241
|
const tierLabel = req.riskMetadata?.tier != null ? req.riskMetadata.tier <= 2 ? `${YELLOW}\u26A0 Tier ${req.riskMetadata.tier}` : `${RED}\u{1F6D1} Tier ${req.riskMetadata.tier}` : `${YELLOW}\u26A0 Review`;
|
|
5640
6242
|
const blockedBy = req.riskMetadata?.blockedByLabel ?? "Policy rule";
|
|
5641
|
-
|
|
6243
|
+
const lines = [
|
|
5642
6244
|
``,
|
|
5643
6245
|
`${BOLD}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET}`,
|
|
5644
6246
|
`${CYAN}\u2551${RESET} Tool: ${BOLD}${req.toolName}${RESET}`,
|
|
5645
6247
|
`${CYAN}\u2551${RESET} Reason: ${tierLabel} \u2014 ${blockedBy}${RESET}`,
|
|
5646
|
-
`${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}
|
|
6248
|
+
`${CYAN}\u2551${RESET} Args: ${GRAY}${argsPreview}${RESET}`
|
|
6249
|
+
];
|
|
6250
|
+
if (localCount >= 2) {
|
|
6251
|
+
lines.push(
|
|
6252
|
+
`${CYAN}\u2551${RESET} ${YELLOW}\u{1F4A1}${RESET} Approved ${localCount}\xD7 before \u2014 ${BOLD}[a]${RESET}${YELLOW} creates a permanent rule${RESET}`
|
|
6253
|
+
);
|
|
6254
|
+
}
|
|
6255
|
+
lines.push(
|
|
5647
6256
|
`${CYAN}\u255A${RESET}`,
|
|
5648
6257
|
``,
|
|
5649
|
-
` ${BOLD}${GREEN}[
|
|
6258
|
+
` ${BOLD}${GREEN}[\u21B5/y]${RESET} Allow ${BOLD}${RED}[n]${RESET} Deny ${BOLD}${YELLOW}[a]${RESET} Always Allow ${BOLD}${CYAN}[t]${RESET} Trust 30m`,
|
|
5650
6259
|
``
|
|
5651
|
-
|
|
6260
|
+
);
|
|
6261
|
+
return lines;
|
|
5652
6262
|
}
|
|
5653
6263
|
async function startTail(options = {}) {
|
|
5654
6264
|
const port = await ensureDaemon();
|
|
@@ -5676,7 +6286,7 @@ async function startTail(options = {}) {
|
|
|
5676
6286
|
req2.end();
|
|
5677
6287
|
});
|
|
5678
6288
|
if (result.ok) {
|
|
5679
|
-
console.log(
|
|
6289
|
+
console.log(import_chalk16.default.green("\u2713 Flight Recorder buffer cleared."));
|
|
5680
6290
|
} else if (result.code === "ECONNREFUSED") {
|
|
5681
6291
|
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
5682
6292
|
} else if (result.code === "ETIMEDOUT") {
|
|
@@ -5693,6 +6303,7 @@ async function startTail(options = {}) {
|
|
|
5693
6303
|
let cardActive = false;
|
|
5694
6304
|
let cardLineCount = 0;
|
|
5695
6305
|
let cancelActiveCard = null;
|
|
6306
|
+
const localAllowCounts = /* @__PURE__ */ new Map();
|
|
5696
6307
|
const canApprove = process.stdout.isTTY && process.stdin.isTTY;
|
|
5697
6308
|
if (canApprove) import_readline3.default.emitKeypressEvents(process.stdin);
|
|
5698
6309
|
function clearCard() {
|
|
@@ -5703,7 +6314,10 @@ async function startTail(options = {}) {
|
|
|
5703
6314
|
}
|
|
5704
6315
|
function printCard(req2) {
|
|
5705
6316
|
process.stdout.write(HIDE_CURSOR + SAVE_CURSOR);
|
|
5706
|
-
const
|
|
6317
|
+
const daemonPrior = req2.allowCount !== void 0 ? req2.allowCount - 1 : 0;
|
|
6318
|
+
const localPrior = localAllowCounts.get(req2.toolName) ?? 0;
|
|
6319
|
+
const priorCount = Math.max(daemonPrior, localPrior);
|
|
6320
|
+
const lines = buildCardLines(req2, priorCount);
|
|
5707
6321
|
for (const line of lines) process.stdout.write(line + "\n");
|
|
5708
6322
|
cardLineCount = lines.length;
|
|
5709
6323
|
}
|
|
@@ -5731,34 +6345,70 @@ async function startTail(options = {}) {
|
|
|
5731
6345
|
process.stdin.pause();
|
|
5732
6346
|
cancelActiveCard = null;
|
|
5733
6347
|
};
|
|
5734
|
-
const settle = (
|
|
6348
|
+
const settle = (action) => {
|
|
5735
6349
|
if (settled) return;
|
|
5736
6350
|
settled = true;
|
|
5737
6351
|
cleanup();
|
|
5738
|
-
|
|
6352
|
+
process.stdout.write(RESTORE_CURSOR + ERASE_DOWN);
|
|
6353
|
+
const stampedLines = buildCardLines(
|
|
6354
|
+
req2,
|
|
6355
|
+
Math.max(
|
|
6356
|
+
req2.allowCount !== void 0 ? req2.allowCount - 1 : 0,
|
|
6357
|
+
localAllowCounts.get(req2.toolName) ?? 0
|
|
6358
|
+
)
|
|
6359
|
+
);
|
|
6360
|
+
const decisionStamp = action === "always-allow" ? import_chalk16.default.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? import_chalk16.default.cyan("\u23F1 TRUST 30m") : action === "allow" ? import_chalk16.default.green("\u2713 ALLOWED") : import_chalk16.default.red("\u2717 DENIED");
|
|
6361
|
+
stampedLines.push(` ${BOLD}\u2192${RESET} ${decisionStamp} ${GRAY}(terminal)${RESET}`, ``);
|
|
6362
|
+
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
5739
6363
|
process.stdout.write(SHOW_CURSOR);
|
|
5740
|
-
|
|
6364
|
+
cardLineCount = 0;
|
|
6365
|
+
if (action === "allow" || action === "always-allow" || action === "trust") {
|
|
6366
|
+
localAllowCounts.set(req2.toolName, (localAllowCounts.get(req2.toolName) ?? 0) + 1);
|
|
6367
|
+
} else if (action === "deny") {
|
|
6368
|
+
localAllowCounts.delete(req2.toolName);
|
|
6369
|
+
}
|
|
6370
|
+
let httpDecision;
|
|
6371
|
+
let httpOpts;
|
|
6372
|
+
if (action === "always-allow") {
|
|
6373
|
+
httpDecision = "allow";
|
|
6374
|
+
httpOpts = { persist: true };
|
|
6375
|
+
} else if (action === "trust") {
|
|
6376
|
+
httpDecision = "trust";
|
|
6377
|
+
httpOpts = { trustDuration: "30m" };
|
|
6378
|
+
} else {
|
|
6379
|
+
httpDecision = action;
|
|
6380
|
+
}
|
|
6381
|
+
postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err) => {
|
|
5741
6382
|
try {
|
|
5742
|
-
|
|
5743
|
-
|
|
6383
|
+
import_fs23.default.appendFileSync(
|
|
6384
|
+
import_path25.default.join(import_os21.default.homedir(), ".node9", "hook-debug.log"),
|
|
5744
6385
|
`[tail] POST /decision failed: ${String(err)}
|
|
5745
6386
|
`
|
|
5746
6387
|
);
|
|
5747
6388
|
} catch {
|
|
5748
6389
|
}
|
|
5749
6390
|
});
|
|
5750
|
-
const decisionLabel = decision === "allow" ? import_chalk14.default.green("\u2713 ALLOWED (terminal)") : import_chalk14.default.red("\u2717 DENIED (terminal)");
|
|
5751
|
-
console.log(`${import_chalk14.default.cyan("\u25C6")} ${import_chalk14.default.bold(req2.toolName.padEnd(16))} ${decisionLabel}`);
|
|
5752
6391
|
approvalQueue.shift();
|
|
5753
6392
|
cardActive = false;
|
|
5754
6393
|
showNextCard();
|
|
5755
6394
|
};
|
|
5756
|
-
cancelActiveCard = () => {
|
|
6395
|
+
cancelActiveCard = (externalDecision) => {
|
|
5757
6396
|
if (settled) return;
|
|
5758
6397
|
settled = true;
|
|
5759
6398
|
cleanup();
|
|
5760
|
-
|
|
6399
|
+
process.stdout.write(RESTORE_CURSOR + ERASE_DOWN);
|
|
6400
|
+
const priorCount = Math.max(
|
|
6401
|
+
req2.allowCount !== void 0 ? req2.allowCount - 1 : 0,
|
|
6402
|
+
localAllowCounts.get(req2.toolName) ?? 0
|
|
6403
|
+
);
|
|
6404
|
+
const stampedLines = buildCardLines(req2, priorCount);
|
|
6405
|
+
if (externalDecision) {
|
|
6406
|
+
const source = externalDecision === "allow" ? import_chalk16.default.green("\u2713 ALLOWED") : import_chalk16.default.red("\u2717 DENIED");
|
|
6407
|
+
stampedLines.push(` ${BOLD}\u2192${RESET} ${source} ${GRAY}(external)${RESET}`, ``);
|
|
6408
|
+
}
|
|
6409
|
+
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
5761
6410
|
process.stdout.write(SHOW_CURSOR);
|
|
6411
|
+
cardLineCount = 0;
|
|
5762
6412
|
approvalQueue.shift();
|
|
5763
6413
|
cardActive = false;
|
|
5764
6414
|
showNextCard();
|
|
@@ -5766,10 +6416,14 @@ async function startTail(options = {}) {
|
|
|
5766
6416
|
process.stdin.resume();
|
|
5767
6417
|
onKeypress = (_str, key) => {
|
|
5768
6418
|
const name = key?.name ?? "";
|
|
5769
|
-
if (name === "
|
|
6419
|
+
if (name === "y" || name === "return") {
|
|
5770
6420
|
settle("allow");
|
|
5771
|
-
} else if (name === "
|
|
6421
|
+
} else if (name === "n" || name === "d" || key?.ctrl && name === "c") {
|
|
5772
6422
|
settle("deny");
|
|
6423
|
+
} else if (name === "a") {
|
|
6424
|
+
settle("always-allow");
|
|
6425
|
+
} else if (name === "t") {
|
|
6426
|
+
settle("trust");
|
|
5773
6427
|
}
|
|
5774
6428
|
};
|
|
5775
6429
|
process.stdin.on("keypress", onKeypress);
|
|
@@ -5782,19 +6436,27 @@ async function startTail(options = {}) {
|
|
|
5782
6436
|
else if (process.platform === "win32")
|
|
5783
6437
|
(0, import_child_process13.execSync)(`cmd /c start "" "${dashboardUrl}"`, { stdio: "ignore" });
|
|
5784
6438
|
else (0, import_child_process13.execSync)(`xdg-open "${dashboardUrl}"`, { stdio: "ignore" });
|
|
6439
|
+
const intToken = getInternalToken();
|
|
6440
|
+
fetch(`http://127.0.0.1:${port}/browser-opened`, {
|
|
6441
|
+
method: "POST",
|
|
6442
|
+
headers: intToken ? { "X-Node9-Internal": intToken } : {}
|
|
6443
|
+
}).catch(() => {
|
|
6444
|
+
});
|
|
5785
6445
|
}
|
|
5786
6446
|
} catch {
|
|
5787
6447
|
}
|
|
5788
|
-
console.log(
|
|
5789
|
-
\u{1F6F0}\uFE0F Node9 tail `) +
|
|
6448
|
+
console.log(import_chalk16.default.cyan.bold(`
|
|
6449
|
+
\u{1F6F0}\uFE0F Node9 tail `) + import_chalk16.default.dim(`\u2192 ${dashboardUrl}`));
|
|
5790
6450
|
if (canApprove) {
|
|
5791
|
-
console.log(
|
|
6451
|
+
console.log(
|
|
6452
|
+
import_chalk16.default.dim("Interactive approvals: [\u21B5/y] Allow [n] Deny [a] Always Allow [t] Trust 30m")
|
|
6453
|
+
);
|
|
5792
6454
|
}
|
|
5793
6455
|
if (options.history) {
|
|
5794
|
-
console.log(
|
|
6456
|
+
console.log(import_chalk16.default.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
|
|
5795
6457
|
} else {
|
|
5796
6458
|
console.log(
|
|
5797
|
-
|
|
6459
|
+
import_chalk16.default.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
|
|
5798
6460
|
);
|
|
5799
6461
|
}
|
|
5800
6462
|
process.on("SIGINT", () => {
|
|
@@ -5804,13 +6466,13 @@ async function startTail(options = {}) {
|
|
|
5804
6466
|
import_readline3.default.clearLine(process.stdout, 0);
|
|
5805
6467
|
import_readline3.default.cursorTo(process.stdout, 0);
|
|
5806
6468
|
}
|
|
5807
|
-
console.log(
|
|
6469
|
+
console.log(import_chalk16.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
5808
6470
|
process.exit(0);
|
|
5809
6471
|
});
|
|
5810
6472
|
const sseUrl = `http://127.0.0.1:${port}/events?capabilities=input`;
|
|
5811
6473
|
const req = import_http2.default.get(sseUrl, (res) => {
|
|
5812
6474
|
if (res.statusCode !== 200) {
|
|
5813
|
-
console.error(
|
|
6475
|
+
console.error(import_chalk16.default.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
5814
6476
|
process.exit(1);
|
|
5815
6477
|
}
|
|
5816
6478
|
let currentEvent = "";
|
|
@@ -5840,7 +6502,7 @@ async function startTail(options = {}) {
|
|
|
5840
6502
|
import_readline3.default.clearLine(process.stdout, 0);
|
|
5841
6503
|
import_readline3.default.cursorTo(process.stdout, 0);
|
|
5842
6504
|
}
|
|
5843
|
-
console.log(
|
|
6505
|
+
console.log(import_chalk16.default.red("\n\u274C Daemon disconnected."));
|
|
5844
6506
|
process.exit(1);
|
|
5845
6507
|
});
|
|
5846
6508
|
});
|
|
@@ -5881,11 +6543,17 @@ async function startTail(options = {}) {
|
|
|
5881
6543
|
}
|
|
5882
6544
|
if (event === "remove") {
|
|
5883
6545
|
try {
|
|
5884
|
-
const { id } = JSON.parse(rawData);
|
|
6546
|
+
const { id, decision } = JSON.parse(rawData);
|
|
5885
6547
|
const idx = approvalQueue.findIndex((r) => r.id === id);
|
|
5886
6548
|
if (idx !== -1) {
|
|
5887
6549
|
if (idx === 0 && cardActive && cancelActiveCard) {
|
|
5888
|
-
|
|
6550
|
+
const toolName = approvalQueue[0].toolName;
|
|
6551
|
+
if (decision === "allow") {
|
|
6552
|
+
localAllowCounts.set(toolName, (localAllowCounts.get(toolName) ?? 0) + 1);
|
|
6553
|
+
} else if (decision === "deny") {
|
|
6554
|
+
localAllowCounts.delete(toolName);
|
|
6555
|
+
}
|
|
6556
|
+
cancelActiveCard(decision);
|
|
5889
6557
|
} else {
|
|
5890
6558
|
approvalQueue.splice(idx, 1);
|
|
5891
6559
|
}
|
|
@@ -5920,25 +6588,26 @@ async function startTail(options = {}) {
|
|
|
5920
6588
|
}
|
|
5921
6589
|
req.on("error", (err) => {
|
|
5922
6590
|
const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
|
|
5923
|
-
console.error(
|
|
6591
|
+
console.error(import_chalk16.default.red(`
|
|
5924
6592
|
\u274C ${msg}`));
|
|
5925
6593
|
process.exit(1);
|
|
5926
6594
|
});
|
|
5927
6595
|
}
|
|
5928
|
-
var import_http2,
|
|
6596
|
+
var import_http2, import_chalk16, import_fs23, import_os21, import_path25, import_readline3, import_child_process13, PID_FILE, ICONS, RESET, BOLD, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, SAVE_CURSOR, RESTORE_CURSOR;
|
|
5929
6597
|
var init_tail = __esm({
|
|
5930
6598
|
"src/tui/tail.ts"() {
|
|
5931
6599
|
"use strict";
|
|
5932
6600
|
import_http2 = __toESM(require("http"));
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
6601
|
+
import_chalk16 = __toESM(require("chalk"));
|
|
6602
|
+
import_fs23 = __toESM(require("fs"));
|
|
6603
|
+
import_os21 = __toESM(require("os"));
|
|
6604
|
+
import_path25 = __toESM(require("path"));
|
|
5937
6605
|
import_readline3 = __toESM(require("readline"));
|
|
5938
6606
|
import_child_process13 = require("child_process");
|
|
5939
6607
|
init_daemon2();
|
|
6608
|
+
init_daemon();
|
|
5940
6609
|
init_core();
|
|
5941
|
-
PID_FILE =
|
|
6610
|
+
PID_FILE = import_path25.default.join(import_os21.default.homedir(), ".node9", "daemon.pid");
|
|
5942
6611
|
ICONS = {
|
|
5943
6612
|
bash: "\u{1F4BB}",
|
|
5944
6613
|
shell: "\u{1F4BB}",
|
|
@@ -5976,9 +6645,9 @@ var import_commander = require("commander");
|
|
|
5976
6645
|
init_core();
|
|
5977
6646
|
|
|
5978
6647
|
// src/setup.ts
|
|
5979
|
-
var
|
|
5980
|
-
var
|
|
5981
|
-
var
|
|
6648
|
+
var import_fs11 = __toESM(require("fs"));
|
|
6649
|
+
var import_path14 = __toESM(require("path"));
|
|
6650
|
+
var import_os11 = __toESM(require("os"));
|
|
5982
6651
|
var import_chalk = __toESM(require("chalk"));
|
|
5983
6652
|
var import_prompts = require("@inquirer/prompts");
|
|
5984
6653
|
function printDaemonTip() {
|
|
@@ -5995,26 +6664,26 @@ function fullPathCommand(subcommand) {
|
|
|
5995
6664
|
}
|
|
5996
6665
|
function readJson(filePath) {
|
|
5997
6666
|
try {
|
|
5998
|
-
if (
|
|
5999
|
-
return JSON.parse(
|
|
6667
|
+
if (import_fs11.default.existsSync(filePath)) {
|
|
6668
|
+
return JSON.parse(import_fs11.default.readFileSync(filePath, "utf-8"));
|
|
6000
6669
|
}
|
|
6001
6670
|
} catch {
|
|
6002
6671
|
}
|
|
6003
6672
|
return null;
|
|
6004
6673
|
}
|
|
6005
6674
|
function writeJson(filePath, data) {
|
|
6006
|
-
const dir =
|
|
6007
|
-
if (!
|
|
6008
|
-
|
|
6675
|
+
const dir = import_path14.default.dirname(filePath);
|
|
6676
|
+
if (!import_fs11.default.existsSync(dir)) import_fs11.default.mkdirSync(dir, { recursive: true });
|
|
6677
|
+
import_fs11.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
6009
6678
|
}
|
|
6010
6679
|
function isNode9Hook(cmd) {
|
|
6011
6680
|
if (!cmd) return false;
|
|
6012
6681
|
return /(?:^|[\s/\\])node9 (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js (?:check|log)/.test(cmd);
|
|
6013
6682
|
}
|
|
6014
6683
|
function teardownClaude() {
|
|
6015
|
-
const homeDir2 =
|
|
6016
|
-
const hooksPath =
|
|
6017
|
-
const mcpPath =
|
|
6684
|
+
const homeDir2 = import_os11.default.homedir();
|
|
6685
|
+
const hooksPath = import_path14.default.join(homeDir2, ".claude", "settings.json");
|
|
6686
|
+
const mcpPath = import_path14.default.join(homeDir2, ".claude.json");
|
|
6018
6687
|
let changed = false;
|
|
6019
6688
|
const settings = readJson(hooksPath);
|
|
6020
6689
|
if (settings?.hooks) {
|
|
@@ -6062,8 +6731,8 @@ function teardownClaude() {
|
|
|
6062
6731
|
}
|
|
6063
6732
|
}
|
|
6064
6733
|
function teardownGemini() {
|
|
6065
|
-
const homeDir2 =
|
|
6066
|
-
const settingsPath =
|
|
6734
|
+
const homeDir2 = import_os11.default.homedir();
|
|
6735
|
+
const settingsPath = import_path14.default.join(homeDir2, ".gemini", "settings.json");
|
|
6067
6736
|
const settings = readJson(settingsPath);
|
|
6068
6737
|
if (!settings) {
|
|
6069
6738
|
console.log(import_chalk.default.blue(" \u2139\uFE0F ~/.gemini/settings.json not found \u2014 nothing to remove"));
|
|
@@ -6101,8 +6770,8 @@ function teardownGemini() {
|
|
|
6101
6770
|
}
|
|
6102
6771
|
}
|
|
6103
6772
|
function teardownCursor() {
|
|
6104
|
-
const homeDir2 =
|
|
6105
|
-
const mcpPath =
|
|
6773
|
+
const homeDir2 = import_os11.default.homedir();
|
|
6774
|
+
const mcpPath = import_path14.default.join(homeDir2, ".cursor", "mcp.json");
|
|
6106
6775
|
const mcpConfig = readJson(mcpPath);
|
|
6107
6776
|
if (!mcpConfig?.mcpServers) {
|
|
6108
6777
|
console.log(import_chalk.default.blue(" \u2139\uFE0F ~/.cursor/mcp.json not found \u2014 nothing to remove"));
|
|
@@ -6128,9 +6797,9 @@ function teardownCursor() {
|
|
|
6128
6797
|
}
|
|
6129
6798
|
}
|
|
6130
6799
|
async function setupClaude() {
|
|
6131
|
-
const homeDir2 =
|
|
6132
|
-
const mcpPath =
|
|
6133
|
-
const hooksPath =
|
|
6800
|
+
const homeDir2 = import_os11.default.homedir();
|
|
6801
|
+
const mcpPath = import_path14.default.join(homeDir2, ".claude.json");
|
|
6802
|
+
const hooksPath = import_path14.default.join(homeDir2, ".claude", "settings.json");
|
|
6134
6803
|
const claudeConfig = readJson(mcpPath) ?? {};
|
|
6135
6804
|
const settings = readJson(hooksPath) ?? {};
|
|
6136
6805
|
const servers = claudeConfig.mcpServers ?? {};
|
|
@@ -6204,8 +6873,8 @@ async function setupClaude() {
|
|
|
6204
6873
|
}
|
|
6205
6874
|
}
|
|
6206
6875
|
async function setupGemini() {
|
|
6207
|
-
const homeDir2 =
|
|
6208
|
-
const settingsPath =
|
|
6876
|
+
const homeDir2 = import_os11.default.homedir();
|
|
6877
|
+
const settingsPath = import_path14.default.join(homeDir2, ".gemini", "settings.json");
|
|
6209
6878
|
const settings = readJson(settingsPath) ?? {};
|
|
6210
6879
|
const servers = settings.mcpServers ?? {};
|
|
6211
6880
|
let anythingChanged = false;
|
|
@@ -6286,9 +6955,28 @@ async function setupGemini() {
|
|
|
6286
6955
|
printDaemonTip();
|
|
6287
6956
|
}
|
|
6288
6957
|
}
|
|
6958
|
+
function detectAgents(homeDir2 = import_os11.default.homedir()) {
|
|
6959
|
+
const exists = (p) => {
|
|
6960
|
+
try {
|
|
6961
|
+
return import_fs11.default.existsSync(p);
|
|
6962
|
+
} catch (err) {
|
|
6963
|
+
const code = err.code;
|
|
6964
|
+
if (code !== "ENOENT") {
|
|
6965
|
+
process.stderr.write(`[node9] detectAgents: cannot access ${p}: ${code ?? String(err)}
|
|
6966
|
+
`);
|
|
6967
|
+
}
|
|
6968
|
+
return false;
|
|
6969
|
+
}
|
|
6970
|
+
};
|
|
6971
|
+
return {
|
|
6972
|
+
claude: exists(import_path14.default.join(homeDir2, ".claude")) || exists(import_path14.default.join(homeDir2, ".claude.json")),
|
|
6973
|
+
gemini: exists(import_path14.default.join(homeDir2, ".gemini")),
|
|
6974
|
+
cursor: exists(import_path14.default.join(homeDir2, ".cursor"))
|
|
6975
|
+
};
|
|
6976
|
+
}
|
|
6289
6977
|
async function setupCursor() {
|
|
6290
|
-
const homeDir2 =
|
|
6291
|
-
const mcpPath =
|
|
6978
|
+
const homeDir2 = import_os11.default.homedir();
|
|
6979
|
+
const mcpPath = import_path14.default.join(homeDir2, ".cursor", "mcp.json");
|
|
6292
6980
|
const mcpConfig = readJson(mcpPath) ?? {};
|
|
6293
6981
|
const servers = mcpConfig.mcpServers ?? {};
|
|
6294
6982
|
let anythingChanged = false;
|
|
@@ -6344,10 +7032,10 @@ async function setupCursor() {
|
|
|
6344
7032
|
|
|
6345
7033
|
// src/cli.ts
|
|
6346
7034
|
init_daemon2();
|
|
6347
|
-
var
|
|
6348
|
-
var
|
|
6349
|
-
var
|
|
6350
|
-
var
|
|
7035
|
+
var import_chalk17 = __toESM(require("chalk"));
|
|
7036
|
+
var import_fs24 = __toESM(require("fs"));
|
|
7037
|
+
var import_path26 = __toESM(require("path"));
|
|
7038
|
+
var import_os22 = __toESM(require("os"));
|
|
6351
7039
|
var import_prompts3 = require("@inquirer/prompts");
|
|
6352
7040
|
|
|
6353
7041
|
// src/utils/duration.ts
|
|
@@ -6568,9 +7256,9 @@ async function autoStartDaemonAndWait() {
|
|
|
6568
7256
|
|
|
6569
7257
|
// src/cli/commands/check.ts
|
|
6570
7258
|
var import_chalk5 = __toESM(require("chalk"));
|
|
6571
|
-
var
|
|
6572
|
-
var
|
|
6573
|
-
var
|
|
7259
|
+
var import_fs17 = __toESM(require("fs"));
|
|
7260
|
+
var import_path19 = __toESM(require("path"));
|
|
7261
|
+
var import_os15 = __toESM(require("os"));
|
|
6574
7262
|
init_orchestrator();
|
|
6575
7263
|
init_daemon();
|
|
6576
7264
|
init_config();
|
|
@@ -6578,26 +7266,26 @@ init_policy();
|
|
|
6578
7266
|
|
|
6579
7267
|
// src/undo.ts
|
|
6580
7268
|
var import_child_process8 = require("child_process");
|
|
6581
|
-
var
|
|
6582
|
-
var
|
|
6583
|
-
var
|
|
6584
|
-
var
|
|
6585
|
-
var SNAPSHOT_STACK_PATH =
|
|
6586
|
-
var UNDO_LATEST_PATH =
|
|
7269
|
+
var import_crypto6 = __toESM(require("crypto"));
|
|
7270
|
+
var import_fs16 = __toESM(require("fs"));
|
|
7271
|
+
var import_path18 = __toESM(require("path"));
|
|
7272
|
+
var import_os14 = __toESM(require("os"));
|
|
7273
|
+
var SNAPSHOT_STACK_PATH = import_path18.default.join(import_os14.default.homedir(), ".node9", "snapshots.json");
|
|
7274
|
+
var UNDO_LATEST_PATH = import_path18.default.join(import_os14.default.homedir(), ".node9", "undo_latest.txt");
|
|
6587
7275
|
var MAX_SNAPSHOTS = 10;
|
|
6588
7276
|
var GIT_TIMEOUT = 15e3;
|
|
6589
7277
|
function readStack() {
|
|
6590
7278
|
try {
|
|
6591
|
-
if (
|
|
6592
|
-
return JSON.parse(
|
|
7279
|
+
if (import_fs16.default.existsSync(SNAPSHOT_STACK_PATH))
|
|
7280
|
+
return JSON.parse(import_fs16.default.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
|
|
6593
7281
|
} catch {
|
|
6594
7282
|
}
|
|
6595
7283
|
return [];
|
|
6596
7284
|
}
|
|
6597
7285
|
function writeStack(stack) {
|
|
6598
|
-
const dir =
|
|
6599
|
-
if (!
|
|
6600
|
-
|
|
7286
|
+
const dir = import_path18.default.dirname(SNAPSHOT_STACK_PATH);
|
|
7287
|
+
if (!import_fs16.default.existsSync(dir)) import_fs16.default.mkdirSync(dir, { recursive: true });
|
|
7288
|
+
import_fs16.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
6601
7289
|
}
|
|
6602
7290
|
function buildArgsSummary(tool, args) {
|
|
6603
7291
|
if (!args || typeof args !== "object") return "";
|
|
@@ -6613,7 +7301,7 @@ function buildArgsSummary(tool, args) {
|
|
|
6613
7301
|
function normalizeCwdForHash(cwd) {
|
|
6614
7302
|
let normalized;
|
|
6615
7303
|
try {
|
|
6616
|
-
normalized =
|
|
7304
|
+
normalized = import_fs16.default.realpathSync(cwd);
|
|
6617
7305
|
} catch {
|
|
6618
7306
|
normalized = cwd;
|
|
6619
7307
|
}
|
|
@@ -6622,17 +7310,17 @@ function normalizeCwdForHash(cwd) {
|
|
|
6622
7310
|
return normalized;
|
|
6623
7311
|
}
|
|
6624
7312
|
function getShadowRepoDir(cwd) {
|
|
6625
|
-
const hash =
|
|
6626
|
-
return
|
|
7313
|
+
const hash = import_crypto6.default.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
|
|
7314
|
+
return import_path18.default.join(import_os14.default.homedir(), ".node9", "snapshots", hash);
|
|
6627
7315
|
}
|
|
6628
7316
|
function cleanOrphanedIndexFiles(shadowDir) {
|
|
6629
7317
|
try {
|
|
6630
7318
|
const cutoff = Date.now() - 6e4;
|
|
6631
|
-
for (const f of
|
|
7319
|
+
for (const f of import_fs16.default.readdirSync(shadowDir)) {
|
|
6632
7320
|
if (f.startsWith("index_")) {
|
|
6633
|
-
const fp =
|
|
7321
|
+
const fp = import_path18.default.join(shadowDir, f);
|
|
6634
7322
|
try {
|
|
6635
|
-
if (
|
|
7323
|
+
if (import_fs16.default.statSync(fp).mtimeMs < cutoff) import_fs16.default.unlinkSync(fp);
|
|
6636
7324
|
} catch {
|
|
6637
7325
|
}
|
|
6638
7326
|
}
|
|
@@ -6644,7 +7332,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
|
|
|
6644
7332
|
const hardcoded = [".git", ".node9"];
|
|
6645
7333
|
const lines = [...hardcoded, ...ignorePaths].join("\n");
|
|
6646
7334
|
try {
|
|
6647
|
-
|
|
7335
|
+
import_fs16.default.writeFileSync(import_path18.default.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
|
|
6648
7336
|
} catch {
|
|
6649
7337
|
}
|
|
6650
7338
|
}
|
|
@@ -6657,25 +7345,25 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
6657
7345
|
timeout: 3e3
|
|
6658
7346
|
});
|
|
6659
7347
|
if (check.status === 0) {
|
|
6660
|
-
const ptPath =
|
|
7348
|
+
const ptPath = import_path18.default.join(shadowDir, "project-path.txt");
|
|
6661
7349
|
try {
|
|
6662
|
-
const stored =
|
|
7350
|
+
const stored = import_fs16.default.readFileSync(ptPath, "utf8").trim();
|
|
6663
7351
|
if (stored === normalizedCwd) return true;
|
|
6664
7352
|
if (process.env.NODE9_DEBUG === "1")
|
|
6665
7353
|
console.error(
|
|
6666
7354
|
`[Node9] Shadow repo path mismatch: stored="${stored}" expected="${normalizedCwd}" \u2014 reinitializing`
|
|
6667
7355
|
);
|
|
6668
|
-
|
|
7356
|
+
import_fs16.default.rmSync(shadowDir, { recursive: true, force: true });
|
|
6669
7357
|
} catch {
|
|
6670
7358
|
try {
|
|
6671
|
-
|
|
7359
|
+
import_fs16.default.writeFileSync(ptPath, normalizedCwd, "utf8");
|
|
6672
7360
|
} catch {
|
|
6673
7361
|
}
|
|
6674
7362
|
return true;
|
|
6675
7363
|
}
|
|
6676
7364
|
}
|
|
6677
7365
|
try {
|
|
6678
|
-
|
|
7366
|
+
import_fs16.default.mkdirSync(shadowDir, { recursive: true });
|
|
6679
7367
|
} catch {
|
|
6680
7368
|
}
|
|
6681
7369
|
const init = (0, import_child_process8.spawnSync)("git", ["init", "--bare", shadowDir], { timeout: 5e3 });
|
|
@@ -6684,7 +7372,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
6684
7372
|
console.error("[Node9] git init --bare failed:", init.stderr?.toString());
|
|
6685
7373
|
return false;
|
|
6686
7374
|
}
|
|
6687
|
-
const configFile =
|
|
7375
|
+
const configFile = import_path18.default.join(shadowDir, "config");
|
|
6688
7376
|
(0, import_child_process8.spawnSync)("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
|
|
6689
7377
|
timeout: 3e3
|
|
6690
7378
|
});
|
|
@@ -6692,7 +7380,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
6692
7380
|
timeout: 3e3
|
|
6693
7381
|
});
|
|
6694
7382
|
try {
|
|
6695
|
-
|
|
7383
|
+
import_fs16.default.writeFileSync(import_path18.default.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
|
|
6696
7384
|
} catch {
|
|
6697
7385
|
}
|
|
6698
7386
|
return true;
|
|
@@ -6715,7 +7403,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
6715
7403
|
const shadowDir = getShadowRepoDir(cwd);
|
|
6716
7404
|
if (!ensureShadowRepo(shadowDir, cwd)) return null;
|
|
6717
7405
|
writeShadowExcludes(shadowDir, ignorePaths);
|
|
6718
|
-
indexFile =
|
|
7406
|
+
indexFile = import_path18.default.join(shadowDir, `index_${process.pid}_${Date.now()}`);
|
|
6719
7407
|
const shadowEnv = {
|
|
6720
7408
|
...process.env,
|
|
6721
7409
|
GIT_DIR: shadowDir,
|
|
@@ -6744,7 +7432,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
6744
7432
|
const shouldGc = stack.length % 5 === 0;
|
|
6745
7433
|
if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
|
|
6746
7434
|
writeStack(stack);
|
|
6747
|
-
|
|
7435
|
+
import_fs16.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
|
|
6748
7436
|
if (shouldGc) {
|
|
6749
7437
|
(0, import_child_process8.spawn)("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
|
|
6750
7438
|
}
|
|
@@ -6755,7 +7443,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
6755
7443
|
} finally {
|
|
6756
7444
|
if (indexFile) {
|
|
6757
7445
|
try {
|
|
6758
|
-
|
|
7446
|
+
import_fs16.default.unlinkSync(indexFile);
|
|
6759
7447
|
} catch {
|
|
6760
7448
|
}
|
|
6761
7449
|
}
|
|
@@ -6824,9 +7512,9 @@ function applyUndo(hash, cwd) {
|
|
|
6824
7512
|
timeout: GIT_TIMEOUT
|
|
6825
7513
|
}).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
6826
7514
|
for (const file of [...tracked, ...untracked]) {
|
|
6827
|
-
const fullPath =
|
|
6828
|
-
if (!snapshotFiles.has(file) &&
|
|
6829
|
-
|
|
7515
|
+
const fullPath = import_path18.default.join(dir, file);
|
|
7516
|
+
if (!snapshotFiles.has(file) && import_fs16.default.existsSync(fullPath)) {
|
|
7517
|
+
import_fs16.default.unlinkSync(fullPath);
|
|
6830
7518
|
}
|
|
6831
7519
|
}
|
|
6832
7520
|
return true;
|
|
@@ -6850,9 +7538,9 @@ function registerCheckCommand(program2) {
|
|
|
6850
7538
|
} catch (err) {
|
|
6851
7539
|
const tempConfig = getConfig();
|
|
6852
7540
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
6853
|
-
const logPath =
|
|
7541
|
+
const logPath = import_path19.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
|
|
6854
7542
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
6855
|
-
|
|
7543
|
+
import_fs17.default.appendFileSync(
|
|
6856
7544
|
logPath,
|
|
6857
7545
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
6858
7546
|
RAW: ${raw}
|
|
@@ -6863,10 +7551,10 @@ RAW: ${raw}
|
|
|
6863
7551
|
}
|
|
6864
7552
|
const config = getConfig(payload.cwd || void 0);
|
|
6865
7553
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
6866
|
-
const logPath =
|
|
6867
|
-
if (!
|
|
6868
|
-
|
|
6869
|
-
|
|
7554
|
+
const logPath = import_path19.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
|
|
7555
|
+
if (!import_fs17.default.existsSync(import_path19.default.dirname(logPath)))
|
|
7556
|
+
import_fs17.default.mkdirSync(import_path19.default.dirname(logPath), { recursive: true });
|
|
7557
|
+
import_fs17.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
6870
7558
|
`);
|
|
6871
7559
|
}
|
|
6872
7560
|
const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
|
|
@@ -6879,8 +7567,8 @@ RAW: ${raw}
|
|
|
6879
7567
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
6880
7568
|
let ttyFd = null;
|
|
6881
7569
|
try {
|
|
6882
|
-
ttyFd =
|
|
6883
|
-
const writeTty = (line) =>
|
|
7570
|
+
ttyFd = import_fs17.default.openSync("/dev/tty", "w");
|
|
7571
|
+
const writeTty = (line) => import_fs17.default.writeSync(ttyFd, line + "\n");
|
|
6884
7572
|
if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
|
|
6885
7573
|
writeTty(import_chalk5.default.bgRed.white.bold(`
|
|
6886
7574
|
\u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
@@ -6896,7 +7584,7 @@ RAW: ${raw}
|
|
|
6896
7584
|
} finally {
|
|
6897
7585
|
if (ttyFd !== null)
|
|
6898
7586
|
try {
|
|
6899
|
-
|
|
7587
|
+
import_fs17.default.closeSync(ttyFd);
|
|
6900
7588
|
} catch {
|
|
6901
7589
|
}
|
|
6902
7590
|
}
|
|
@@ -6927,7 +7615,7 @@ RAW: ${raw}
|
|
|
6927
7615
|
if (shouldSnapshot(toolName, toolInput, config)) {
|
|
6928
7616
|
await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
|
|
6929
7617
|
}
|
|
6930
|
-
const safeCwdForAuth = typeof payload.cwd === "string" &&
|
|
7618
|
+
const safeCwdForAuth = typeof payload.cwd === "string" && import_path19.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
6931
7619
|
const result = await authorizeHeadless(toolName, toolInput, meta, {
|
|
6932
7620
|
cwd: safeCwdForAuth
|
|
6933
7621
|
});
|
|
@@ -6939,12 +7627,12 @@ RAW: ${raw}
|
|
|
6939
7627
|
}
|
|
6940
7628
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
6941
7629
|
try {
|
|
6942
|
-
const tty =
|
|
6943
|
-
|
|
7630
|
+
const tty = import_fs17.default.openSync("/dev/tty", "w");
|
|
7631
|
+
import_fs17.default.writeSync(
|
|
6944
7632
|
tty,
|
|
6945
7633
|
import_chalk5.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
|
|
6946
7634
|
);
|
|
6947
|
-
|
|
7635
|
+
import_fs17.default.closeSync(tty);
|
|
6948
7636
|
} catch {
|
|
6949
7637
|
}
|
|
6950
7638
|
const daemonReady = await autoStartDaemonAndWait();
|
|
@@ -6971,9 +7659,9 @@ RAW: ${raw}
|
|
|
6971
7659
|
});
|
|
6972
7660
|
} catch (err) {
|
|
6973
7661
|
if (process.env.NODE9_DEBUG === "1") {
|
|
6974
|
-
const logPath =
|
|
7662
|
+
const logPath = import_path19.default.join(import_os15.default.homedir(), ".node9", "hook-debug.log");
|
|
6975
7663
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
6976
|
-
|
|
7664
|
+
import_fs17.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
6977
7665
|
`);
|
|
6978
7666
|
}
|
|
6979
7667
|
process.exit(0);
|
|
@@ -7007,9 +7695,9 @@ RAW: ${raw}
|
|
|
7007
7695
|
}
|
|
7008
7696
|
|
|
7009
7697
|
// src/cli/commands/log.ts
|
|
7010
|
-
var
|
|
7011
|
-
var
|
|
7012
|
-
var
|
|
7698
|
+
var import_fs18 = __toESM(require("fs"));
|
|
7699
|
+
var import_path20 = __toESM(require("path"));
|
|
7700
|
+
var import_os16 = __toESM(require("os"));
|
|
7013
7701
|
init_audit();
|
|
7014
7702
|
init_config();
|
|
7015
7703
|
init_policy();
|
|
@@ -7031,11 +7719,11 @@ function registerLogCommand(program2) {
|
|
|
7031
7719
|
decision: "allowed",
|
|
7032
7720
|
source: "post-hook"
|
|
7033
7721
|
};
|
|
7034
|
-
const logPath =
|
|
7035
|
-
if (!
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
const safeCwd = typeof payload.cwd === "string" &&
|
|
7722
|
+
const logPath = import_path20.default.join(import_os16.default.homedir(), ".node9", "audit.log");
|
|
7723
|
+
if (!import_fs18.default.existsSync(import_path20.default.dirname(logPath)))
|
|
7724
|
+
import_fs18.default.mkdirSync(import_path20.default.dirname(logPath), { recursive: true });
|
|
7725
|
+
import_fs18.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
7726
|
+
const safeCwd = typeof payload.cwd === "string" && import_path20.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
7039
7727
|
const config = getConfig(safeCwd);
|
|
7040
7728
|
if (shouldSnapshot(tool, {}, config)) {
|
|
7041
7729
|
await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
|
|
@@ -7044,9 +7732,9 @@ function registerLogCommand(program2) {
|
|
|
7044
7732
|
const msg = err instanceof Error ? err.message : String(err);
|
|
7045
7733
|
process.stderr.write(`[Node9] audit log error: ${msg}
|
|
7046
7734
|
`);
|
|
7047
|
-
const debugPath =
|
|
7735
|
+
const debugPath = import_path20.default.join(import_os16.default.homedir(), ".node9", "hook-debug.log");
|
|
7048
7736
|
try {
|
|
7049
|
-
|
|
7737
|
+
import_fs18.default.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
|
|
7050
7738
|
`);
|
|
7051
7739
|
} catch {
|
|
7052
7740
|
}
|
|
@@ -7350,14 +8038,14 @@ function registerConfigShowCommand(program2) {
|
|
|
7350
8038
|
|
|
7351
8039
|
// src/cli/commands/doctor.ts
|
|
7352
8040
|
var import_chalk7 = __toESM(require("chalk"));
|
|
7353
|
-
var
|
|
7354
|
-
var
|
|
7355
|
-
var
|
|
8041
|
+
var import_fs19 = __toESM(require("fs"));
|
|
8042
|
+
var import_path21 = __toESM(require("path"));
|
|
8043
|
+
var import_os17 = __toESM(require("os"));
|
|
7356
8044
|
var import_child_process9 = require("child_process");
|
|
7357
8045
|
init_daemon();
|
|
7358
8046
|
function registerDoctorCommand(program2, version2) {
|
|
7359
8047
|
program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
7360
|
-
const homeDir2 =
|
|
8048
|
+
const homeDir2 = import_os17.default.homedir();
|
|
7361
8049
|
let failures = 0;
|
|
7362
8050
|
function pass(msg) {
|
|
7363
8051
|
console.log(import_chalk7.default.green(" \u2705 ") + msg);
|
|
@@ -7406,10 +8094,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7406
8094
|
);
|
|
7407
8095
|
}
|
|
7408
8096
|
section("Configuration");
|
|
7409
|
-
const globalConfigPath =
|
|
7410
|
-
if (
|
|
8097
|
+
const globalConfigPath = import_path21.default.join(homeDir2, ".node9", "config.json");
|
|
8098
|
+
if (import_fs19.default.existsSync(globalConfigPath)) {
|
|
7411
8099
|
try {
|
|
7412
|
-
JSON.parse(
|
|
8100
|
+
JSON.parse(import_fs19.default.readFileSync(globalConfigPath, "utf-8"));
|
|
7413
8101
|
pass("~/.node9/config.json found and valid");
|
|
7414
8102
|
} catch {
|
|
7415
8103
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -7417,10 +8105,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7417
8105
|
} else {
|
|
7418
8106
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
7419
8107
|
}
|
|
7420
|
-
const projectConfigPath =
|
|
7421
|
-
if (
|
|
8108
|
+
const projectConfigPath = import_path21.default.join(process.cwd(), "node9.config.json");
|
|
8109
|
+
if (import_fs19.default.existsSync(projectConfigPath)) {
|
|
7422
8110
|
try {
|
|
7423
|
-
JSON.parse(
|
|
8111
|
+
JSON.parse(import_fs19.default.readFileSync(projectConfigPath, "utf-8"));
|
|
7424
8112
|
pass("node9.config.json found and valid (project)");
|
|
7425
8113
|
} catch {
|
|
7426
8114
|
fail(
|
|
@@ -7429,8 +8117,8 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7429
8117
|
);
|
|
7430
8118
|
}
|
|
7431
8119
|
}
|
|
7432
|
-
const credsPath =
|
|
7433
|
-
if (
|
|
8120
|
+
const credsPath = import_path21.default.join(homeDir2, ".node9", "credentials.json");
|
|
8121
|
+
if (import_fs19.default.existsSync(credsPath)) {
|
|
7434
8122
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
7435
8123
|
} else {
|
|
7436
8124
|
warn(
|
|
@@ -7439,10 +8127,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7439
8127
|
);
|
|
7440
8128
|
}
|
|
7441
8129
|
section("Agent Hooks");
|
|
7442
|
-
const claudeSettingsPath =
|
|
7443
|
-
if (
|
|
8130
|
+
const claudeSettingsPath = import_path21.default.join(homeDir2, ".claude", "settings.json");
|
|
8131
|
+
if (import_fs19.default.existsSync(claudeSettingsPath)) {
|
|
7444
8132
|
try {
|
|
7445
|
-
const cs = JSON.parse(
|
|
8133
|
+
const cs = JSON.parse(import_fs19.default.readFileSync(claudeSettingsPath, "utf-8"));
|
|
7446
8134
|
const hasHook = cs.hooks?.PreToolUse?.some(
|
|
7447
8135
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
7448
8136
|
);
|
|
@@ -7458,10 +8146,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7458
8146
|
} else {
|
|
7459
8147
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
7460
8148
|
}
|
|
7461
|
-
const geminiSettingsPath =
|
|
7462
|
-
if (
|
|
8149
|
+
const geminiSettingsPath = import_path21.default.join(homeDir2, ".gemini", "settings.json");
|
|
8150
|
+
if (import_fs19.default.existsSync(geminiSettingsPath)) {
|
|
7463
8151
|
try {
|
|
7464
|
-
const gs = JSON.parse(
|
|
8152
|
+
const gs = JSON.parse(import_fs19.default.readFileSync(geminiSettingsPath, "utf-8"));
|
|
7465
8153
|
const hasHook = gs.hooks?.BeforeTool?.some(
|
|
7466
8154
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
7467
8155
|
);
|
|
@@ -7477,10 +8165,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7477
8165
|
} else {
|
|
7478
8166
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
7479
8167
|
}
|
|
7480
|
-
const cursorHooksPath =
|
|
7481
|
-
if (
|
|
8168
|
+
const cursorHooksPath = import_path21.default.join(homeDir2, ".cursor", "hooks.json");
|
|
8169
|
+
if (import_fs19.default.existsSync(cursorHooksPath)) {
|
|
7482
8170
|
try {
|
|
7483
|
-
const cur = JSON.parse(
|
|
8171
|
+
const cur = JSON.parse(import_fs19.default.readFileSync(cursorHooksPath, "utf-8"));
|
|
7484
8172
|
const hasHook = cur.hooks?.preToolUse?.some(
|
|
7485
8173
|
(h) => h.command?.includes("node9") || h.command?.includes("cli.js")
|
|
7486
8174
|
);
|
|
@@ -7518,9 +8206,9 @@ function registerDoctorCommand(program2, version2) {
|
|
|
7518
8206
|
|
|
7519
8207
|
// src/cli/commands/audit.ts
|
|
7520
8208
|
var import_chalk8 = __toESM(require("chalk"));
|
|
7521
|
-
var
|
|
7522
|
-
var
|
|
7523
|
-
var
|
|
8209
|
+
var import_fs20 = __toESM(require("fs"));
|
|
8210
|
+
var import_path22 = __toESM(require("path"));
|
|
8211
|
+
var import_os18 = __toESM(require("os"));
|
|
7524
8212
|
function formatRelativeTime(timestamp) {
|
|
7525
8213
|
const diff = Date.now() - new Date(timestamp).getTime();
|
|
7526
8214
|
const sec = Math.floor(diff / 1e3);
|
|
@@ -7533,14 +8221,14 @@ function formatRelativeTime(timestamp) {
|
|
|
7533
8221
|
}
|
|
7534
8222
|
function registerAuditCommand(program2) {
|
|
7535
8223
|
program2.command("audit").description("View local execution audit log").option("--tail <n>", "Number of entries to show", "20").option("--tool <pattern>", "Filter by tool name (substring match)").option("--deny", "Show only denied actions").option("--json", "Output raw JSON").action((options) => {
|
|
7536
|
-
const logPath =
|
|
7537
|
-
if (!
|
|
8224
|
+
const logPath = import_path22.default.join(import_os18.default.homedir(), ".node9", "audit.log");
|
|
8225
|
+
if (!import_fs20.default.existsSync(logPath)) {
|
|
7538
8226
|
console.log(
|
|
7539
8227
|
import_chalk8.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
7540
8228
|
);
|
|
7541
8229
|
return;
|
|
7542
8230
|
}
|
|
7543
|
-
const raw =
|
|
8231
|
+
const raw = import_fs20.default.readFileSync(logPath, "utf-8");
|
|
7544
8232
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
7545
8233
|
let entries = lines.flatMap((line) => {
|
|
7546
8234
|
try {
|
|
@@ -7658,11 +8346,44 @@ function registerDaemonCommand(program2) {
|
|
|
7658
8346
|
|
|
7659
8347
|
// src/cli/commands/status.ts
|
|
7660
8348
|
var import_chalk10 = __toESM(require("chalk"));
|
|
7661
|
-
var
|
|
7662
|
-
var
|
|
7663
|
-
var
|
|
8349
|
+
var import_fs21 = __toESM(require("fs"));
|
|
8350
|
+
var import_path23 = __toESM(require("path"));
|
|
8351
|
+
var import_os19 = __toESM(require("os"));
|
|
7664
8352
|
init_core();
|
|
7665
8353
|
init_daemon();
|
|
8354
|
+
function readJson2(filePath) {
|
|
8355
|
+
try {
|
|
8356
|
+
if (import_fs21.default.existsSync(filePath)) return JSON.parse(import_fs21.default.readFileSync(filePath, "utf-8"));
|
|
8357
|
+
} catch {
|
|
8358
|
+
}
|
|
8359
|
+
return null;
|
|
8360
|
+
}
|
|
8361
|
+
function isNode9Hook2(cmd) {
|
|
8362
|
+
if (!cmd) return false;
|
|
8363
|
+
return /(?:^|[\s/\\])node9 (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js (?:check|log)/.test(cmd);
|
|
8364
|
+
}
|
|
8365
|
+
function wrappedMcpServers(servers) {
|
|
8366
|
+
if (!servers) return [];
|
|
8367
|
+
return Object.entries(servers).filter(([, s]) => s.command === "node9" && Array.isArray(s.args) && s.args.length > 0).map(([name, s]) => `${name} \u2192 ${s.args.join(" ")}`);
|
|
8368
|
+
}
|
|
8369
|
+
function printAgentSection(label, hookPairs, wrapped) {
|
|
8370
|
+
console.log(import_chalk10.default.bold(` ${label}`));
|
|
8371
|
+
for (const { name, present } of hookPairs) {
|
|
8372
|
+
if (present) {
|
|
8373
|
+
console.log(import_chalk10.default.green(` \u2713 ${name}`));
|
|
8374
|
+
} else {
|
|
8375
|
+
console.log(import_chalk10.default.red(` \u2717 ${name}`) + import_chalk10.default.gray(" (not wired)"));
|
|
8376
|
+
}
|
|
8377
|
+
}
|
|
8378
|
+
if (wrapped.length > 0) {
|
|
8379
|
+
console.log(import_chalk10.default.cyan(` MCP proxied:`));
|
|
8380
|
+
for (const entry of wrapped) {
|
|
8381
|
+
console.log(import_chalk10.default.gray(` \u2022 ${entry}`));
|
|
8382
|
+
}
|
|
8383
|
+
} else {
|
|
8384
|
+
console.log(import_chalk10.default.gray(` MCP proxied: none`));
|
|
8385
|
+
}
|
|
8386
|
+
}
|
|
7666
8387
|
function registerStatusCommand(program2) {
|
|
7667
8388
|
program2.command("status").description("Show current Node9 mode, policy source, and persistent decisions").action(() => {
|
|
7668
8389
|
const creds = getCredentials();
|
|
@@ -7697,19 +8418,72 @@ function registerStatusCommand(program2) {
|
|
|
7697
8418
|
console.log("");
|
|
7698
8419
|
const modeLabel = settings.mode === "audit" ? import_chalk10.default.blue("audit") : settings.mode === "strict" ? import_chalk10.default.red("strict") : import_chalk10.default.white("standard");
|
|
7699
8420
|
console.log(` Mode: ${modeLabel}`);
|
|
7700
|
-
const projectConfig =
|
|
7701
|
-
const globalConfig =
|
|
8421
|
+
const projectConfig = import_path23.default.join(process.cwd(), "node9.config.json");
|
|
8422
|
+
const globalConfig = import_path23.default.join(import_os19.default.homedir(), ".node9", "config.json");
|
|
7702
8423
|
console.log(
|
|
7703
|
-
` Local: ${
|
|
8424
|
+
` Local: ${import_fs21.default.existsSync(projectConfig) ? import_chalk10.default.green("Active (node9.config.json)") : import_chalk10.default.gray("Not present")}`
|
|
7704
8425
|
);
|
|
7705
8426
|
console.log(
|
|
7706
|
-
` Global: ${
|
|
8427
|
+
` Global: ${import_fs21.default.existsSync(globalConfig) ? import_chalk10.default.green("Active (~/.node9/config.json)") : import_chalk10.default.gray("Not present")}`
|
|
7707
8428
|
);
|
|
7708
8429
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
7709
8430
|
console.log(
|
|
7710
8431
|
` Sandbox: ${import_chalk10.default.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
7711
8432
|
);
|
|
7712
8433
|
}
|
|
8434
|
+
const homeDir2 = import_os19.default.homedir();
|
|
8435
|
+
const claudeSettings = readJson2(
|
|
8436
|
+
import_path23.default.join(homeDir2, ".claude", "settings.json")
|
|
8437
|
+
);
|
|
8438
|
+
const claudeConfig = readJson2(import_path23.default.join(homeDir2, ".claude.json"));
|
|
8439
|
+
const geminiSettings = readJson2(
|
|
8440
|
+
import_path23.default.join(homeDir2, ".gemini", "settings.json")
|
|
8441
|
+
);
|
|
8442
|
+
const cursorConfig = readJson2(import_path23.default.join(homeDir2, ".cursor", "mcp.json"));
|
|
8443
|
+
const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
|
|
8444
|
+
if (agentFound) {
|
|
8445
|
+
console.log("");
|
|
8446
|
+
console.log(import_chalk10.default.bold(" Agent Wiring:"));
|
|
8447
|
+
console.log("");
|
|
8448
|
+
if (claudeSettings || claudeConfig) {
|
|
8449
|
+
const preHook = claudeSettings?.hooks?.PreToolUse?.some(
|
|
8450
|
+
(m) => m.hooks.some((h) => isNode9Hook2(h.command))
|
|
8451
|
+
) ?? false;
|
|
8452
|
+
const postHook = claudeSettings?.hooks?.PostToolUse?.some(
|
|
8453
|
+
(m) => m.hooks.some((h) => isNode9Hook2(h.command))
|
|
8454
|
+
) ?? false;
|
|
8455
|
+
printAgentSection(
|
|
8456
|
+
"Claude Code",
|
|
8457
|
+
[
|
|
8458
|
+
{ name: "PreToolUse (node9 check)", present: preHook },
|
|
8459
|
+
{ name: "PostToolUse (node9 log)", present: postHook }
|
|
8460
|
+
],
|
|
8461
|
+
wrappedMcpServers(claudeConfig?.mcpServers)
|
|
8462
|
+
);
|
|
8463
|
+
console.log("");
|
|
8464
|
+
}
|
|
8465
|
+
if (geminiSettings) {
|
|
8466
|
+
const beforeHook = geminiSettings.hooks?.BeforeTool?.some(
|
|
8467
|
+
(m) => m.hooks.some((h) => isNode9Hook2(h.command))
|
|
8468
|
+
) ?? false;
|
|
8469
|
+
const afterHook = geminiSettings.hooks?.AfterTool?.some(
|
|
8470
|
+
(m) => m.hooks.some((h) => isNode9Hook2(h.command))
|
|
8471
|
+
) ?? false;
|
|
8472
|
+
printAgentSection(
|
|
8473
|
+
"Gemini CLI",
|
|
8474
|
+
[
|
|
8475
|
+
{ name: "BeforeTool (node9 check)", present: beforeHook },
|
|
8476
|
+
{ name: "AfterTool (node9 log)", present: afterHook }
|
|
8477
|
+
],
|
|
8478
|
+
wrappedMcpServers(geminiSettings.mcpServers)
|
|
8479
|
+
);
|
|
8480
|
+
console.log("");
|
|
8481
|
+
}
|
|
8482
|
+
if (cursorConfig) {
|
|
8483
|
+
printAgentSection("Cursor", [], wrappedMcpServers(cursorConfig.mcpServers));
|
|
8484
|
+
console.log("");
|
|
8485
|
+
}
|
|
8486
|
+
}
|
|
7713
8487
|
const pauseState = checkPause();
|
|
7714
8488
|
if (pauseState.paused) {
|
|
7715
8489
|
const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
|
|
@@ -7722,8 +8496,63 @@ function registerStatusCommand(program2) {
|
|
|
7722
8496
|
});
|
|
7723
8497
|
}
|
|
7724
8498
|
|
|
7725
|
-
// src/cli/commands/
|
|
8499
|
+
// src/cli/commands/init.ts
|
|
7726
8500
|
var import_chalk11 = __toESM(require("chalk"));
|
|
8501
|
+
var import_fs22 = __toESM(require("fs"));
|
|
8502
|
+
var import_path24 = __toESM(require("path"));
|
|
8503
|
+
var import_os20 = __toESM(require("os"));
|
|
8504
|
+
init_core();
|
|
8505
|
+
function registerInitCommand(program2) {
|
|
8506
|
+
program2.command("init").description("Set up Node9: create config and wire all detected AI agents").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").option("--skip-setup", "Only create config \u2014 do not wire AI agents").action(async (options) => {
|
|
8507
|
+
console.log(import_chalk11.default.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
|
|
8508
|
+
const configPath = import_path24.default.join(import_os20.default.homedir(), ".node9", "config.json");
|
|
8509
|
+
if (import_fs22.default.existsSync(configPath) && !options.force) {
|
|
8510
|
+
console.log(import_chalk11.default.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
8511
|
+
} else {
|
|
8512
|
+
const requestedMode = options.mode.toLowerCase();
|
|
8513
|
+
const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
|
|
8514
|
+
const configToSave = {
|
|
8515
|
+
...DEFAULT_CONFIG,
|
|
8516
|
+
settings: { ...DEFAULT_CONFIG.settings, mode: safeMode }
|
|
8517
|
+
};
|
|
8518
|
+
const dir = import_path24.default.dirname(configPath);
|
|
8519
|
+
if (!import_fs22.default.existsSync(dir)) import_fs22.default.mkdirSync(dir, { recursive: true });
|
|
8520
|
+
import_fs22.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
8521
|
+
console.log(import_chalk11.default.green(`\u2705 Config created: ${configPath}`));
|
|
8522
|
+
console.log(import_chalk11.default.gray(` Mode: ${safeMode}`));
|
|
8523
|
+
}
|
|
8524
|
+
if (options.skipSetup) return;
|
|
8525
|
+
console.log("");
|
|
8526
|
+
const detected = detectAgents();
|
|
8527
|
+
const found = Object.keys(detected).filter(
|
|
8528
|
+
(k) => detected[k]
|
|
8529
|
+
);
|
|
8530
|
+
if (found.length === 0) {
|
|
8531
|
+
console.log(
|
|
8532
|
+
import_chalk11.default.gray("No AI agents detected. Install Claude Code, Gemini CLI, or Cursor")
|
|
8533
|
+
);
|
|
8534
|
+
console.log(import_chalk11.default.gray("then run: node9 addto <claude|gemini|cursor>"));
|
|
8535
|
+
return;
|
|
8536
|
+
}
|
|
8537
|
+
console.log(import_chalk11.default.bold("Detected agents:"));
|
|
8538
|
+
for (const agent of found) {
|
|
8539
|
+
console.log(import_chalk11.default.green(` \u2713 ${agent}`));
|
|
8540
|
+
}
|
|
8541
|
+
console.log("");
|
|
8542
|
+
for (const agent of found) {
|
|
8543
|
+
console.log(import_chalk11.default.bold(`Wiring ${agent}...`));
|
|
8544
|
+
if (agent === "claude") await setupClaude();
|
|
8545
|
+
else if (agent === "gemini") await setupGemini();
|
|
8546
|
+
else if (agent === "cursor") await setupCursor();
|
|
8547
|
+
console.log("");
|
|
8548
|
+
}
|
|
8549
|
+
console.log(import_chalk11.default.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
|
|
8550
|
+
console.log(import_chalk11.default.gray(" Run: node9 daemon start"));
|
|
8551
|
+
});
|
|
8552
|
+
}
|
|
8553
|
+
|
|
8554
|
+
// src/cli/commands/undo.ts
|
|
8555
|
+
var import_chalk12 = __toESM(require("chalk"));
|
|
7727
8556
|
var import_prompts2 = require("@inquirer/prompts");
|
|
7728
8557
|
function registerUndoCommand(program2) {
|
|
7729
8558
|
program2.command("undo").description(
|
|
@@ -7735,22 +8564,22 @@ function registerUndoCommand(program2) {
|
|
|
7735
8564
|
if (history.length === 0) {
|
|
7736
8565
|
if (!options.all && allHistory.length > 0) {
|
|
7737
8566
|
console.log(
|
|
7738
|
-
|
|
8567
|
+
import_chalk12.default.yellow(
|
|
7739
8568
|
`
|
|
7740
8569
|
\u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
|
|
7741
|
-
Run ${
|
|
8570
|
+
Run ${import_chalk12.default.cyan("node9 undo --all")} to see snapshots from all projects.
|
|
7742
8571
|
`
|
|
7743
8572
|
)
|
|
7744
8573
|
);
|
|
7745
8574
|
} else {
|
|
7746
|
-
console.log(
|
|
8575
|
+
console.log(import_chalk12.default.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
|
|
7747
8576
|
}
|
|
7748
8577
|
return;
|
|
7749
8578
|
}
|
|
7750
8579
|
const idx = history.length - steps;
|
|
7751
8580
|
if (idx < 0) {
|
|
7752
8581
|
console.log(
|
|
7753
|
-
|
|
8582
|
+
import_chalk12.default.yellow(
|
|
7754
8583
|
`
|
|
7755
8584
|
\u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
|
|
7756
8585
|
`
|
|
@@ -7762,19 +8591,19 @@ function registerUndoCommand(program2) {
|
|
|
7762
8591
|
const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
|
|
7763
8592
|
const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
|
|
7764
8593
|
console.log(
|
|
7765
|
-
|
|
8594
|
+
import_chalk12.default.magenta.bold(`
|
|
7766
8595
|
\u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`)
|
|
7767
8596
|
);
|
|
7768
8597
|
console.log(
|
|
7769
|
-
|
|
7770
|
-
` Tool: ${
|
|
8598
|
+
import_chalk12.default.white(
|
|
8599
|
+
` Tool: ${import_chalk12.default.cyan(snapshot.tool)}${snapshot.argsSummary ? import_chalk12.default.gray(" \u2192 " + snapshot.argsSummary) : ""}`
|
|
7771
8600
|
)
|
|
7772
8601
|
);
|
|
7773
|
-
console.log(
|
|
7774
|
-
console.log(
|
|
8602
|
+
console.log(import_chalk12.default.white(` When: ${import_chalk12.default.gray(ageStr)}`));
|
|
8603
|
+
console.log(import_chalk12.default.white(` Dir: ${import_chalk12.default.gray(snapshot.cwd)}`));
|
|
7775
8604
|
if (steps > 1)
|
|
7776
8605
|
console.log(
|
|
7777
|
-
|
|
8606
|
+
import_chalk12.default.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
|
|
7778
8607
|
);
|
|
7779
8608
|
console.log("");
|
|
7780
8609
|
const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
|
|
@@ -7782,21 +8611,21 @@ function registerUndoCommand(program2) {
|
|
|
7782
8611
|
const lines = diff.split("\n");
|
|
7783
8612
|
for (const line of lines) {
|
|
7784
8613
|
if (line.startsWith("+++") || line.startsWith("---")) {
|
|
7785
|
-
console.log(
|
|
8614
|
+
console.log(import_chalk12.default.bold(line));
|
|
7786
8615
|
} else if (line.startsWith("+")) {
|
|
7787
|
-
console.log(
|
|
8616
|
+
console.log(import_chalk12.default.green(line));
|
|
7788
8617
|
} else if (line.startsWith("-")) {
|
|
7789
|
-
console.log(
|
|
8618
|
+
console.log(import_chalk12.default.red(line));
|
|
7790
8619
|
} else if (line.startsWith("@@")) {
|
|
7791
|
-
console.log(
|
|
8620
|
+
console.log(import_chalk12.default.cyan(line));
|
|
7792
8621
|
} else {
|
|
7793
|
-
console.log(
|
|
8622
|
+
console.log(import_chalk12.default.gray(line));
|
|
7794
8623
|
}
|
|
7795
8624
|
}
|
|
7796
8625
|
console.log("");
|
|
7797
8626
|
} else {
|
|
7798
8627
|
console.log(
|
|
7799
|
-
|
|
8628
|
+
import_chalk12.default.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
|
|
7800
8629
|
);
|
|
7801
8630
|
}
|
|
7802
8631
|
const proceed = await (0, import_prompts2.confirm)({
|
|
@@ -7805,18 +8634,18 @@ function registerUndoCommand(program2) {
|
|
|
7805
8634
|
});
|
|
7806
8635
|
if (proceed) {
|
|
7807
8636
|
if (applyUndo(snapshot.hash, snapshot.cwd)) {
|
|
7808
|
-
console.log(
|
|
8637
|
+
console.log(import_chalk12.default.green("\n\u2705 Reverted successfully.\n"));
|
|
7809
8638
|
} else {
|
|
7810
|
-
console.error(
|
|
8639
|
+
console.error(import_chalk12.default.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
|
|
7811
8640
|
}
|
|
7812
8641
|
} else {
|
|
7813
|
-
console.log(
|
|
8642
|
+
console.log(import_chalk12.default.gray("\nCancelled.\n"));
|
|
7814
8643
|
}
|
|
7815
8644
|
});
|
|
7816
8645
|
}
|
|
7817
8646
|
|
|
7818
8647
|
// src/cli/commands/watch.ts
|
|
7819
|
-
var
|
|
8648
|
+
var import_chalk13 = __toESM(require("chalk"));
|
|
7820
8649
|
var import_child_process11 = require("child_process");
|
|
7821
8650
|
init_daemon();
|
|
7822
8651
|
function registerWatchCommand(program2) {
|
|
@@ -7833,7 +8662,7 @@ function registerWatchCommand(program2) {
|
|
|
7833
8662
|
throw new Error("not running");
|
|
7834
8663
|
}
|
|
7835
8664
|
} catch {
|
|
7836
|
-
console.error(
|
|
8665
|
+
console.error(import_chalk13.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
|
|
7837
8666
|
const child = (0, import_child_process11.spawn)(process.execPath, [process.argv[1], "daemon"], {
|
|
7838
8667
|
detached: true,
|
|
7839
8668
|
stdio: "ignore",
|
|
@@ -7855,12 +8684,12 @@ function registerWatchCommand(program2) {
|
|
|
7855
8684
|
}
|
|
7856
8685
|
}
|
|
7857
8686
|
if (!ready) {
|
|
7858
|
-
console.error(
|
|
8687
|
+
console.error(import_chalk13.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
7859
8688
|
process.exit(1);
|
|
7860
8689
|
}
|
|
7861
8690
|
}
|
|
7862
8691
|
console.error(
|
|
7863
|
-
|
|
8692
|
+
import_chalk13.default.cyan.bold("\u{1F6E1}\uFE0F Node9 watch") + import_chalk13.default.dim(` \u2192 localhost:${port}`) + import_chalk13.default.dim(
|
|
7864
8693
|
"\n Tip: run `node9 tail` in another terminal to review and approve AI actions.\n"
|
|
7865
8694
|
)
|
|
7866
8695
|
);
|
|
@@ -7869,7 +8698,7 @@ function registerWatchCommand(program2) {
|
|
|
7869
8698
|
env: { ...process.env, NODE9_WATCH_MODE: "1" }
|
|
7870
8699
|
});
|
|
7871
8700
|
if (result.error) {
|
|
7872
|
-
console.error(
|
|
8701
|
+
console.error(import_chalk13.default.red(`\u274C Failed to run command: ${result.error.message}`));
|
|
7873
8702
|
process.exit(1);
|
|
7874
8703
|
}
|
|
7875
8704
|
process.exit(result.status ?? 0);
|
|
@@ -7878,7 +8707,7 @@ function registerWatchCommand(program2) {
|
|
|
7878
8707
|
|
|
7879
8708
|
// src/mcp-gateway/index.ts
|
|
7880
8709
|
var import_readline2 = __toESM(require("readline"));
|
|
7881
|
-
var
|
|
8710
|
+
var import_chalk14 = __toESM(require("chalk"));
|
|
7882
8711
|
var import_child_process12 = require("child_process");
|
|
7883
8712
|
var import_execa3 = require("execa");
|
|
7884
8713
|
init_orchestrator();
|
|
@@ -7942,13 +8771,13 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
7942
8771
|
const prov = checkProvenance(executable);
|
|
7943
8772
|
if (prov.trustLevel === "suspect") {
|
|
7944
8773
|
console.error(
|
|
7945
|
-
|
|
8774
|
+
import_chalk14.default.red(
|
|
7946
8775
|
`\u26A0\uFE0F Node9: Upstream MCP server binary is suspect \u2014 ${prov.reason} (${prov.resolvedPath})`
|
|
7947
8776
|
)
|
|
7948
8777
|
);
|
|
7949
|
-
console.error(
|
|
8778
|
+
console.error(import_chalk14.default.red(" Verify this binary is trusted before proceeding."));
|
|
7950
8779
|
}
|
|
7951
|
-
console.error(
|
|
8780
|
+
console.error(import_chalk14.default.green(`\u{1F680} Node9 MCP Gateway: Monitoring [${upstreamCommand}]`));
|
|
7952
8781
|
const UPSTREAM_INJECTOR_VARS = /* @__PURE__ */ new Set([
|
|
7953
8782
|
"NODE_OPTIONS",
|
|
7954
8783
|
"NODE_PATH",
|
|
@@ -8012,10 +8841,10 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
8012
8841
|
mcpServer
|
|
8013
8842
|
});
|
|
8014
8843
|
if (!result.approved) {
|
|
8015
|
-
console.error(
|
|
8844
|
+
console.error(import_chalk14.default.red(`
|
|
8016
8845
|
\u{1F6D1} Node9 MCP Gateway: Action Blocked`));
|
|
8017
|
-
console.error(
|
|
8018
|
-
console.error(
|
|
8846
|
+
console.error(import_chalk14.default.gray(` Tool: ${toolName}`));
|
|
8847
|
+
console.error(import_chalk14.default.gray(` Reason: ${result.reason ?? "Security Policy"}
|
|
8019
8848
|
`));
|
|
8020
8849
|
const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
|
|
8021
8850
|
const isHumanDecision = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
|
|
@@ -8087,22 +8916,77 @@ function registerMcpGatewayCommand(program2) {
|
|
|
8087
8916
|
});
|
|
8088
8917
|
}
|
|
8089
8918
|
|
|
8919
|
+
// src/cli/commands/trust.ts
|
|
8920
|
+
var import_chalk15 = __toESM(require("chalk"));
|
|
8921
|
+
init_trusted_hosts();
|
|
8922
|
+
function isValidHost(host) {
|
|
8923
|
+
return /^(\*\.)?[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/.test(host);
|
|
8924
|
+
}
|
|
8925
|
+
function registerTrustCommand(program2) {
|
|
8926
|
+
const trustCmd = program2.command("trust").description("Manage trusted network hosts (reduces approval friction for known destinations)");
|
|
8927
|
+
trustCmd.command("add <host>").description("Add a trusted host \u2014 pipe-chain blocks targeting this host are downgraded").action((host) => {
|
|
8928
|
+
const normalized = normalizeHost(host.trim());
|
|
8929
|
+
if (!isValidHost(normalized)) {
|
|
8930
|
+
console.error(
|
|
8931
|
+
import_chalk15.default.red(`
|
|
8932
|
+
\u274C Invalid host: "${host}"
|
|
8933
|
+
`) + import_chalk15.default.gray(" Use an FQDN like api.mycompany.com or *.mycompany.com\n")
|
|
8934
|
+
);
|
|
8935
|
+
process.exit(1);
|
|
8936
|
+
}
|
|
8937
|
+
addTrustedHost(normalized);
|
|
8938
|
+
console.log(import_chalk15.default.green(`
|
|
8939
|
+
\u2705 ${normalized} added to trusted hosts.`));
|
|
8940
|
+
console.log(
|
|
8941
|
+
import_chalk15.default.gray(" Pipe-chain blocks to this host: critical \u2192 review, high \u2192 allow\n")
|
|
8942
|
+
);
|
|
8943
|
+
});
|
|
8944
|
+
trustCmd.command("remove <host>").description("Remove a trusted host").action((host) => {
|
|
8945
|
+
const normalized = normalizeHost(host.trim());
|
|
8946
|
+
const removed = removeTrustedHost(normalized);
|
|
8947
|
+
if (!removed) {
|
|
8948
|
+
console.error(import_chalk15.default.yellow(`
|
|
8949
|
+
\u26A0\uFE0F "${normalized}" is not in the trusted hosts list.
|
|
8950
|
+
`));
|
|
8951
|
+
process.exit(1);
|
|
8952
|
+
}
|
|
8953
|
+
console.log(import_chalk15.default.green(`
|
|
8954
|
+
\u2705 ${normalized} removed from trusted hosts.
|
|
8955
|
+
`));
|
|
8956
|
+
});
|
|
8957
|
+
trustCmd.command("list").description("Show all trusted hosts").action(() => {
|
|
8958
|
+
const hosts = readTrustedHosts();
|
|
8959
|
+
if (hosts.length === 0) {
|
|
8960
|
+
console.log(import_chalk15.default.gray("\n No trusted hosts configured.\n"));
|
|
8961
|
+
console.log(` Add one: ${import_chalk15.default.cyan("node9 trust add api.mycompany.com")}
|
|
8962
|
+
`);
|
|
8963
|
+
return;
|
|
8964
|
+
}
|
|
8965
|
+
console.log(import_chalk15.default.bold("\n\u{1F513} Trusted Hosts\n"));
|
|
8966
|
+
for (const entry of hosts) {
|
|
8967
|
+
const date = new Date(entry.addedAt).toLocaleDateString();
|
|
8968
|
+
console.log(` ${import_chalk15.default.cyan(entry.host.padEnd(40))} ${import_chalk15.default.gray(`added ${date}`)}`);
|
|
8969
|
+
}
|
|
8970
|
+
console.log("");
|
|
8971
|
+
});
|
|
8972
|
+
}
|
|
8973
|
+
|
|
8090
8974
|
// src/cli.ts
|
|
8091
8975
|
var { version } = JSON.parse(
|
|
8092
|
-
|
|
8976
|
+
import_fs24.default.readFileSync(import_path26.default.join(__dirname, "../package.json"), "utf-8")
|
|
8093
8977
|
);
|
|
8094
8978
|
var program = new import_commander.Command();
|
|
8095
8979
|
program.name("node9").description("The Sudo Command for AI Agents").version(version);
|
|
8096
8980
|
program.command("login").argument("<apiKey>").option("--local", "Save key for audit/logging only \u2014 local config still controls all decisions").option("--profile <name>", 'Save as a named profile (default: "default")').action((apiKey, options) => {
|
|
8097
8981
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
8098
|
-
const credPath =
|
|
8099
|
-
if (!
|
|
8100
|
-
|
|
8982
|
+
const credPath = import_path26.default.join(import_os22.default.homedir(), ".node9", "credentials.json");
|
|
8983
|
+
if (!import_fs24.default.existsSync(import_path26.default.dirname(credPath)))
|
|
8984
|
+
import_fs24.default.mkdirSync(import_path26.default.dirname(credPath), { recursive: true });
|
|
8101
8985
|
const profileName = options.profile || "default";
|
|
8102
8986
|
let existingCreds = {};
|
|
8103
8987
|
try {
|
|
8104
|
-
if (
|
|
8105
|
-
const raw = JSON.parse(
|
|
8988
|
+
if (import_fs24.default.existsSync(credPath)) {
|
|
8989
|
+
const raw = JSON.parse(import_fs24.default.readFileSync(credPath, "utf-8"));
|
|
8106
8990
|
if (raw.apiKey) {
|
|
8107
8991
|
existingCreds = {
|
|
8108
8992
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
|
|
@@ -8114,13 +8998,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
8114
8998
|
} catch {
|
|
8115
8999
|
}
|
|
8116
9000
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
8117
|
-
|
|
9001
|
+
import_fs24.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
8118
9002
|
if (profileName === "default") {
|
|
8119
|
-
const configPath =
|
|
9003
|
+
const configPath = import_path26.default.join(import_os22.default.homedir(), ".node9", "config.json");
|
|
8120
9004
|
let config = {};
|
|
8121
9005
|
try {
|
|
8122
|
-
if (
|
|
8123
|
-
config = JSON.parse(
|
|
9006
|
+
if (import_fs24.default.existsSync(configPath))
|
|
9007
|
+
config = JSON.parse(import_fs24.default.readFileSync(configPath, "utf-8"));
|
|
8124
9008
|
} catch {
|
|
8125
9009
|
}
|
|
8126
9010
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -8135,36 +9019,36 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
8135
9019
|
approvers.cloud = false;
|
|
8136
9020
|
}
|
|
8137
9021
|
s.approvers = approvers;
|
|
8138
|
-
if (!
|
|
8139
|
-
|
|
8140
|
-
|
|
9022
|
+
if (!import_fs24.default.existsSync(import_path26.default.dirname(configPath)))
|
|
9023
|
+
import_fs24.default.mkdirSync(import_path26.default.dirname(configPath), { recursive: true });
|
|
9024
|
+
import_fs24.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
8141
9025
|
}
|
|
8142
9026
|
if (options.profile && profileName !== "default") {
|
|
8143
|
-
console.log(
|
|
8144
|
-
console.log(
|
|
9027
|
+
console.log(import_chalk17.default.green(`\u2705 Profile "${profileName}" saved`));
|
|
9028
|
+
console.log(import_chalk17.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
8145
9029
|
} else if (options.local) {
|
|
8146
|
-
console.log(
|
|
8147
|
-
console.log(
|
|
9030
|
+
console.log(import_chalk17.default.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
9031
|
+
console.log(import_chalk17.default.gray(` All decisions stay on this machine.`));
|
|
8148
9032
|
} else {
|
|
8149
|
-
console.log(
|
|
8150
|
-
console.log(
|
|
9033
|
+
console.log(import_chalk17.default.green(`\u2705 Logged in \u2014 agent mode`));
|
|
9034
|
+
console.log(import_chalk17.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
8151
9035
|
}
|
|
8152
9036
|
});
|
|
8153
9037
|
program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to protect: claude | gemini | cursor").action(async (target) => {
|
|
8154
9038
|
if (target === "gemini") return await setupGemini();
|
|
8155
9039
|
if (target === "claude") return await setupClaude();
|
|
8156
9040
|
if (target === "cursor") return await setupCursor();
|
|
8157
|
-
console.error(
|
|
9041
|
+
console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
8158
9042
|
process.exit(1);
|
|
8159
9043
|
});
|
|
8160
9044
|
program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor").argument("[target]", "The agent to protect: claude | gemini | cursor").action(async (target) => {
|
|
8161
9045
|
if (!target) {
|
|
8162
|
-
console.log(
|
|
8163
|
-
console.log(" Usage: " +
|
|
9046
|
+
console.log(import_chalk17.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
9047
|
+
console.log(" Usage: " + import_chalk17.default.white("node9 setup <target>") + "\n");
|
|
8164
9048
|
console.log(" Targets:");
|
|
8165
|
-
console.log(" " +
|
|
8166
|
-
console.log(" " +
|
|
8167
|
-
console.log(" " +
|
|
9049
|
+
console.log(" " + import_chalk17.default.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
9050
|
+
console.log(" " + import_chalk17.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
9051
|
+
console.log(" " + import_chalk17.default.green("cursor") + " \u2014 Cursor (hook mode)");
|
|
8168
9052
|
console.log("");
|
|
8169
9053
|
return;
|
|
8170
9054
|
}
|
|
@@ -8172,7 +9056,7 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
8172
9056
|
if (t === "gemini") return await setupGemini();
|
|
8173
9057
|
if (t === "claude") return await setupClaude();
|
|
8174
9058
|
if (t === "cursor") return await setupCursor();
|
|
8175
|
-
console.error(
|
|
9059
|
+
console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
8176
9060
|
process.exit(1);
|
|
8177
9061
|
});
|
|
8178
9062
|
program.command("removefrom").description("Remove Node9 hooks from an AI agent configuration").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to remove from: claude | gemini | cursor").action((target) => {
|
|
@@ -8181,30 +9065,30 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
|
|
|
8181
9065
|
else if (target === "gemini") fn = teardownGemini;
|
|
8182
9066
|
else if (target === "cursor") fn = teardownCursor;
|
|
8183
9067
|
else {
|
|
8184
|
-
console.error(
|
|
9068
|
+
console.error(import_chalk17.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
8185
9069
|
process.exit(1);
|
|
8186
9070
|
}
|
|
8187
|
-
console.log(
|
|
9071
|
+
console.log(import_chalk17.default.cyan(`
|
|
8188
9072
|
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
8189
9073
|
`));
|
|
8190
9074
|
try {
|
|
8191
9075
|
fn();
|
|
8192
9076
|
} catch (err) {
|
|
8193
|
-
console.error(
|
|
9077
|
+
console.error(import_chalk17.default.red(` \u26A0\uFE0F Failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
8194
9078
|
process.exit(1);
|
|
8195
9079
|
}
|
|
8196
|
-
console.log(
|
|
9080
|
+
console.log(import_chalk17.default.gray("\n Restart the agent for changes to take effect."));
|
|
8197
9081
|
});
|
|
8198
9082
|
program.command("uninstall").description("Remove all Node9 hooks and optionally delete config files").option("--purge", "Also delete ~/.node9/ directory (config, audit log, credentials)").action(async (options) => {
|
|
8199
|
-
console.log(
|
|
8200
|
-
console.log(
|
|
9083
|
+
console.log(import_chalk17.default.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
9084
|
+
console.log(import_chalk17.default.bold("Stopping daemon..."));
|
|
8201
9085
|
try {
|
|
8202
9086
|
stopDaemon();
|
|
8203
|
-
console.log(
|
|
9087
|
+
console.log(import_chalk17.default.green(" \u2705 Daemon stopped"));
|
|
8204
9088
|
} catch {
|
|
8205
|
-
console.log(
|
|
9089
|
+
console.log(import_chalk17.default.blue(" \u2139\uFE0F Daemon was not running"));
|
|
8206
9090
|
}
|
|
8207
|
-
console.log(
|
|
9091
|
+
console.log(import_chalk17.default.bold("\nRemoving hooks..."));
|
|
8208
9092
|
let teardownFailed = false;
|
|
8209
9093
|
for (const [label, fn] of [
|
|
8210
9094
|
["Claude", teardownClaude],
|
|
@@ -8216,45 +9100,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
8216
9100
|
} catch (err) {
|
|
8217
9101
|
teardownFailed = true;
|
|
8218
9102
|
console.error(
|
|
8219
|
-
|
|
9103
|
+
import_chalk17.default.red(
|
|
8220
9104
|
` \u26A0\uFE0F Failed to remove ${label} hooks: ${err instanceof Error ? err.message : String(err)}`
|
|
8221
9105
|
)
|
|
8222
9106
|
);
|
|
8223
9107
|
}
|
|
8224
9108
|
}
|
|
8225
9109
|
if (options.purge) {
|
|
8226
|
-
const node9Dir =
|
|
8227
|
-
if (
|
|
9110
|
+
const node9Dir = import_path26.default.join(import_os22.default.homedir(), ".node9");
|
|
9111
|
+
if (import_fs24.default.existsSync(node9Dir)) {
|
|
8228
9112
|
const confirmed = await (0, import_prompts3.confirm)({
|
|
8229
9113
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
8230
9114
|
default: false
|
|
8231
9115
|
});
|
|
8232
9116
|
if (confirmed) {
|
|
8233
|
-
|
|
8234
|
-
if (
|
|
9117
|
+
import_fs24.default.rmSync(node9Dir, { recursive: true });
|
|
9118
|
+
if (import_fs24.default.existsSync(node9Dir)) {
|
|
8235
9119
|
console.error(
|
|
8236
|
-
|
|
9120
|
+
import_chalk17.default.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
8237
9121
|
);
|
|
8238
9122
|
} else {
|
|
8239
|
-
console.log(
|
|
9123
|
+
console.log(import_chalk17.default.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
8240
9124
|
}
|
|
8241
9125
|
} else {
|
|
8242
|
-
console.log(
|
|
9126
|
+
console.log(import_chalk17.default.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
8243
9127
|
}
|
|
8244
9128
|
} else {
|
|
8245
|
-
console.log(
|
|
9129
|
+
console.log(import_chalk17.default.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
8246
9130
|
}
|
|
8247
9131
|
} else {
|
|
8248
9132
|
console.log(
|
|
8249
|
-
|
|
9133
|
+
import_chalk17.default.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
8250
9134
|
);
|
|
8251
9135
|
}
|
|
8252
9136
|
if (teardownFailed) {
|
|
8253
|
-
console.error(
|
|
9137
|
+
console.error(import_chalk17.default.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
8254
9138
|
process.exit(1);
|
|
8255
9139
|
}
|
|
8256
|
-
console.log(
|
|
8257
|
-
console.log(
|
|
9140
|
+
console.log(import_chalk17.default.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g @node9/proxy"));
|
|
9141
|
+
console.log(import_chalk17.default.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
|
|
8258
9142
|
});
|
|
8259
9143
|
registerDoctorCommand(program, version);
|
|
8260
9144
|
program.command("explain").description(
|
|
@@ -8267,7 +9151,7 @@ program.command("explain").description(
|
|
|
8267
9151
|
try {
|
|
8268
9152
|
args = JSON.parse(trimmed);
|
|
8269
9153
|
} catch {
|
|
8270
|
-
console.error(
|
|
9154
|
+
console.error(import_chalk17.default.red(`
|
|
8271
9155
|
\u274C Invalid JSON: ${trimmed}
|
|
8272
9156
|
`));
|
|
8273
9157
|
process.exit(1);
|
|
@@ -8278,83 +9162,59 @@ program.command("explain").description(
|
|
|
8278
9162
|
}
|
|
8279
9163
|
const result = await explainPolicy(tool, args);
|
|
8280
9164
|
console.log("");
|
|
8281
|
-
console.log(
|
|
9165
|
+
console.log(import_chalk17.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
8282
9166
|
console.log("");
|
|
8283
|
-
console.log(` ${
|
|
9167
|
+
console.log(` ${import_chalk17.default.bold("Tool:")} ${import_chalk17.default.white(result.tool)}`);
|
|
8284
9168
|
if (argsRaw) {
|
|
8285
9169
|
const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
8286
|
-
console.log(` ${
|
|
9170
|
+
console.log(` ${import_chalk17.default.bold("Input:")} ${import_chalk17.default.gray(preview)}`);
|
|
8287
9171
|
}
|
|
8288
9172
|
console.log("");
|
|
8289
|
-
console.log(
|
|
9173
|
+
console.log(import_chalk17.default.bold("Config Sources (Waterfall):"));
|
|
8290
9174
|
for (const tier of result.waterfall) {
|
|
8291
|
-
const num =
|
|
9175
|
+
const num = import_chalk17.default.gray(` ${tier.tier}.`);
|
|
8292
9176
|
const label = tier.label.padEnd(16);
|
|
8293
9177
|
let statusStr;
|
|
8294
9178
|
if (tier.tier === 1) {
|
|
8295
|
-
statusStr =
|
|
9179
|
+
statusStr = import_chalk17.default.gray(tier.note ?? "");
|
|
8296
9180
|
} else if (tier.status === "active") {
|
|
8297
|
-
const loc = tier.path ?
|
|
8298
|
-
const note = tier.note ?
|
|
8299
|
-
statusStr =
|
|
9181
|
+
const loc = tier.path ? import_chalk17.default.gray(tier.path) : "";
|
|
9182
|
+
const note = tier.note ? import_chalk17.default.gray(`(${tier.note})`) : "";
|
|
9183
|
+
statusStr = import_chalk17.default.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
|
|
8300
9184
|
} else {
|
|
8301
|
-
statusStr =
|
|
9185
|
+
statusStr = import_chalk17.default.gray("\u25CB " + (tier.note ?? "not found"));
|
|
8302
9186
|
}
|
|
8303
|
-
console.log(`${num} ${
|
|
9187
|
+
console.log(`${num} ${import_chalk17.default.white(label)} ${statusStr}`);
|
|
8304
9188
|
}
|
|
8305
9189
|
console.log("");
|
|
8306
|
-
console.log(
|
|
9190
|
+
console.log(import_chalk17.default.bold("Policy Evaluation:"));
|
|
8307
9191
|
for (const step of result.steps) {
|
|
8308
9192
|
const isFinal = step.isFinal;
|
|
8309
9193
|
let icon;
|
|
8310
|
-
if (step.outcome === "allow") icon =
|
|
8311
|
-
else if (step.outcome === "review") icon =
|
|
8312
|
-
else if (step.outcome === "skip") icon =
|
|
8313
|
-
else icon =
|
|
9194
|
+
if (step.outcome === "allow") icon = import_chalk17.default.green(" \u2705");
|
|
9195
|
+
else if (step.outcome === "review") icon = import_chalk17.default.red(" \u{1F534}");
|
|
9196
|
+
else if (step.outcome === "skip") icon = import_chalk17.default.gray(" \u2500 ");
|
|
9197
|
+
else icon = import_chalk17.default.gray(" \u25CB ");
|
|
8314
9198
|
const name = step.name.padEnd(18);
|
|
8315
|
-
const nameStr = isFinal ?
|
|
8316
|
-
const detail = isFinal ?
|
|
8317
|
-
const arrow = isFinal ?
|
|
9199
|
+
const nameStr = isFinal ? import_chalk17.default.white.bold(name) : import_chalk17.default.white(name);
|
|
9200
|
+
const detail = isFinal ? import_chalk17.default.white(step.detail) : import_chalk17.default.gray(step.detail);
|
|
9201
|
+
const arrow = isFinal ? import_chalk17.default.yellow(" \u2190 STOP") : "";
|
|
8318
9202
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
8319
9203
|
}
|
|
8320
9204
|
console.log("");
|
|
8321
9205
|
if (result.decision === "allow") {
|
|
8322
|
-
console.log(
|
|
9206
|
+
console.log(import_chalk17.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk17.default.gray(" \u2014 no approval needed"));
|
|
8323
9207
|
} else {
|
|
8324
9208
|
console.log(
|
|
8325
|
-
|
|
9209
|
+
import_chalk17.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk17.default.gray(" \u2014 human approval required")
|
|
8326
9210
|
);
|
|
8327
9211
|
if (result.blockedByLabel) {
|
|
8328
|
-
console.log(
|
|
9212
|
+
console.log(import_chalk17.default.gray(` Reason: ${result.blockedByLabel}`));
|
|
8329
9213
|
}
|
|
8330
9214
|
}
|
|
8331
9215
|
console.log("");
|
|
8332
9216
|
});
|
|
8333
|
-
program
|
|
8334
|
-
const configPath = import_path23.default.join(import_os19.default.homedir(), ".node9", "config.json");
|
|
8335
|
-
if (import_fs21.default.existsSync(configPath) && !options.force) {
|
|
8336
|
-
console.log(import_chalk15.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
8337
|
-
console.log(import_chalk15.default.gray(` Run with --force to overwrite.`));
|
|
8338
|
-
return;
|
|
8339
|
-
}
|
|
8340
|
-
const requestedMode = options.mode.toLowerCase();
|
|
8341
|
-
const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
|
|
8342
|
-
const configToSave = {
|
|
8343
|
-
...DEFAULT_CONFIG,
|
|
8344
|
-
settings: {
|
|
8345
|
-
...DEFAULT_CONFIG.settings,
|
|
8346
|
-
mode: safeMode
|
|
8347
|
-
}
|
|
8348
|
-
};
|
|
8349
|
-
const dir = import_path23.default.dirname(configPath);
|
|
8350
|
-
if (!import_fs21.default.existsSync(dir)) import_fs21.default.mkdirSync(dir, { recursive: true });
|
|
8351
|
-
import_fs21.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
8352
|
-
console.log(import_chalk15.default.green(`\u2705 Global config created: ${configPath}`));
|
|
8353
|
-
console.log(import_chalk15.default.cyan(` Mode set to: ${safeMode}`));
|
|
8354
|
-
console.log(
|
|
8355
|
-
import_chalk15.default.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
|
|
8356
|
-
);
|
|
8357
|
-
});
|
|
9217
|
+
registerInitCommand(program);
|
|
8358
9218
|
registerAuditCommand(program);
|
|
8359
9219
|
registerStatusCommand(program);
|
|
8360
9220
|
registerDaemonCommand(program);
|
|
@@ -8363,7 +9223,7 @@ program.command("tail").description("Stream live agent activity to the terminal"
|
|
|
8363
9223
|
try {
|
|
8364
9224
|
await startTail2(options);
|
|
8365
9225
|
} catch (err) {
|
|
8366
|
-
console.error(
|
|
9226
|
+
console.error(import_chalk17.default.red(`\u274C ${err instanceof Error ? err.message : String(err)}`));
|
|
8367
9227
|
process.exit(1);
|
|
8368
9228
|
}
|
|
8369
9229
|
});
|
|
@@ -8375,7 +9235,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
8375
9235
|
const ms = parseDuration(options.duration);
|
|
8376
9236
|
if (ms === null) {
|
|
8377
9237
|
console.error(
|
|
8378
|
-
|
|
9238
|
+
import_chalk17.default.red(`
|
|
8379
9239
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
8380
9240
|
`)
|
|
8381
9241
|
);
|
|
@@ -8383,20 +9243,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
8383
9243
|
}
|
|
8384
9244
|
pauseNode9(ms, options.duration);
|
|
8385
9245
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
8386
|
-
console.log(
|
|
9246
|
+
console.log(import_chalk17.default.yellow(`
|
|
8387
9247
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
8388
|
-
console.log(
|
|
8389
|
-
console.log(
|
|
9248
|
+
console.log(import_chalk17.default.gray(` All tool calls will be allowed without review.`));
|
|
9249
|
+
console.log(import_chalk17.default.gray(` Run "node9 resume" to re-enable early.
|
|
8390
9250
|
`));
|
|
8391
9251
|
});
|
|
8392
9252
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
8393
9253
|
const { paused } = checkPause();
|
|
8394
9254
|
if (!paused) {
|
|
8395
|
-
console.log(
|
|
9255
|
+
console.log(import_chalk17.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
8396
9256
|
return;
|
|
8397
9257
|
}
|
|
8398
9258
|
resumeNode9();
|
|
8399
|
-
console.log(
|
|
9259
|
+
console.log(import_chalk17.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
8400
9260
|
});
|
|
8401
9261
|
var HOOK_BASED_AGENTS = {
|
|
8402
9262
|
claude: "claude",
|
|
@@ -8409,15 +9269,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
8409
9269
|
if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
|
|
8410
9270
|
const target = HOOK_BASED_AGENTS[firstArg2];
|
|
8411
9271
|
console.error(
|
|
8412
|
-
|
|
9272
|
+
import_chalk17.default.yellow(`
|
|
8413
9273
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
8414
9274
|
);
|
|
8415
|
-
console.error(
|
|
9275
|
+
console.error(import_chalk17.default.white(`
|
|
8416
9276
|
"${target}" uses its own hook system. Use:`));
|
|
8417
9277
|
console.error(
|
|
8418
|
-
|
|
9278
|
+
import_chalk17.default.green(` node9 addto ${target} `) + import_chalk17.default.gray("# one-time setup")
|
|
8419
9279
|
);
|
|
8420
|
-
console.error(
|
|
9280
|
+
console.error(import_chalk17.default.green(` ${target} `) + import_chalk17.default.gray("# run normally"));
|
|
8421
9281
|
process.exit(1);
|
|
8422
9282
|
}
|
|
8423
9283
|
const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
|
|
@@ -8434,7 +9294,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
8434
9294
|
}
|
|
8435
9295
|
);
|
|
8436
9296
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
8437
|
-
console.error(
|
|
9297
|
+
console.error(import_chalk17.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
8438
9298
|
const daemonReady = await autoStartDaemonAndWait();
|
|
8439
9299
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
8440
9300
|
}
|
|
@@ -8447,12 +9307,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
8447
9307
|
}
|
|
8448
9308
|
if (!result.approved) {
|
|
8449
9309
|
console.error(
|
|
8450
|
-
|
|
9310
|
+
import_chalk17.default.red(`
|
|
8451
9311
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
8452
9312
|
);
|
|
8453
9313
|
process.exit(1);
|
|
8454
9314
|
}
|
|
8455
|
-
console.error(
|
|
9315
|
+
console.error(import_chalk17.default.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
8456
9316
|
await runProxy(fullCommand);
|
|
8457
9317
|
} else {
|
|
8458
9318
|
program.help();
|
|
@@ -8461,14 +9321,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
8461
9321
|
registerUndoCommand(program);
|
|
8462
9322
|
registerShieldCommand(program);
|
|
8463
9323
|
registerConfigShowCommand(program);
|
|
9324
|
+
registerTrustCommand(program);
|
|
8464
9325
|
if (process.argv[2] !== "daemon") {
|
|
8465
9326
|
process.on("unhandledRejection", (reason) => {
|
|
8466
9327
|
const isCheckHook = process.argv[2] === "check";
|
|
8467
9328
|
if (isCheckHook) {
|
|
8468
9329
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
8469
|
-
const logPath =
|
|
9330
|
+
const logPath = import_path26.default.join(import_os22.default.homedir(), ".node9", "hook-debug.log");
|
|
8470
9331
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
8471
|
-
|
|
9332
|
+
import_fs24.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
8472
9333
|
`);
|
|
8473
9334
|
}
|
|
8474
9335
|
process.exit(0);
|