@node9/proxy 1.5.5 → 1.7.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 +70 -8
- package/dist/cli.js +784 -171
- package/dist/cli.mjs +782 -169
- package/dist/index.js +2 -1
- package/dist/index.mjs +2 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -139,8 +139,8 @@ function sanitizeConfig(raw) {
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
const lines = result.error.issues.map((issue) => {
|
|
142
|
-
const
|
|
143
|
-
return ` \u2022 ${
|
|
142
|
+
const path31 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
143
|
+
return ` \u2022 ${path31}: ${issue.message}`;
|
|
144
144
|
});
|
|
145
145
|
return {
|
|
146
146
|
sanitized,
|
|
@@ -297,9 +297,9 @@ function readShieldsFile() {
|
|
|
297
297
|
(e) => typeof e === "string" && e.length > 0 && e in SHIELDS
|
|
298
298
|
) : [];
|
|
299
299
|
return { active, overrides: validateOverrides(parsed.overrides) };
|
|
300
|
-
} catch (
|
|
301
|
-
if (
|
|
302
|
-
process.stderr.write(`[node9] Warning: could not read shields state: ${String(
|
|
300
|
+
} catch (err2) {
|
|
301
|
+
if (err2.code !== "ENOENT") {
|
|
302
|
+
process.stderr.write(`[node9] Warning: could not read shields state: ${String(err2)}
|
|
303
303
|
`);
|
|
304
304
|
}
|
|
305
305
|
return { active: [] };
|
|
@@ -714,8 +714,8 @@ function tryLoadConfig(filePath) {
|
|
|
714
714
|
let raw;
|
|
715
715
|
try {
|
|
716
716
|
raw = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
|
|
717
|
-
} catch (
|
|
718
|
-
const msg =
|
|
717
|
+
} catch (err2) {
|
|
718
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
719
719
|
process.stderr.write(
|
|
720
720
|
`
|
|
721
721
|
\u26A0\uFE0F Node9: Failed to parse ${filePath}
|
|
@@ -777,7 +777,7 @@ var init_config = __esm({
|
|
|
777
777
|
DEFAULT_CONFIG = {
|
|
778
778
|
version: "1.0",
|
|
779
779
|
settings: {
|
|
780
|
-
mode: "
|
|
780
|
+
mode: "standard",
|
|
781
781
|
autoStartDaemon: true,
|
|
782
782
|
enableUndo: true,
|
|
783
783
|
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
@@ -1043,10 +1043,10 @@ function getCompiledRegex(pattern, flags = "") {
|
|
|
1043
1043
|
regexCache.set(key, cached);
|
|
1044
1044
|
return cached;
|
|
1045
1045
|
}
|
|
1046
|
-
const
|
|
1047
|
-
if (
|
|
1046
|
+
const err2 = validateRegex(pattern);
|
|
1047
|
+
if (err2) {
|
|
1048
1048
|
if (process.env.NODE9_DEBUG === "1")
|
|
1049
|
-
console.error(`[Node9] Regex blocked: ${
|
|
1049
|
+
console.error(`[Node9] Regex blocked: ${err2} \u2014 pattern: "${pattern}"`);
|
|
1050
1050
|
return null;
|
|
1051
1051
|
}
|
|
1052
1052
|
try {
|
|
@@ -1081,8 +1081,8 @@ function scanFilePath(filePath, cwd = process.cwd()) {
|
|
|
1081
1081
|
try {
|
|
1082
1082
|
const absolute = path4.resolve(cwd, filePath);
|
|
1083
1083
|
resolved = fs4.realpathSync.native(absolute);
|
|
1084
|
-
} catch (
|
|
1085
|
-
const code =
|
|
1084
|
+
} catch (err2) {
|
|
1085
|
+
const code = err2.code;
|
|
1086
1086
|
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
1087
1087
|
resolved = path4.resolve(cwd, filePath);
|
|
1088
1088
|
} else {
|
|
@@ -1757,9 +1757,9 @@ function matchesPattern(text, patterns) {
|
|
|
1757
1757
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1758
1758
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1759
1759
|
}
|
|
1760
|
-
function getNestedValue(obj,
|
|
1760
|
+
function getNestedValue(obj, path31) {
|
|
1761
1761
|
if (!obj || typeof obj !== "object") return null;
|
|
1762
|
-
return
|
|
1762
|
+
return path31.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1763
1763
|
}
|
|
1764
1764
|
function shouldSnapshot(toolName, args, config) {
|
|
1765
1765
|
if (!config.settings.enableUndo) return false;
|
|
@@ -2409,9 +2409,9 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
2409
2409
|
trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > now);
|
|
2410
2410
|
trust.entries.push({ tool: toolName, expiry: now + durationMs });
|
|
2411
2411
|
atomicWriteSync(TRUST_FILE, JSON.stringify(trust, null, 2));
|
|
2412
|
-
} catch (
|
|
2412
|
+
} catch (err2) {
|
|
2413
2413
|
if (process.env.NODE9_DEBUG === "1") {
|
|
2414
|
-
console.error("[Node9 Trust Error]:",
|
|
2414
|
+
console.error("[Node9 Trust Error]:", err2);
|
|
2415
2415
|
}
|
|
2416
2416
|
}
|
|
2417
2417
|
}
|
|
@@ -2610,13 +2610,13 @@ async function checkTaint(paths) {
|
|
|
2610
2610
|
signal: AbortSignal.timeout(2e3)
|
|
2611
2611
|
});
|
|
2612
2612
|
return await res.json();
|
|
2613
|
-
} catch (
|
|
2613
|
+
} catch (err2) {
|
|
2614
2614
|
try {
|
|
2615
2615
|
const { appendToLog: appendToLog2, HOOK_DEBUG_LOG: HOOK_DEBUG_LOG2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
|
|
2616
2616
|
appendToLog2(HOOK_DEBUG_LOG2, {
|
|
2617
2617
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2618
2618
|
event: "checkTaint-error",
|
|
2619
|
-
error: String(
|
|
2619
|
+
error: String(err2),
|
|
2620
2620
|
paths
|
|
2621
2621
|
});
|
|
2622
2622
|
} catch {
|
|
@@ -3109,10 +3109,10 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
3109
3109
|
`
|
|
3110
3110
|
);
|
|
3111
3111
|
}
|
|
3112
|
-
} catch (
|
|
3112
|
+
} catch (err2) {
|
|
3113
3113
|
fs10.appendFileSync(
|
|
3114
3114
|
HOOK_DEBUG_LOG,
|
|
3115
|
-
`[resolve-cloud] PATCH failed for ${requestId}: ${
|
|
3115
|
+
`[resolve-cloud] PATCH failed for ${requestId}: ${err2.message}
|
|
3116
3116
|
`
|
|
3117
3117
|
);
|
|
3118
3118
|
}
|
|
@@ -3176,6 +3176,7 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
3176
3176
|
await notifyActivity({
|
|
3177
3177
|
id: actId,
|
|
3178
3178
|
tool: toolName,
|
|
3179
|
+
args,
|
|
3179
3180
|
ts: actTs,
|
|
3180
3181
|
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
|
|
3181
3182
|
label: result.blockedByLabel,
|
|
@@ -3479,10 +3480,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3479
3480
|
blockedBy: cloudResult.approved ? void 0 : "team-policy",
|
|
3480
3481
|
blockedByLabel: "Organization Policy (SaaS)"
|
|
3481
3482
|
};
|
|
3482
|
-
} catch (
|
|
3483
|
-
const error =
|
|
3484
|
-
if (error.name === "AbortError" || error.message?.includes("Aborted")) throw
|
|
3485
|
-
throw
|
|
3483
|
+
} catch (err2) {
|
|
3484
|
+
const error = err2;
|
|
3485
|
+
if (error.name === "AbortError" || error.message?.includes("Aborted")) throw err2;
|
|
3486
|
+
throw err2;
|
|
3486
3487
|
}
|
|
3487
3488
|
})()
|
|
3488
3489
|
);
|
|
@@ -3575,10 +3576,10 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
3575
3576
|
}
|
|
3576
3577
|
};
|
|
3577
3578
|
for (const p of racePromises) {
|
|
3578
|
-
p.then(finish).catch((
|
|
3579
|
-
if (
|
|
3579
|
+
p.then(finish).catch((err2) => {
|
|
3580
|
+
if (err2.name === "AbortError" || err2.message?.includes("canceled") || err2.message?.includes("Aborted"))
|
|
3580
3581
|
return;
|
|
3581
|
-
if (
|
|
3582
|
+
if (err2.message === "Abandoned") {
|
|
3582
3583
|
finish({
|
|
3583
3584
|
approved: false,
|
|
3584
3585
|
reason: "Browser dashboard closed without making a decision.",
|
|
@@ -5453,6 +5454,7 @@ var init_session_counters = __esm({
|
|
|
5453
5454
|
_blocked = 0;
|
|
5454
5455
|
_dlpHits = 0;
|
|
5455
5456
|
_wouldBlock = 0;
|
|
5457
|
+
_estimatedCost = 0;
|
|
5456
5458
|
_lastRuleHit = null;
|
|
5457
5459
|
_lastBlockedTool = null;
|
|
5458
5460
|
incrementAllowed() {
|
|
@@ -5467,6 +5469,10 @@ var init_session_counters = __esm({
|
|
|
5467
5469
|
incrementWouldBlock() {
|
|
5468
5470
|
this._wouldBlock++;
|
|
5469
5471
|
}
|
|
5472
|
+
addCost(amount) {
|
|
5473
|
+
if (!isFinite(amount) || amount < 0) return;
|
|
5474
|
+
this._estimatedCost += amount;
|
|
5475
|
+
}
|
|
5470
5476
|
recordRuleHit(label) {
|
|
5471
5477
|
this._lastRuleHit = label;
|
|
5472
5478
|
}
|
|
@@ -5479,6 +5485,7 @@ var init_session_counters = __esm({
|
|
|
5479
5485
|
blocked: this._blocked,
|
|
5480
5486
|
dlpHits: this._dlpHits,
|
|
5481
5487
|
wouldBlock: this._wouldBlock,
|
|
5488
|
+
estimatedCost: this._estimatedCost,
|
|
5482
5489
|
lastRuleHit: this._lastRuleHit,
|
|
5483
5490
|
lastBlockedTool: this._lastBlockedTool
|
|
5484
5491
|
};
|
|
@@ -5488,6 +5495,7 @@ var init_session_counters = __esm({
|
|
|
5488
5495
|
this._blocked = 0;
|
|
5489
5496
|
this._dlpHits = 0;
|
|
5490
5497
|
this._wouldBlock = 0;
|
|
5498
|
+
this._estimatedCost = 0;
|
|
5491
5499
|
this._lastRuleHit = null;
|
|
5492
5500
|
this._lastBlockedTool = null;
|
|
5493
5501
|
}
|
|
@@ -5598,21 +5606,21 @@ function atomicWriteSync2(filePath, data, options) {
|
|
|
5598
5606
|
const tmpPath = `${filePath}.${randomUUID3()}.tmp`;
|
|
5599
5607
|
try {
|
|
5600
5608
|
fs13.writeFileSync(tmpPath, data, options);
|
|
5601
|
-
} catch (
|
|
5609
|
+
} catch (err2) {
|
|
5602
5610
|
try {
|
|
5603
5611
|
fs13.unlinkSync(tmpPath);
|
|
5604
5612
|
} catch {
|
|
5605
5613
|
}
|
|
5606
|
-
throw
|
|
5614
|
+
throw err2;
|
|
5607
5615
|
}
|
|
5608
5616
|
try {
|
|
5609
5617
|
fs13.renameSync(tmpPath, filePath);
|
|
5610
|
-
} catch (
|
|
5618
|
+
} catch (err2) {
|
|
5611
5619
|
try {
|
|
5612
5620
|
fs13.unlinkSync(tmpPath);
|
|
5613
5621
|
} catch {
|
|
5614
5622
|
}
|
|
5615
|
-
throw
|
|
5623
|
+
throw err2;
|
|
5616
5624
|
}
|
|
5617
5625
|
}
|
|
5618
5626
|
function redactArgs(value) {
|
|
@@ -5718,15 +5726,38 @@ function openBrowser(url) {
|
|
|
5718
5726
|
} catch {
|
|
5719
5727
|
}
|
|
5720
5728
|
}
|
|
5729
|
+
function estimateToolCost(tool, args) {
|
|
5730
|
+
const a = args ?? {};
|
|
5731
|
+
const t = tool.toLowerCase().replace(/[^a-z_]/g, "_");
|
|
5732
|
+
if (t.includes("read") || t === "glob" || t === "grep") {
|
|
5733
|
+
const filePath = a.file_path ?? a.path;
|
|
5734
|
+
if (filePath) {
|
|
5735
|
+
try {
|
|
5736
|
+
const bytes = fs13.statSync(filePath).size;
|
|
5737
|
+
return bytes / BYTES_PER_TOKEN / 1e6 * INPUT_PRICE_PER_1M;
|
|
5738
|
+
} catch {
|
|
5739
|
+
}
|
|
5740
|
+
}
|
|
5741
|
+
}
|
|
5742
|
+
if (t.includes("write")) {
|
|
5743
|
+
const content = a.content ?? "";
|
|
5744
|
+
return String(content).length / BYTES_PER_TOKEN / 1e6 * OUTPUT_PRICE_PER_1M;
|
|
5745
|
+
}
|
|
5746
|
+
if (t.includes("edit") || t === "str_replace_based_edit_tool") {
|
|
5747
|
+
const newStr = a.new_string ?? "";
|
|
5748
|
+
return String(newStr).length / BYTES_PER_TOKEN / 1e6 * OUTPUT_PRICE_PER_1M;
|
|
5749
|
+
}
|
|
5750
|
+
return void 0;
|
|
5751
|
+
}
|
|
5721
5752
|
function broadcast(event, data) {
|
|
5722
5753
|
if (event === "activity") {
|
|
5723
5754
|
activityRing.push({ event, data });
|
|
5724
5755
|
if (activityRing.length > ACTIVITY_RING_SIZE) activityRing.shift();
|
|
5725
5756
|
} else if (event === "activity-result") {
|
|
5726
|
-
const { id, status, label } = data;
|
|
5757
|
+
const { id, status, label, costEstimate } = data;
|
|
5727
5758
|
for (let i = activityRing.length - 1; i >= 0; i--) {
|
|
5728
5759
|
if (activityRing[i].data.id === id) {
|
|
5729
|
-
Object.assign(activityRing[i].data, { status, label });
|
|
5760
|
+
Object.assign(activityRing[i].data, { status, label, costEstimate });
|
|
5730
5761
|
break;
|
|
5731
5762
|
}
|
|
5732
5763
|
}
|
|
@@ -5791,6 +5822,16 @@ function startActivitySocket() {
|
|
|
5791
5822
|
sessionHistory.recordTestFail(data.ts);
|
|
5792
5823
|
return;
|
|
5793
5824
|
}
|
|
5825
|
+
if (data.status === "snapshot") {
|
|
5826
|
+
broadcast("snapshot", {
|
|
5827
|
+
hash: data.hash,
|
|
5828
|
+
tool: data.tool,
|
|
5829
|
+
argsSummary: data.argsSummary,
|
|
5830
|
+
fileCount: data.fileCount,
|
|
5831
|
+
ts: data.ts
|
|
5832
|
+
});
|
|
5833
|
+
return;
|
|
5834
|
+
}
|
|
5794
5835
|
if (data.status === "pending") {
|
|
5795
5836
|
broadcast("activity", {
|
|
5796
5837
|
id: data.id,
|
|
@@ -5818,10 +5859,13 @@ function startActivitySocket() {
|
|
|
5818
5859
|
sessionCounters.incrementBlocked();
|
|
5819
5860
|
sessionCounters.recordBlockedTool(data.tool);
|
|
5820
5861
|
}
|
|
5862
|
+
const costEstimate = data.status === "allow" ? estimateToolCost(data.tool, data.args) : void 0;
|
|
5863
|
+
if (costEstimate != null && costEstimate > 0) sessionCounters.addCost(costEstimate);
|
|
5821
5864
|
broadcast("activity-result", {
|
|
5822
5865
|
id: data.id,
|
|
5823
5866
|
status: data.status,
|
|
5824
|
-
label: data.label
|
|
5867
|
+
label: data.label,
|
|
5868
|
+
costEstimate
|
|
5825
5869
|
});
|
|
5826
5870
|
}
|
|
5827
5871
|
} catch {
|
|
@@ -5838,7 +5882,7 @@ function startActivitySocket() {
|
|
|
5838
5882
|
}
|
|
5839
5883
|
});
|
|
5840
5884
|
}
|
|
5841
|
-
var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE, WRITE_TOOL_NAMES;
|
|
5885
|
+
var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, suggestions, taintStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, SECRET_KEY_RE, INPUT_PRICE_PER_1M, OUTPUT_PRICE_PER_1M, BYTES_PER_TOKEN, WRITE_TOOL_NAMES;
|
|
5842
5886
|
var init_state2 = __esm({
|
|
5843
5887
|
"src/daemon/state.ts"() {
|
|
5844
5888
|
"use strict";
|
|
@@ -5876,6 +5920,9 @@ var init_state2 = __esm({
|
|
|
5876
5920
|
ACTIVITY_RING_SIZE = 100;
|
|
5877
5921
|
activityRing = [];
|
|
5878
5922
|
SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
5923
|
+
INPUT_PRICE_PER_1M = 3;
|
|
5924
|
+
OUTPUT_PRICE_PER_1M = 15;
|
|
5925
|
+
BYTES_PER_TOKEN = 4;
|
|
5879
5926
|
WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
5880
5927
|
"write",
|
|
5881
5928
|
"write_file",
|
|
@@ -5922,21 +5969,21 @@ function patchConfig(configPath, patch) {
|
|
|
5922
5969
|
const tmp = configPath + ".node9-tmp";
|
|
5923
5970
|
try {
|
|
5924
5971
|
fs14.writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
|
|
5925
|
-
} catch (
|
|
5972
|
+
} catch (err2) {
|
|
5926
5973
|
try {
|
|
5927
5974
|
fs14.unlinkSync(tmp);
|
|
5928
5975
|
} catch {
|
|
5929
5976
|
}
|
|
5930
|
-
throw
|
|
5977
|
+
throw err2;
|
|
5931
5978
|
}
|
|
5932
5979
|
try {
|
|
5933
5980
|
fs14.renameSync(tmp, configPath);
|
|
5934
|
-
} catch (
|
|
5981
|
+
} catch (err2) {
|
|
5935
5982
|
try {
|
|
5936
5983
|
fs14.unlinkSync(tmp);
|
|
5937
5984
|
} catch {
|
|
5938
5985
|
}
|
|
5939
|
-
throw
|
|
5986
|
+
throw err2;
|
|
5940
5987
|
}
|
|
5941
5988
|
}
|
|
5942
5989
|
var GLOBAL_CONFIG_PATH;
|
|
@@ -6191,11 +6238,11 @@ data: ${JSON.stringify(item.data)}
|
|
|
6191
6238
|
e.earlyDecision = decision;
|
|
6192
6239
|
e.earlyReason = result.reason;
|
|
6193
6240
|
}
|
|
6194
|
-
}).catch((
|
|
6241
|
+
}).catch((err2) => {
|
|
6195
6242
|
const e = pending.get(id);
|
|
6196
6243
|
if (!e) return;
|
|
6197
6244
|
clearTimeout(e.timer);
|
|
6198
|
-
const reason =
|
|
6245
|
+
const reason = err2?.reason || "No response \u2014 request timed out";
|
|
6199
6246
|
if (e.waiter) e.waiter("deny", reason);
|
|
6200
6247
|
else {
|
|
6201
6248
|
e.earlyDecision = "deny";
|
|
@@ -6322,8 +6369,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
6322
6369
|
const s = getGlobalSettings();
|
|
6323
6370
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6324
6371
|
return res.end(JSON.stringify({ ...s, autoStarted }));
|
|
6325
|
-
} catch (
|
|
6326
|
-
console.error(chalk2.red("[node9 daemon] GET /settings failed:"),
|
|
6372
|
+
} catch (err2) {
|
|
6373
|
+
console.error(chalk2.red("[node9 daemon] GET /settings failed:"), err2);
|
|
6327
6374
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
6328
6375
|
return res.end(JSON.stringify({ error: "internal" }));
|
|
6329
6376
|
}
|
|
@@ -6339,7 +6386,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
6339
6386
|
allowed: counters.allowed,
|
|
6340
6387
|
blocked: counters.blocked,
|
|
6341
6388
|
dlpHits: counters.dlpHits,
|
|
6342
|
-
wouldBlock: counters.wouldBlock
|
|
6389
|
+
wouldBlock: counters.wouldBlock,
|
|
6390
|
+
estimatedCost: counters.estimatedCost
|
|
6343
6391
|
},
|
|
6344
6392
|
taintedCount: taintStore.list().length,
|
|
6345
6393
|
lastRuleHit: counters.lastRuleHit,
|
|
@@ -6347,8 +6395,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
6347
6395
|
};
|
|
6348
6396
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6349
6397
|
return res.end(JSON.stringify(status));
|
|
6350
|
-
} catch (
|
|
6351
|
-
console.error(chalk2.red("[node9 daemon] GET /status failed:"),
|
|
6398
|
+
} catch (err2) {
|
|
6399
|
+
console.error(chalk2.red("[node9 daemon] GET /status failed:"), err2);
|
|
6352
6400
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
6353
6401
|
return res.end(JSON.stringify({ error: "internal" }));
|
|
6354
6402
|
}
|
|
@@ -6388,8 +6436,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
6388
6436
|
const s = getGlobalSettings();
|
|
6389
6437
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6390
6438
|
return res.end(JSON.stringify({ hasKey: hasStoredSlackKey(), enabled: s.slackEnabled }));
|
|
6391
|
-
} catch (
|
|
6392
|
-
console.error(chalk2.red("[node9 daemon] GET /slack-status failed:"),
|
|
6439
|
+
} catch (err2) {
|
|
6440
|
+
console.error(chalk2.red("[node9 daemon] GET /slack-status failed:"), err2);
|
|
6393
6441
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
6394
6442
|
return res.end(JSON.stringify({ error: "internal" }));
|
|
6395
6443
|
}
|
|
@@ -6471,6 +6519,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
6471
6519
|
}
|
|
6472
6520
|
if (req.method === "POST" && pathname === "/events/clear") {
|
|
6473
6521
|
activityRing.length = 0;
|
|
6522
|
+
sessionCounters.reset();
|
|
6474
6523
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6475
6524
|
return res.end(JSON.stringify({ ok: true }));
|
|
6476
6525
|
}
|
|
@@ -6549,10 +6598,10 @@ data: ${JSON.stringify(item.data)}
|
|
|
6549
6598
|
broadcast("suggestion:resolved", { id, status: "applied" });
|
|
6550
6599
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6551
6600
|
return res.end(JSON.stringify({ ok: true }));
|
|
6552
|
-
} catch (
|
|
6553
|
-
console.error(chalk2.red("[node9 daemon] POST /suggestions/:id/apply failed:"),
|
|
6601
|
+
} catch (err2) {
|
|
6602
|
+
console.error(chalk2.red("[node9 daemon] POST /suggestions/:id/apply failed:"), err2);
|
|
6554
6603
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
6555
|
-
return res.end(JSON.stringify({ error: String(
|
|
6604
|
+
return res.end(JSON.stringify({ error: String(err2) }));
|
|
6556
6605
|
}
|
|
6557
6606
|
}
|
|
6558
6607
|
if (req.method === "POST" && pathname.startsWith("/suggestions/") && pathname.endsWith("/dismiss")) {
|
|
@@ -6767,10 +6816,10 @@ __export(tail_exports, {
|
|
|
6767
6816
|
});
|
|
6768
6817
|
import http2 from "http";
|
|
6769
6818
|
import chalk17 from "chalk";
|
|
6770
|
-
import
|
|
6771
|
-
import
|
|
6772
|
-
import
|
|
6773
|
-
import
|
|
6819
|
+
import fs25 from "fs";
|
|
6820
|
+
import os21 from "os";
|
|
6821
|
+
import path28 from "path";
|
|
6822
|
+
import readline5 from "readline";
|
|
6774
6823
|
import { spawn as spawn9, execSync as execSync3 } from "child_process";
|
|
6775
6824
|
function getIcon(tool) {
|
|
6776
6825
|
const t = tool.toLowerCase();
|
|
@@ -6783,7 +6832,7 @@ function formatBase(activity) {
|
|
|
6783
6832
|
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
6784
6833
|
const icon = getIcon(activity.tool);
|
|
6785
6834
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
6786
|
-
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
|
|
6835
|
+
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os21.homedir(), "~");
|
|
6787
6836
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
6788
6837
|
return `${chalk17.gray(time)} ${icon} ${chalk17.white.bold(toolName)} ${chalk17.dim(argsPreview)}`;
|
|
6789
6838
|
}
|
|
@@ -6797,11 +6846,13 @@ function renderResult(activity, result) {
|
|
|
6797
6846
|
} else {
|
|
6798
6847
|
status = chalk17.red("\u2717 BLOCK");
|
|
6799
6848
|
}
|
|
6849
|
+
const cost = result.costEstimate ?? activity.costEstimate;
|
|
6850
|
+
const costSuffix = cost == null ? "" : chalk17.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
|
|
6800
6851
|
if (process.stdout.isTTY) {
|
|
6801
|
-
|
|
6802
|
-
|
|
6852
|
+
readline5.clearLine(process.stdout, 0);
|
|
6853
|
+
readline5.cursorTo(process.stdout, 0);
|
|
6803
6854
|
}
|
|
6804
|
-
console.log(`${base} ${status}`);
|
|
6855
|
+
console.log(`${base} ${status}${costSuffix}`);
|
|
6805
6856
|
}
|
|
6806
6857
|
function renderPending(activity) {
|
|
6807
6858
|
if (!process.stdout.isTTY) return;
|
|
@@ -6809,9 +6860,9 @@ function renderPending(activity) {
|
|
|
6809
6860
|
}
|
|
6810
6861
|
async function ensureDaemon() {
|
|
6811
6862
|
let pidPort = null;
|
|
6812
|
-
if (
|
|
6863
|
+
if (fs25.existsSync(PID_FILE)) {
|
|
6813
6864
|
try {
|
|
6814
|
-
const { port } = JSON.parse(
|
|
6865
|
+
const { port } = JSON.parse(fs25.readFileSync(PID_FILE, "utf-8"));
|
|
6815
6866
|
pidPort = port;
|
|
6816
6867
|
} catch {
|
|
6817
6868
|
console.error(chalk17.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
@@ -6930,6 +6981,39 @@ function buildRecoveryCardLines(req) {
|
|
|
6930
6981
|
``
|
|
6931
6982
|
];
|
|
6932
6983
|
}
|
|
6984
|
+
function readApproversFromDisk() {
|
|
6985
|
+
const configPath = path28.join(os21.homedir(), ".node9", "config.json");
|
|
6986
|
+
try {
|
|
6987
|
+
const raw = JSON.parse(fs25.readFileSync(configPath, "utf-8"));
|
|
6988
|
+
const settings = raw.settings ?? {};
|
|
6989
|
+
return settings.approvers ?? {};
|
|
6990
|
+
} catch {
|
|
6991
|
+
return {};
|
|
6992
|
+
}
|
|
6993
|
+
}
|
|
6994
|
+
function approverStatusLine() {
|
|
6995
|
+
const a = readApproversFromDisk();
|
|
6996
|
+
const fmt = (label, key) => {
|
|
6997
|
+
const on = a[key] !== false;
|
|
6998
|
+
return `[${key[0]}]${label.slice(1)} ${on ? chalk17.green("\u2713") : chalk17.dim("\u2717")}`;
|
|
6999
|
+
};
|
|
7000
|
+
return `${fmt("native", "native")} ${fmt("browser", "browser")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
|
|
7001
|
+
}
|
|
7002
|
+
function toggleApprover(channel) {
|
|
7003
|
+
const configPath = path28.join(os21.homedir(), ".node9", "config.json");
|
|
7004
|
+
try {
|
|
7005
|
+
const raw = JSON.parse(fs25.readFileSync(configPath, "utf-8"));
|
|
7006
|
+
const settings = raw.settings ?? {};
|
|
7007
|
+
const approvers = settings.approvers ?? {};
|
|
7008
|
+
approvers[channel] = approvers[channel] === false;
|
|
7009
|
+
settings.approvers = approvers;
|
|
7010
|
+
raw.settings = settings;
|
|
7011
|
+
fs25.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
|
|
7012
|
+
} catch (err2) {
|
|
7013
|
+
process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
|
|
7014
|
+
`);
|
|
7015
|
+
}
|
|
7016
|
+
}
|
|
6933
7017
|
async function startTail(options = {}) {
|
|
6934
7018
|
const port = await ensureDaemon();
|
|
6935
7019
|
if (options.clear) {
|
|
@@ -6948,7 +7032,7 @@ async function startTail(options = {}) {
|
|
|
6948
7032
|
res.resume();
|
|
6949
7033
|
}
|
|
6950
7034
|
);
|
|
6951
|
-
req2.once("error", (
|
|
7035
|
+
req2.once("error", (err2) => resolve({ ok: false, code: err2.code }));
|
|
6952
7036
|
req2.setTimeout(2e3, () => {
|
|
6953
7037
|
resolve({ ok: false, code: "ETIMEDOUT" });
|
|
6954
7038
|
req2.destroy();
|
|
@@ -6975,10 +7059,48 @@ async function startTail(options = {}) {
|
|
|
6975
7059
|
let cancelActiveCard = null;
|
|
6976
7060
|
const localAllowCounts = /* @__PURE__ */ new Map();
|
|
6977
7061
|
const canApprove = process.stdout.isTTY && process.stdin.isTTY;
|
|
6978
|
-
if (canApprove)
|
|
7062
|
+
if (canApprove) readline5.emitKeypressEvents(process.stdin);
|
|
7063
|
+
let idleKeypressHandler = null;
|
|
7064
|
+
function enterIdleMode() {
|
|
7065
|
+
if (!canApprove || idleKeypressHandler !== null) return;
|
|
7066
|
+
try {
|
|
7067
|
+
process.stdin.setRawMode(true);
|
|
7068
|
+
} catch {
|
|
7069
|
+
return;
|
|
7070
|
+
}
|
|
7071
|
+
process.stdin.resume();
|
|
7072
|
+
idleKeypressHandler = (_str, key) => {
|
|
7073
|
+
const name = key?.name ?? "";
|
|
7074
|
+
if (key?.ctrl && name === "c") {
|
|
7075
|
+
process.kill(process.pid, "SIGINT");
|
|
7076
|
+
return;
|
|
7077
|
+
}
|
|
7078
|
+
if (name === "q") {
|
|
7079
|
+
process.kill(process.pid, "SIGINT");
|
|
7080
|
+
return;
|
|
7081
|
+
}
|
|
7082
|
+
const channel = name === "n" ? "native" : name === "b" ? "browser" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
|
|
7083
|
+
if (channel) {
|
|
7084
|
+
toggleApprover(channel);
|
|
7085
|
+
console.log(chalk17.dim(` Approvers: ${approverStatusLine()}`));
|
|
7086
|
+
}
|
|
7087
|
+
};
|
|
7088
|
+
process.stdin.on("keypress", idleKeypressHandler);
|
|
7089
|
+
}
|
|
7090
|
+
function exitIdleMode() {
|
|
7091
|
+
if (idleKeypressHandler) {
|
|
7092
|
+
process.stdin.removeListener("keypress", idleKeypressHandler);
|
|
7093
|
+
idleKeypressHandler = null;
|
|
7094
|
+
}
|
|
7095
|
+
try {
|
|
7096
|
+
process.stdin.setRawMode(false);
|
|
7097
|
+
} catch {
|
|
7098
|
+
}
|
|
7099
|
+
process.stdin.pause();
|
|
7100
|
+
}
|
|
6979
7101
|
function clearCard() {
|
|
6980
7102
|
if (cardLineCount > 0) {
|
|
6981
|
-
|
|
7103
|
+
readline5.moveCursor(process.stdout, 0, -cardLineCount);
|
|
6982
7104
|
process.stdout.write(ERASE_DOWN);
|
|
6983
7105
|
cardLineCount = 0;
|
|
6984
7106
|
}
|
|
@@ -6994,10 +7116,12 @@ async function startTail(options = {}) {
|
|
|
6994
7116
|
}
|
|
6995
7117
|
function showNextCard() {
|
|
6996
7118
|
if (cardActive || approvalQueue.length === 0 || !canApprove) return;
|
|
7119
|
+
exitIdleMode();
|
|
6997
7120
|
try {
|
|
6998
7121
|
process.stdin.setRawMode(true);
|
|
6999
7122
|
} catch {
|
|
7000
7123
|
cardActive = false;
|
|
7124
|
+
enterIdleMode();
|
|
7001
7125
|
return;
|
|
7002
7126
|
}
|
|
7003
7127
|
cardActive = true;
|
|
@@ -7009,12 +7133,8 @@ async function startTail(options = {}) {
|
|
|
7009
7133
|
const handler = onKeypress;
|
|
7010
7134
|
onKeypress = null;
|
|
7011
7135
|
if (handler) process.stdin.removeListener("keypress", handler);
|
|
7012
|
-
try {
|
|
7013
|
-
process.stdin.setRawMode(false);
|
|
7014
|
-
} catch {
|
|
7015
|
-
}
|
|
7016
|
-
process.stdin.pause();
|
|
7017
7136
|
cancelActiveCard = null;
|
|
7137
|
+
enterIdleMode();
|
|
7018
7138
|
};
|
|
7019
7139
|
const settle = (action) => {
|
|
7020
7140
|
if (settled) return;
|
|
@@ -7054,11 +7174,11 @@ async function startTail(options = {}) {
|
|
|
7054
7174
|
} else {
|
|
7055
7175
|
httpDecision = action;
|
|
7056
7176
|
}
|
|
7057
|
-
postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((
|
|
7177
|
+
postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err2) => {
|
|
7058
7178
|
try {
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
`[tail] POST /decision failed: ${String(
|
|
7179
|
+
fs25.appendFileSync(
|
|
7180
|
+
path28.join(os21.homedir(), ".node9", "hook-debug.log"),
|
|
7181
|
+
`[tail] POST /decision failed: ${String(err2)}
|
|
7062
7182
|
`
|
|
7063
7183
|
);
|
|
7064
7184
|
} catch {
|
|
@@ -7141,23 +7261,21 @@ async function startTail(options = {}) {
|
|
|
7141
7261
|
console.log(chalk17.cyan.bold(`
|
|
7142
7262
|
\u{1F6F0}\uFE0F Node9 tail `) + chalk17.dim(`\u2192 ${dashboardUrl}`));
|
|
7143
7263
|
if (canApprove) {
|
|
7144
|
-
console.log(
|
|
7145
|
-
|
|
7146
|
-
);
|
|
7264
|
+
console.log(chalk17.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
|
|
7265
|
+
console.log(chalk17.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
|
|
7147
7266
|
}
|
|
7148
7267
|
if (options.history) {
|
|
7149
|
-
console.log(chalk17.dim("Showing history + live events
|
|
7268
|
+
console.log(chalk17.dim("Showing history + live events.\n"));
|
|
7150
7269
|
} else {
|
|
7151
|
-
console.log(
|
|
7152
|
-
chalk17.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
|
|
7153
|
-
);
|
|
7270
|
+
console.log(chalk17.dim("Showing live events only. Use --history to include past.\n"));
|
|
7154
7271
|
}
|
|
7155
7272
|
process.on("SIGINT", () => {
|
|
7273
|
+
exitIdleMode();
|
|
7156
7274
|
clearCard();
|
|
7157
7275
|
process.stdout.write(SHOW_CURSOR);
|
|
7158
7276
|
if (process.stdout.isTTY) {
|
|
7159
|
-
|
|
7160
|
-
|
|
7277
|
+
readline5.clearLine(process.stdout, 0);
|
|
7278
|
+
readline5.cursorTo(process.stdout, 0);
|
|
7161
7279
|
}
|
|
7162
7280
|
console.log(chalk17.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
7163
7281
|
process.exit(0);
|
|
@@ -7168,11 +7286,12 @@ async function startTail(options = {}) {
|
|
|
7168
7286
|
console.error(chalk17.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
7169
7287
|
process.exit(1);
|
|
7170
7288
|
}
|
|
7289
|
+
if (canApprove) enterIdleMode();
|
|
7171
7290
|
let currentEvent = "";
|
|
7172
7291
|
let currentData = "";
|
|
7173
7292
|
res.on("error", () => {
|
|
7174
7293
|
});
|
|
7175
|
-
const rl =
|
|
7294
|
+
const rl = readline5.createInterface({ input: res, crlfDelay: Infinity });
|
|
7176
7295
|
rl.on("error", () => {
|
|
7177
7296
|
});
|
|
7178
7297
|
rl.on("line", (line) => {
|
|
@@ -7192,8 +7311,8 @@ async function startTail(options = {}) {
|
|
|
7192
7311
|
clearCard();
|
|
7193
7312
|
process.stdout.write(SHOW_CURSOR);
|
|
7194
7313
|
if (process.stdout.isTTY) {
|
|
7195
|
-
|
|
7196
|
-
|
|
7314
|
+
readline5.clearLine(process.stdout, 0);
|
|
7315
|
+
readline5.cursorTo(process.stdout, 0);
|
|
7197
7316
|
}
|
|
7198
7317
|
console.log(chalk17.red("\n\u274C Daemon disconnected."));
|
|
7199
7318
|
process.exit(1);
|
|
@@ -7271,6 +7390,18 @@ async function startTail(options = {}) {
|
|
|
7271
7390
|
const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
|
|
7272
7391
|
if (slowTool) renderPending(data);
|
|
7273
7392
|
}
|
|
7393
|
+
if (event === "snapshot") {
|
|
7394
|
+
const time = new Date(data.ts).toLocaleTimeString([], { hour12: false });
|
|
7395
|
+
const hash = data.hash ?? "";
|
|
7396
|
+
const summary = data.argsSummary ?? data.tool;
|
|
7397
|
+
const fileCount = data.fileCount ?? 0;
|
|
7398
|
+
const files = fileCount > 0 ? chalk17.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
|
|
7399
|
+
process.stdout.write(
|
|
7400
|
+
`${chalk17.dim(time)} ${chalk17.cyan("\u{1F4F8} snapshot")} ${chalk17.dim(hash)} ${summary}${files}
|
|
7401
|
+
`
|
|
7402
|
+
);
|
|
7403
|
+
return;
|
|
7404
|
+
}
|
|
7274
7405
|
if (event === "activity-result") {
|
|
7275
7406
|
const original = activityPending.get(data.id);
|
|
7276
7407
|
if (original) {
|
|
@@ -7279,8 +7410,8 @@ async function startTail(options = {}) {
|
|
|
7279
7410
|
}
|
|
7280
7411
|
}
|
|
7281
7412
|
}
|
|
7282
|
-
req.on("error", (
|
|
7283
|
-
const msg =
|
|
7413
|
+
req.on("error", (err2) => {
|
|
7414
|
+
const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
|
|
7284
7415
|
console.error(chalk17.red(`
|
|
7285
7416
|
\u274C ${msg}`));
|
|
7286
7417
|
process.exit(1);
|
|
@@ -7293,7 +7424,7 @@ var init_tail = __esm({
|
|
|
7293
7424
|
init_daemon2();
|
|
7294
7425
|
init_daemon();
|
|
7295
7426
|
init_core();
|
|
7296
|
-
PID_FILE =
|
|
7427
|
+
PID_FILE = path28.join(os21.homedir(), ".node9", "daemon.pid");
|
|
7297
7428
|
ICONS = {
|
|
7298
7429
|
bash: "\u{1F4BB}",
|
|
7299
7430
|
shell: "\u{1F4BB}",
|
|
@@ -7332,9 +7463,9 @@ __export(hud_exports, {
|
|
|
7332
7463
|
main: () => main,
|
|
7333
7464
|
renderEnvironmentLine: () => renderEnvironmentLine
|
|
7334
7465
|
});
|
|
7335
|
-
import
|
|
7336
|
-
import
|
|
7337
|
-
import
|
|
7466
|
+
import fs26 from "fs";
|
|
7467
|
+
import path29 from "path";
|
|
7468
|
+
import os22 from "os";
|
|
7338
7469
|
import http3 from "http";
|
|
7339
7470
|
async function readStdin() {
|
|
7340
7471
|
const chunks = [];
|
|
@@ -7410,9 +7541,9 @@ function formatTimeLeft(resetsAt) {
|
|
|
7410
7541
|
return ` (${m}m left)`;
|
|
7411
7542
|
}
|
|
7412
7543
|
function safeReadJson(filePath) {
|
|
7413
|
-
if (!
|
|
7544
|
+
if (!fs26.existsSync(filePath)) return null;
|
|
7414
7545
|
try {
|
|
7415
|
-
return JSON.parse(
|
|
7546
|
+
return JSON.parse(fs26.readFileSync(filePath, "utf-8"));
|
|
7416
7547
|
} catch {
|
|
7417
7548
|
return null;
|
|
7418
7549
|
}
|
|
@@ -7433,12 +7564,12 @@ function countHooksInFile(filePath) {
|
|
|
7433
7564
|
return Object.keys(cfg.hooks).length;
|
|
7434
7565
|
}
|
|
7435
7566
|
function countRulesInDir(rulesDir) {
|
|
7436
|
-
if (!
|
|
7567
|
+
if (!fs26.existsSync(rulesDir)) return 0;
|
|
7437
7568
|
let count = 0;
|
|
7438
7569
|
try {
|
|
7439
|
-
for (const entry of
|
|
7570
|
+
for (const entry of fs26.readdirSync(rulesDir, { withFileTypes: true })) {
|
|
7440
7571
|
if (entry.isDirectory()) {
|
|
7441
|
-
count += countRulesInDir(
|
|
7572
|
+
count += countRulesInDir(path29.join(rulesDir, entry.name));
|
|
7442
7573
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
7443
7574
|
count++;
|
|
7444
7575
|
}
|
|
@@ -7449,46 +7580,46 @@ function countRulesInDir(rulesDir) {
|
|
|
7449
7580
|
}
|
|
7450
7581
|
function isSamePath(a, b) {
|
|
7451
7582
|
try {
|
|
7452
|
-
return
|
|
7583
|
+
return path29.resolve(a) === path29.resolve(b);
|
|
7453
7584
|
} catch {
|
|
7454
7585
|
return false;
|
|
7455
7586
|
}
|
|
7456
7587
|
}
|
|
7457
7588
|
function countConfigs(cwd) {
|
|
7458
|
-
const homeDir2 =
|
|
7459
|
-
const claudeDir =
|
|
7589
|
+
const homeDir2 = os22.homedir();
|
|
7590
|
+
const claudeDir = path29.join(homeDir2, ".claude");
|
|
7460
7591
|
let claudeMdCount = 0;
|
|
7461
7592
|
let rulesCount = 0;
|
|
7462
7593
|
let hooksCount = 0;
|
|
7463
7594
|
const userMcpServers = /* @__PURE__ */ new Set();
|
|
7464
7595
|
const projectMcpServers = /* @__PURE__ */ new Set();
|
|
7465
|
-
if (
|
|
7466
|
-
rulesCount += countRulesInDir(
|
|
7467
|
-
const userSettings =
|
|
7596
|
+
if (fs26.existsSync(path29.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
7597
|
+
rulesCount += countRulesInDir(path29.join(claudeDir, "rules"));
|
|
7598
|
+
const userSettings = path29.join(claudeDir, "settings.json");
|
|
7468
7599
|
for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
|
|
7469
7600
|
hooksCount += countHooksInFile(userSettings);
|
|
7470
|
-
const userClaudeJson =
|
|
7601
|
+
const userClaudeJson = path29.join(homeDir2, ".claude.json");
|
|
7471
7602
|
for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
|
|
7472
7603
|
for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
|
|
7473
7604
|
userMcpServers.delete(name);
|
|
7474
7605
|
}
|
|
7475
7606
|
if (cwd) {
|
|
7476
|
-
if (
|
|
7477
|
-
if (
|
|
7478
|
-
const projectClaudeDir =
|
|
7607
|
+
if (fs26.existsSync(path29.join(cwd, "CLAUDE.md"))) claudeMdCount++;
|
|
7608
|
+
if (fs26.existsSync(path29.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
|
|
7609
|
+
const projectClaudeDir = path29.join(cwd, ".claude");
|
|
7479
7610
|
const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
|
|
7480
7611
|
if (!overlapsUserScope) {
|
|
7481
|
-
if (
|
|
7482
|
-
rulesCount += countRulesInDir(
|
|
7483
|
-
const projSettings =
|
|
7612
|
+
if (fs26.existsSync(path29.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
7613
|
+
rulesCount += countRulesInDir(path29.join(projectClaudeDir, "rules"));
|
|
7614
|
+
const projSettings = path29.join(projectClaudeDir, "settings.json");
|
|
7484
7615
|
for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
|
|
7485
7616
|
hooksCount += countHooksInFile(projSettings);
|
|
7486
7617
|
}
|
|
7487
|
-
if (
|
|
7488
|
-
const localSettings =
|
|
7618
|
+
if (fs26.existsSync(path29.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
|
|
7619
|
+
const localSettings = path29.join(projectClaudeDir, "settings.local.json");
|
|
7489
7620
|
for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
|
|
7490
7621
|
hooksCount += countHooksInFile(localSettings);
|
|
7491
|
-
const mcpJsonServers = getMcpServerNames(
|
|
7622
|
+
const mcpJsonServers = getMcpServerNames(path29.join(cwd, ".mcp.json"));
|
|
7492
7623
|
const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
|
|
7493
7624
|
for (const name of disabledMcpJson) mcpJsonServers.delete(name);
|
|
7494
7625
|
for (const name of mcpJsonServers) projectMcpServers.add(name);
|
|
@@ -7546,6 +7677,11 @@ function renderSecurityLine(status) {
|
|
|
7546
7677
|
parts.push(color(RED2, `\u{1F6A8} ${status.session.dlpHits} dlp`));
|
|
7547
7678
|
}
|
|
7548
7679
|
}
|
|
7680
|
+
if (status.session.estimatedCost > 0) {
|
|
7681
|
+
const cost = status.session.estimatedCost;
|
|
7682
|
+
const costStr = cost >= 0.01 ? `$${cost.toFixed(2)}` : cost >= 1e-3 ? `$${cost.toFixed(3)}` : "<$0.001";
|
|
7683
|
+
parts.push(color(DIM, `~${costStr}`));
|
|
7684
|
+
}
|
|
7549
7685
|
if (status.taintedCount > 0) {
|
|
7550
7686
|
parts.push(color(YELLOW2, `\u{1F4A7} ${status.taintedCount} tainted`));
|
|
7551
7687
|
}
|
|
@@ -7601,11 +7737,11 @@ async function main() {
|
|
|
7601
7737
|
try {
|
|
7602
7738
|
const cwd = stdin.cwd ?? process.cwd();
|
|
7603
7739
|
for (const configPath of [
|
|
7604
|
-
|
|
7605
|
-
|
|
7740
|
+
path29.join(cwd, "node9.config.json"),
|
|
7741
|
+
path29.join(os22.homedir(), ".node9", "config.json")
|
|
7606
7742
|
]) {
|
|
7607
|
-
if (!
|
|
7608
|
-
const cfg = JSON.parse(
|
|
7743
|
+
if (!fs26.existsSync(configPath)) continue;
|
|
7744
|
+
const cfg = JSON.parse(fs26.readFileSync(configPath, "utf-8"));
|
|
7609
7745
|
const hud = cfg.settings?.hud;
|
|
7610
7746
|
if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
|
|
7611
7747
|
}
|
|
@@ -7654,6 +7790,16 @@ import path14 from "path";
|
|
|
7654
7790
|
import os10 from "os";
|
|
7655
7791
|
import chalk from "chalk";
|
|
7656
7792
|
import { confirm } from "@inquirer/prompts";
|
|
7793
|
+
var NODE9_MCP_SERVER_ENTRY = { command: "node9", args: ["mcp-server"] };
|
|
7794
|
+
function hasNode9McpServer(servers) {
|
|
7795
|
+
const entry = servers["node9"];
|
|
7796
|
+
return !!entry && entry.command === "node9" && Array.isArray(entry.args) && entry.args[0] === "mcp-server";
|
|
7797
|
+
}
|
|
7798
|
+
function removeNode9McpServer(servers) {
|
|
7799
|
+
if (!hasNode9McpServer(servers)) return false;
|
|
7800
|
+
delete servers["node9"];
|
|
7801
|
+
return true;
|
|
7802
|
+
}
|
|
7657
7803
|
function printDaemonTip() {
|
|
7658
7804
|
console.log(
|
|
7659
7805
|
chalk.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + chalk.white("\n To view your history or manage persistent rules, run:") + chalk.green("\n node9 daemon --openui")
|
|
@@ -7711,6 +7857,10 @@ function teardownClaude() {
|
|
|
7711
7857
|
const claudeConfig = readJson(mcpPath);
|
|
7712
7858
|
if (claudeConfig?.mcpServers) {
|
|
7713
7859
|
let mcpChanged = false;
|
|
7860
|
+
if (removeNode9McpServer(claudeConfig.mcpServers)) {
|
|
7861
|
+
mcpChanged = true;
|
|
7862
|
+
console.log(chalk.green(" \u2705 Removed node9 MCP server entry from ~/.claude.json"));
|
|
7863
|
+
}
|
|
7714
7864
|
for (const [name, server] of Object.entries(claudeConfig.mcpServers)) {
|
|
7715
7865
|
if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
|
|
7716
7866
|
const [originalCmd, ...originalArgs] = server.args;
|
|
@@ -7754,6 +7904,10 @@ function teardownGemini() {
|
|
|
7754
7904
|
}
|
|
7755
7905
|
}
|
|
7756
7906
|
if (settings.mcpServers) {
|
|
7907
|
+
if (removeNode9McpServer(settings.mcpServers)) {
|
|
7908
|
+
changed = true;
|
|
7909
|
+
console.log(chalk.green(" \u2705 Removed node9 MCP server entry from ~/.gemini/settings.json"));
|
|
7910
|
+
}
|
|
7757
7911
|
for (const [name, server] of Object.entries(settings.mcpServers)) {
|
|
7758
7912
|
if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
|
|
7759
7913
|
const [originalCmd, ...originalArgs] = server.args;
|
|
@@ -7782,6 +7936,10 @@ function teardownCursor() {
|
|
|
7782
7936
|
return;
|
|
7783
7937
|
}
|
|
7784
7938
|
let changed = false;
|
|
7939
|
+
if (removeNode9McpServer(mcpConfig.mcpServers)) {
|
|
7940
|
+
changed = true;
|
|
7941
|
+
console.log(chalk.green(" \u2705 Removed node9 MCP server entry from ~/.cursor/mcp.json"));
|
|
7942
|
+
}
|
|
7785
7943
|
for (const [name, server] of Object.entries(mcpConfig.mcpServers)) {
|
|
7786
7944
|
if (server.command === "node9" && Array.isArray(server.args) && server.args.length > 0) {
|
|
7787
7945
|
const [originalCmd, ...originalArgs] = server.args;
|
|
@@ -7807,6 +7965,7 @@ async function setupClaude() {
|
|
|
7807
7965
|
const claudeConfig = readJson(mcpPath) ?? {};
|
|
7808
7966
|
const settings = readJson(hooksPath) ?? {};
|
|
7809
7967
|
const servers = claudeConfig.mcpServers ?? {};
|
|
7968
|
+
let hooksChanged = false;
|
|
7810
7969
|
let anythingChanged = false;
|
|
7811
7970
|
if (!settings.hooks) settings.hooks = {};
|
|
7812
7971
|
const hasPreHook = settings.hooks.PreToolUse?.some(
|
|
@@ -7819,6 +7978,7 @@ async function setupClaude() {
|
|
|
7819
7978
|
hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
|
|
7820
7979
|
});
|
|
7821
7980
|
console.log(chalk.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
|
|
7981
|
+
hooksChanged = true;
|
|
7822
7982
|
anythingChanged = true;
|
|
7823
7983
|
}
|
|
7824
7984
|
const hasPostHook = settings.hooks.PostToolUse?.some(
|
|
@@ -7831,9 +7991,17 @@ async function setupClaude() {
|
|
|
7831
7991
|
hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
|
|
7832
7992
|
});
|
|
7833
7993
|
console.log(chalk.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
|
|
7994
|
+
hooksChanged = true;
|
|
7834
7995
|
anythingChanged = true;
|
|
7835
7996
|
}
|
|
7836
|
-
if (
|
|
7997
|
+
if (!hasNode9McpServer(servers)) {
|
|
7998
|
+
servers["node9"] = NODE9_MCP_SERVER_ENTRY;
|
|
7999
|
+
claudeConfig.mcpServers = servers;
|
|
8000
|
+
writeJson(mcpPath, claudeConfig);
|
|
8001
|
+
console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
|
|
8002
|
+
anythingChanged = true;
|
|
8003
|
+
}
|
|
8004
|
+
if (hooksChanged) {
|
|
7837
8005
|
writeJson(hooksPath, settings);
|
|
7838
8006
|
console.log("");
|
|
7839
8007
|
}
|
|
@@ -7881,6 +8049,7 @@ async function setupGemini() {
|
|
|
7881
8049
|
const settingsPath = path14.join(homeDir2, ".gemini", "settings.json");
|
|
7882
8050
|
const settings = readJson(settingsPath) ?? {};
|
|
7883
8051
|
const servers = settings.mcpServers ?? {};
|
|
8052
|
+
let hooksChanged = false;
|
|
7884
8053
|
let anythingChanged = false;
|
|
7885
8054
|
if (!settings.hooks) settings.hooks = {};
|
|
7886
8055
|
const hasBeforeHook = Array.isArray(settings.hooks.BeforeTool) && settings.hooks.BeforeTool.some(
|
|
@@ -7901,6 +8070,7 @@ async function setupGemini() {
|
|
|
7901
8070
|
]
|
|
7902
8071
|
});
|
|
7903
8072
|
console.log(chalk.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
|
|
8073
|
+
hooksChanged = true;
|
|
7904
8074
|
anythingChanged = true;
|
|
7905
8075
|
}
|
|
7906
8076
|
const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
|
|
@@ -7914,9 +8084,17 @@ async function setupGemini() {
|
|
|
7914
8084
|
hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
|
|
7915
8085
|
});
|
|
7916
8086
|
console.log(chalk.green(" \u2705 AfterTool hook added \u2192 node9 log"));
|
|
8087
|
+
hooksChanged = true;
|
|
7917
8088
|
anythingChanged = true;
|
|
7918
8089
|
}
|
|
7919
|
-
if (
|
|
8090
|
+
if (!hasNode9McpServer(servers)) {
|
|
8091
|
+
servers["node9"] = NODE9_MCP_SERVER_ENTRY;
|
|
8092
|
+
settings.mcpServers = servers;
|
|
8093
|
+
console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
|
|
8094
|
+
hooksChanged = true;
|
|
8095
|
+
anythingChanged = true;
|
|
8096
|
+
}
|
|
8097
|
+
if (hooksChanged) {
|
|
7920
8098
|
writeJson(settingsPath, settings);
|
|
7921
8099
|
console.log("");
|
|
7922
8100
|
}
|
|
@@ -7963,10 +8141,10 @@ function detectAgents(homeDir2 = os10.homedir()) {
|
|
|
7963
8141
|
const exists = (p) => {
|
|
7964
8142
|
try {
|
|
7965
8143
|
return fs11.existsSync(p);
|
|
7966
|
-
} catch (
|
|
7967
|
-
const code =
|
|
8144
|
+
} catch (err2) {
|
|
8145
|
+
const code = err2.code;
|
|
7968
8146
|
if (code !== "ENOENT") {
|
|
7969
|
-
process.stderr.write(`[node9] detectAgents: cannot access ${p}: ${code ?? String(
|
|
8147
|
+
process.stderr.write(`[node9] detectAgents: cannot access ${p}: ${code ?? String(err2)}
|
|
7970
8148
|
`);
|
|
7971
8149
|
}
|
|
7972
8150
|
return false;
|
|
@@ -7984,6 +8162,13 @@ async function setupCursor() {
|
|
|
7984
8162
|
const mcpConfig = readJson(mcpPath) ?? {};
|
|
7985
8163
|
const servers = mcpConfig.mcpServers ?? {};
|
|
7986
8164
|
let anythingChanged = false;
|
|
8165
|
+
if (!hasNode9McpServer(servers)) {
|
|
8166
|
+
servers["node9"] = NODE9_MCP_SERVER_ENTRY;
|
|
8167
|
+
mcpConfig.mcpServers = servers;
|
|
8168
|
+
writeJson(mcpPath, mcpConfig);
|
|
8169
|
+
console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
|
|
8170
|
+
anythingChanged = true;
|
|
8171
|
+
}
|
|
7987
8172
|
const serversToWrap = [];
|
|
7988
8173
|
for (const [name, server] of Object.entries(servers)) {
|
|
7989
8174
|
if (!server.command || server.command === "node9") continue;
|
|
@@ -8083,9 +8268,9 @@ function teardownHud() {
|
|
|
8083
8268
|
// src/cli.ts
|
|
8084
8269
|
init_daemon2();
|
|
8085
8270
|
import chalk18 from "chalk";
|
|
8086
|
-
import
|
|
8087
|
-
import
|
|
8088
|
-
import
|
|
8271
|
+
import fs27 from "fs";
|
|
8272
|
+
import path30 from "path";
|
|
8273
|
+
import os23 from "os";
|
|
8089
8274
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
8090
8275
|
|
|
8091
8276
|
// src/utils/duration.ts
|
|
@@ -8320,8 +8505,29 @@ import os14 from "os";
|
|
|
8320
8505
|
import { spawnSync as spawnSync4, spawn as spawn5 } from "child_process";
|
|
8321
8506
|
import crypto2 from "crypto";
|
|
8322
8507
|
import fs17 from "fs";
|
|
8508
|
+
import net3 from "net";
|
|
8323
8509
|
import path19 from "path";
|
|
8324
8510
|
import os13 from "os";
|
|
8511
|
+
var ACTIVITY_SOCKET_PATH3 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path19.join(os13.tmpdir(), "node9-activity.sock");
|
|
8512
|
+
function notifySnapshotTaken(hash, tool, argsSummary, fileCount) {
|
|
8513
|
+
try {
|
|
8514
|
+
const payload = JSON.stringify({
|
|
8515
|
+
status: "snapshot",
|
|
8516
|
+
hash,
|
|
8517
|
+
tool,
|
|
8518
|
+
argsSummary,
|
|
8519
|
+
fileCount,
|
|
8520
|
+
ts: Date.now()
|
|
8521
|
+
});
|
|
8522
|
+
const sock = net3.createConnection(ACTIVITY_SOCKET_PATH3);
|
|
8523
|
+
sock.on("connect", () => {
|
|
8524
|
+
sock.end(payload);
|
|
8525
|
+
});
|
|
8526
|
+
sock.on("error", () => {
|
|
8527
|
+
});
|
|
8528
|
+
} catch {
|
|
8529
|
+
}
|
|
8530
|
+
}
|
|
8325
8531
|
var SNAPSHOT_STACK_PATH = path19.join(os13.homedir(), ".node9", "snapshots.json");
|
|
8326
8532
|
var UNDO_LATEST_PATH = path19.join(os13.homedir(), ".node9", "undo_latest.txt");
|
|
8327
8533
|
var MAX_SNAPSHOTS = 10;
|
|
@@ -8504,6 +8710,9 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
8504
8710
|
if (filesRes.status === 0) {
|
|
8505
8711
|
capturedFiles = filesRes.stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
8506
8712
|
}
|
|
8713
|
+
if (capturedFiles.length === 0) {
|
|
8714
|
+
return prevEntry.hash;
|
|
8715
|
+
}
|
|
8507
8716
|
const diffRes = spawnSync4("git", ["diff", prevEntry.hash, commitHash], {
|
|
8508
8717
|
env: shadowEnv,
|
|
8509
8718
|
timeout: GIT_TIMEOUT
|
|
@@ -8541,13 +8750,15 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
8541
8750
|
}
|
|
8542
8751
|
if (cwdCount > MAX_SNAPSHOTS) stack.splice(oldestCwdIdx, 1);
|
|
8543
8752
|
writeStack(stack);
|
|
8753
|
+
const entry = stack[stack.length - 1];
|
|
8754
|
+
notifySnapshotTaken(commitHash.slice(0, 7), tool, entry.argsSummary, capturedFiles.length);
|
|
8544
8755
|
fs17.writeFileSync(UNDO_LATEST_PATH, commitHash);
|
|
8545
8756
|
if (shouldGc) {
|
|
8546
8757
|
spawn5("git", ["gc", "--auto"], { env: shadowEnv, detached: true, stdio: "ignore" }).unref();
|
|
8547
8758
|
}
|
|
8548
8759
|
return commitHash;
|
|
8549
|
-
} catch (
|
|
8550
|
-
if (process.env.NODE9_DEBUG === "1") console.error("[Node9 Undo Engine Error]:",
|
|
8760
|
+
} catch (err2) {
|
|
8761
|
+
if (process.env.NODE9_DEBUG === "1") console.error("[Node9 Undo Engine Error]:", err2);
|
|
8551
8762
|
return null;
|
|
8552
8763
|
} finally {
|
|
8553
8764
|
if (indexFile) {
|
|
@@ -8651,11 +8862,11 @@ function registerCheckCommand(program2) {
|
|
|
8651
8862
|
let payload = JSON.parse(raw);
|
|
8652
8863
|
try {
|
|
8653
8864
|
payload = JSON.parse(raw);
|
|
8654
|
-
} catch (
|
|
8865
|
+
} catch (err2) {
|
|
8655
8866
|
const tempConfig = getConfig();
|
|
8656
8867
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
8657
8868
|
const logPath = path20.join(os14.homedir(), ".node9", "hook-debug.log");
|
|
8658
|
-
const errMsg =
|
|
8869
|
+
const errMsg = err2 instanceof Error ? err2.message : String(err2);
|
|
8659
8870
|
fs18.appendFileSync(
|
|
8660
8871
|
logPath,
|
|
8661
8872
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
@@ -8776,10 +8987,10 @@ RAW: ${raw}
|
|
|
8776
8987
|
...result,
|
|
8777
8988
|
blockedByLabel: result.blockedByLabel
|
|
8778
8989
|
});
|
|
8779
|
-
} catch (
|
|
8990
|
+
} catch (err2) {
|
|
8780
8991
|
if (process.env.NODE9_DEBUG === "1") {
|
|
8781
8992
|
const logPath = path20.join(os14.homedir(), ".node9", "hook-debug.log");
|
|
8782
|
-
const errMsg =
|
|
8993
|
+
const errMsg = err2 instanceof Error ? err2.message : String(err2);
|
|
8783
8994
|
fs18.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
8784
8995
|
`);
|
|
8785
8996
|
}
|
|
@@ -8925,8 +9136,8 @@ function registerLogCommand(program2) {
|
|
|
8925
9136
|
if (shouldSnapshot(tool, {}, config)) {
|
|
8926
9137
|
await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
|
|
8927
9138
|
}
|
|
8928
|
-
} catch (
|
|
8929
|
-
const msg =
|
|
9139
|
+
} catch (err2) {
|
|
9140
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
8930
9141
|
process.stderr.write(`[Node9] audit log error: ${msg}
|
|
8931
9142
|
`);
|
|
8932
9143
|
const debugPath = path21.join(os15.homedir(), ".node9", "hook-debug.log");
|
|
@@ -9699,24 +9910,78 @@ import chalk11 from "chalk";
|
|
|
9699
9910
|
import fs23 from "fs";
|
|
9700
9911
|
import path25 from "path";
|
|
9701
9912
|
import os19 from "os";
|
|
9913
|
+
import https from "https";
|
|
9914
|
+
function fireTelemetryPing(agents) {
|
|
9915
|
+
try {
|
|
9916
|
+
const body = JSON.stringify({
|
|
9917
|
+
event: "init_completed",
|
|
9918
|
+
agents_detected: agents,
|
|
9919
|
+
os: process.platform,
|
|
9920
|
+
node9_version: process.env.npm_package_version ?? "unknown"
|
|
9921
|
+
});
|
|
9922
|
+
const req = https.request(
|
|
9923
|
+
{
|
|
9924
|
+
hostname: "api.node9.ai",
|
|
9925
|
+
path: "/api/v1/telemetry",
|
|
9926
|
+
method: "POST",
|
|
9927
|
+
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
|
|
9928
|
+
timeout: 3e3
|
|
9929
|
+
},
|
|
9930
|
+
(res) => {
|
|
9931
|
+
res.resume();
|
|
9932
|
+
}
|
|
9933
|
+
);
|
|
9934
|
+
req.on("error", () => {
|
|
9935
|
+
});
|
|
9936
|
+
req.on("timeout", () => {
|
|
9937
|
+
req.destroy();
|
|
9938
|
+
});
|
|
9939
|
+
req.end(body);
|
|
9940
|
+
} catch {
|
|
9941
|
+
}
|
|
9942
|
+
}
|
|
9702
9943
|
function registerInitCommand(program2) {
|
|
9703
9944
|
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) => {
|
|
9704
9945
|
console.log(chalk11.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
|
|
9946
|
+
let chosenMode = options.mode.toLowerCase();
|
|
9947
|
+
if (!["standard", "strict", "audit"].includes(chosenMode)) {
|
|
9948
|
+
chosenMode = DEFAULT_CONFIG.settings.mode;
|
|
9949
|
+
}
|
|
9950
|
+
{
|
|
9951
|
+
const { confirm: confirm3 } = await import("@inquirer/prompts");
|
|
9952
|
+
const enableShields = await confirm3({
|
|
9953
|
+
message: "Enable recommended safety shields? (blocks rm -rf, SQL drops, pipe-to-shell)",
|
|
9954
|
+
default: true
|
|
9955
|
+
});
|
|
9956
|
+
if (enableShields) chosenMode = "standard";
|
|
9957
|
+
console.log("");
|
|
9958
|
+
}
|
|
9705
9959
|
const configPath = path25.join(os19.homedir(), ".node9", "config.json");
|
|
9706
9960
|
if (fs23.existsSync(configPath) && !options.force) {
|
|
9707
|
-
|
|
9961
|
+
try {
|
|
9962
|
+
const existing = JSON.parse(fs23.readFileSync(configPath, "utf-8"));
|
|
9963
|
+
const settings = existing.settings ?? {};
|
|
9964
|
+
if (settings.mode !== chosenMode) {
|
|
9965
|
+
settings.mode = chosenMode;
|
|
9966
|
+
existing.settings = settings;
|
|
9967
|
+
fs23.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
|
|
9968
|
+
console.log(chalk11.green(`\u2705 Mode updated: ${chosenMode}`));
|
|
9969
|
+
} else {
|
|
9970
|
+
console.log(chalk11.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
9971
|
+
}
|
|
9972
|
+
} catch {
|
|
9973
|
+
console.log(chalk11.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
9974
|
+
}
|
|
9708
9975
|
} else {
|
|
9709
|
-
const requestedMode = options.mode.toLowerCase();
|
|
9710
|
-
const safeMode = ["standard", "strict", "audit"].includes(requestedMode) ? requestedMode : DEFAULT_CONFIG.settings.mode;
|
|
9711
9976
|
const configToSave = {
|
|
9712
9977
|
...DEFAULT_CONFIG,
|
|
9713
|
-
settings: { ...DEFAULT_CONFIG.settings, mode:
|
|
9978
|
+
settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
|
|
9714
9979
|
};
|
|
9715
9980
|
const dir = path25.dirname(configPath);
|
|
9716
9981
|
if (!fs23.existsSync(dir)) fs23.mkdirSync(dir, { recursive: true });
|
|
9717
|
-
fs23.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
9982
|
+
fs23.writeFileSync(configPath, JSON.stringify(configToSave, null, 2) + "\n");
|
|
9718
9983
|
console.log(chalk11.green(`\u2705 Config created: ${configPath}`));
|
|
9719
|
-
console.log(chalk11.gray(` Mode: ${
|
|
9984
|
+
console.log(chalk11.gray(` Mode: ${chosenMode}`));
|
|
9720
9985
|
}
|
|
9721
9986
|
if (options.skipSetup) return;
|
|
9722
9987
|
console.log("");
|
|
@@ -9743,14 +10008,20 @@ function registerInitCommand(program2) {
|
|
|
9743
10008
|
else if (agent === "cursor") await setupCursor();
|
|
9744
10009
|
console.log("");
|
|
9745
10010
|
}
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
9749
|
-
|
|
10011
|
+
{
|
|
10012
|
+
const { confirm: confirm3 } = await import("@inquirer/prompts");
|
|
10013
|
+
const sendTelemetry = await confirm3({
|
|
10014
|
+
message: "Send anonymous usage stats to help improve node9? (no code, no args)",
|
|
10015
|
+
default: true
|
|
10016
|
+
});
|
|
10017
|
+
if (sendTelemetry) fireTelemetryPing(found);
|
|
9750
10018
|
console.log("");
|
|
9751
10019
|
}
|
|
9752
10020
|
console.log(chalk11.green.bold("\u{1F6E1}\uFE0F Node9 is ready!"));
|
|
9753
|
-
console.log(
|
|
10021
|
+
console.log("");
|
|
10022
|
+
console.log(chalk11.white(" Start watching: ") + chalk11.cyan("node9 tail"));
|
|
10023
|
+
console.log(chalk11.white(" Browser view: ") + chalk11.cyan("node9 daemon --openui"));
|
|
10024
|
+
console.log(chalk11.white(" Cloud dashboard: ") + chalk11.cyan("node9.ai"));
|
|
9754
10025
|
});
|
|
9755
10026
|
}
|
|
9756
10027
|
|
|
@@ -10312,6 +10583,347 @@ function registerMcpGatewayCommand(program2) {
|
|
|
10312
10583
|
});
|
|
10313
10584
|
}
|
|
10314
10585
|
|
|
10586
|
+
// src/mcp-server/index.ts
|
|
10587
|
+
import readline4 from "readline";
|
|
10588
|
+
import fs24 from "fs";
|
|
10589
|
+
import os20 from "os";
|
|
10590
|
+
import path27 from "path";
|
|
10591
|
+
init_core();
|
|
10592
|
+
init_daemon();
|
|
10593
|
+
init_shields();
|
|
10594
|
+
function ok(id, result) {
|
|
10595
|
+
return JSON.stringify({ jsonrpc: "2.0", id: id ?? null, result });
|
|
10596
|
+
}
|
|
10597
|
+
function err(id, code, message) {
|
|
10598
|
+
return JSON.stringify({ jsonrpc: "2.0", id: id ?? null, error: { code, message } });
|
|
10599
|
+
}
|
|
10600
|
+
var TOOLS = [
|
|
10601
|
+
{
|
|
10602
|
+
name: "node9_status",
|
|
10603
|
+
description: "Show the current node9 protection status: mode, daemon state, undo engine, pause state, active shields, and whether agent hooks are wired. Use this to understand what protection is active before doing risky work.",
|
|
10604
|
+
inputSchema: { type: "object", properties: {}, required: [] }
|
|
10605
|
+
},
|
|
10606
|
+
{
|
|
10607
|
+
name: "node9_config_get",
|
|
10608
|
+
description: "Read the current node9 configuration: security mode, approver channels, timeouts, DLP settings, and the number of active smart rules. Returns the merged config (env > cloud > project > global > defaults).",
|
|
10609
|
+
inputSchema: { type: "object", properties: {}, required: [] }
|
|
10610
|
+
},
|
|
10611
|
+
{
|
|
10612
|
+
name: "node9_shield_list",
|
|
10613
|
+
description: "List all available node9 shields and which ones are currently active. Shields are pre-packaged rule sets for specific services (postgres, aws, github, filesystem).",
|
|
10614
|
+
inputSchema: { type: "object", properties: {}, required: [] }
|
|
10615
|
+
},
|
|
10616
|
+
{
|
|
10617
|
+
name: "node9_shield_enable",
|
|
10618
|
+
description: "Enable a node9 shield for a specific service. Shields only add protection \u2014 they cannot be used to weaken or bypass node9. Use node9_shield_list to see available shield names.",
|
|
10619
|
+
inputSchema: {
|
|
10620
|
+
type: "object",
|
|
10621
|
+
properties: {
|
|
10622
|
+
service: {
|
|
10623
|
+
type: "string",
|
|
10624
|
+
description: 'Shield name to enable (e.g. "postgres", "aws", "github", "filesystem").'
|
|
10625
|
+
}
|
|
10626
|
+
},
|
|
10627
|
+
required: ["service"]
|
|
10628
|
+
}
|
|
10629
|
+
},
|
|
10630
|
+
{
|
|
10631
|
+
name: "node9_approver_list",
|
|
10632
|
+
description: "List all node9 approver channels and their current enabled/disabled state. Approvers are the channels through which node9 asks a human to approve risky tool calls. Channels: native (OS popup), browser (web UI), cloud (team policy server), terminal (stdin).",
|
|
10633
|
+
inputSchema: { type: "object", properties: {}, required: [] }
|
|
10634
|
+
},
|
|
10635
|
+
{
|
|
10636
|
+
name: "node9_approver_set",
|
|
10637
|
+
description: "Enable or disable a specific node9 approver channel in the global config (~/.node9/config.json). Use this to turn individual channels on or off without touching other settings. Channels: native, browser, cloud, terminal. WARNING: disabling all approvers means node9 cannot prompt for human approval \u2014 use with care.",
|
|
10638
|
+
inputSchema: {
|
|
10639
|
+
type: "object",
|
|
10640
|
+
properties: {
|
|
10641
|
+
channel: {
|
|
10642
|
+
type: "string",
|
|
10643
|
+
enum: ["native", "browser", "cloud", "terminal"],
|
|
10644
|
+
description: "Approver channel to configure."
|
|
10645
|
+
},
|
|
10646
|
+
enabled: {
|
|
10647
|
+
type: "boolean",
|
|
10648
|
+
description: "true to enable the channel, false to disable it."
|
|
10649
|
+
}
|
|
10650
|
+
},
|
|
10651
|
+
required: ["channel", "enabled"]
|
|
10652
|
+
}
|
|
10653
|
+
},
|
|
10654
|
+
{
|
|
10655
|
+
name: "node9_undo_list",
|
|
10656
|
+
description: "List the node9 snapshot history. Each entry shows the git hash, tool that triggered it, a short summary, affected files, working directory, and timestamp. Use this to find a hash before calling node9_undo_revert.",
|
|
10657
|
+
inputSchema: { type: "object", properties: {}, required: [] }
|
|
10658
|
+
},
|
|
10659
|
+
{
|
|
10660
|
+
name: "node9_undo_revert",
|
|
10661
|
+
description: "Revert the working directory to a specific node9 snapshot. Call node9_undo_list first to find the hash you want to restore. WARNING: this overwrites current files \u2014 any unsaved work will be lost.",
|
|
10662
|
+
inputSchema: {
|
|
10663
|
+
type: "object",
|
|
10664
|
+
properties: {
|
|
10665
|
+
hash: {
|
|
10666
|
+
type: "string",
|
|
10667
|
+
description: "The full git commit hash from node9_undo_list."
|
|
10668
|
+
},
|
|
10669
|
+
cwd: {
|
|
10670
|
+
type: "string",
|
|
10671
|
+
description: "Absolute path to the project directory. Defaults to process.cwd()."
|
|
10672
|
+
}
|
|
10673
|
+
},
|
|
10674
|
+
required: ["hash"]
|
|
10675
|
+
}
|
|
10676
|
+
}
|
|
10677
|
+
];
|
|
10678
|
+
function handleStatus() {
|
|
10679
|
+
const config = getConfig();
|
|
10680
|
+
const settings = config.settings;
|
|
10681
|
+
const paused = checkPause();
|
|
10682
|
+
const daemonUp = isDaemonRunning();
|
|
10683
|
+
const activeShields = readActiveShields();
|
|
10684
|
+
const lines = [];
|
|
10685
|
+
lines.push(`Mode: ${settings.mode}`);
|
|
10686
|
+
lines.push(`Daemon: ${daemonUp ? "running" : "stopped"}`);
|
|
10687
|
+
lines.push(`Undo engine: ${settings.enableUndo ? "enabled" : "disabled"}`);
|
|
10688
|
+
if (paused.paused) {
|
|
10689
|
+
const until = paused.expiresAt ? new Date(paused.expiresAt).toLocaleTimeString() : "indefinitely";
|
|
10690
|
+
lines.push(`PAUSED until ${until} \u2014 all tool calls currently allowed`);
|
|
10691
|
+
} else {
|
|
10692
|
+
lines.push(`Pause state: not paused`);
|
|
10693
|
+
}
|
|
10694
|
+
lines.push(`Active shields: ${activeShields.length > 0 ? activeShields.join(", ") : "none"}`);
|
|
10695
|
+
lines.push(`Smart rules: ${config.policy.smartRules.length} loaded`);
|
|
10696
|
+
lines.push(`DLP: ${config.policy.dlp?.enabled !== false ? "enabled" : "disabled"}`);
|
|
10697
|
+
const projectConfig = path27.join(process.cwd(), "node9.config.json");
|
|
10698
|
+
const globalConfig = path27.join(os20.homedir(), ".node9", "config.json");
|
|
10699
|
+
lines.push(
|
|
10700
|
+
`Project config (node9.config.json): ${fs24.existsSync(projectConfig) ? "present" : "not found"}`
|
|
10701
|
+
);
|
|
10702
|
+
lines.push(
|
|
10703
|
+
`Global config (~/.node9/config.json): ${fs24.existsSync(globalConfig) ? "present" : "not found"}`
|
|
10704
|
+
);
|
|
10705
|
+
return lines.join("\n");
|
|
10706
|
+
}
|
|
10707
|
+
function handleConfigGet() {
|
|
10708
|
+
const config = getConfig();
|
|
10709
|
+
const s = config.settings;
|
|
10710
|
+
const lines = [
|
|
10711
|
+
`mode: ${s.mode}`,
|
|
10712
|
+
`enableUndo: ${s.enableUndo}`,
|
|
10713
|
+
`flightRecorder: ${s.flightRecorder}`,
|
|
10714
|
+
`approvalTimeoutMs: ${s.approvalTimeoutMs}`,
|
|
10715
|
+
`approvers:`,
|
|
10716
|
+
` native: ${s.approvers.native}`,
|
|
10717
|
+
` browser: ${s.approvers.browser}`,
|
|
10718
|
+
` cloud: ${s.approvers.cloud}`,
|
|
10719
|
+
` terminal: ${s.approvers.terminal}`,
|
|
10720
|
+
`dlp.enabled: ${config.policy.dlp?.enabled !== false}`,
|
|
10721
|
+
`dlp.scanIgnoredTools: ${config.policy.dlp?.scanIgnoredTools !== false}`,
|
|
10722
|
+
`smartRules: ${config.policy.smartRules.length} active`,
|
|
10723
|
+
`sandboxPaths: ${config.policy.sandboxPaths.length > 0 ? config.policy.sandboxPaths.join(", ") : "none"}`
|
|
10724
|
+
];
|
|
10725
|
+
return lines.join("\n");
|
|
10726
|
+
}
|
|
10727
|
+
function handleShieldList() {
|
|
10728
|
+
const all = listShields();
|
|
10729
|
+
const active = new Set(readActiveShields());
|
|
10730
|
+
if (all.length === 0) return "No shields available.";
|
|
10731
|
+
const lines = all.map((shield) => {
|
|
10732
|
+
const on = active.has(shield.name);
|
|
10733
|
+
const ruleCount = shield.smartRules.length;
|
|
10734
|
+
return `${on ? "[active]" : "[off] "} ${shield.name.padEnd(12)} \u2014 ${shield.description ?? ""} (${ruleCount} rule${ruleCount === 1 ? "" : "s"})`;
|
|
10735
|
+
});
|
|
10736
|
+
lines.unshift(`${active.size} of ${all.length} shields active:
|
|
10737
|
+
`);
|
|
10738
|
+
return lines.join("\n");
|
|
10739
|
+
}
|
|
10740
|
+
function handleShieldEnable(args) {
|
|
10741
|
+
const service = args.service;
|
|
10742
|
+
if (typeof service !== "string" || !service) {
|
|
10743
|
+
throw new Error("service is required");
|
|
10744
|
+
}
|
|
10745
|
+
const name = resolveShieldName(service);
|
|
10746
|
+
if (!name) {
|
|
10747
|
+
throw new Error(
|
|
10748
|
+
`Unknown shield: "${service}". Run node9_shield_list to see available shields.`
|
|
10749
|
+
);
|
|
10750
|
+
}
|
|
10751
|
+
const active = readActiveShields();
|
|
10752
|
+
if (active.includes(name)) {
|
|
10753
|
+
return `Shield "${name}" is already active.`;
|
|
10754
|
+
}
|
|
10755
|
+
writeActiveShields([...active, name]);
|
|
10756
|
+
const shield = getShield(name);
|
|
10757
|
+
return `Shield "${name}" enabled \u2014 ${shield.smartRules.length} smart rule${shield.smartRules.length === 1 ? "" : "s"} now active.`;
|
|
10758
|
+
}
|
|
10759
|
+
var GLOBAL_CONFIG_PATH2 = path27.join(os20.homedir(), ".node9", "config.json");
|
|
10760
|
+
var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
|
|
10761
|
+
function readGlobalConfigRaw() {
|
|
10762
|
+
try {
|
|
10763
|
+
if (fs24.existsSync(GLOBAL_CONFIG_PATH2)) {
|
|
10764
|
+
return JSON.parse(fs24.readFileSync(GLOBAL_CONFIG_PATH2, "utf-8"));
|
|
10765
|
+
}
|
|
10766
|
+
} catch {
|
|
10767
|
+
}
|
|
10768
|
+
return {};
|
|
10769
|
+
}
|
|
10770
|
+
function writeGlobalConfigRaw(data) {
|
|
10771
|
+
const dir = path27.dirname(GLOBAL_CONFIG_PATH2);
|
|
10772
|
+
if (!fs24.existsSync(dir)) fs24.mkdirSync(dir, { recursive: true });
|
|
10773
|
+
fs24.writeFileSync(GLOBAL_CONFIG_PATH2, JSON.stringify(data, null, 2) + "\n");
|
|
10774
|
+
}
|
|
10775
|
+
function handleApproverList() {
|
|
10776
|
+
const config = getConfig();
|
|
10777
|
+
const approvers = config.settings.approvers;
|
|
10778
|
+
const lines = ["Approver channels:\n"];
|
|
10779
|
+
for (const ch of APPROVER_CHANNELS) {
|
|
10780
|
+
const on = approvers[ch];
|
|
10781
|
+
lines.push(` ${on ? "[enabled] " : "[disabled]"} ${ch}`);
|
|
10782
|
+
}
|
|
10783
|
+
const enabledCount = APPROVER_CHANNELS.filter((ch) => approvers[ch]).length;
|
|
10784
|
+
if (enabledCount === 0) {
|
|
10785
|
+
lines.push("\nWARNING: all approver channels are disabled \u2014 node9 cannot prompt for approval.");
|
|
10786
|
+
}
|
|
10787
|
+
return lines.join("\n");
|
|
10788
|
+
}
|
|
10789
|
+
function handleApproverSet(args) {
|
|
10790
|
+
const channel = args.channel;
|
|
10791
|
+
const enabled = args.enabled;
|
|
10792
|
+
if (!channel || !APPROVER_CHANNELS.includes(channel)) {
|
|
10793
|
+
throw new Error(
|
|
10794
|
+
`Invalid channel: "${channel}". Must be one of: ${APPROVER_CHANNELS.join(", ")}.`
|
|
10795
|
+
);
|
|
10796
|
+
}
|
|
10797
|
+
if (typeof enabled !== "boolean") {
|
|
10798
|
+
throw new Error("enabled must be a boolean (true or false).");
|
|
10799
|
+
}
|
|
10800
|
+
const raw = readGlobalConfigRaw();
|
|
10801
|
+
const settings = raw.settings ?? {};
|
|
10802
|
+
const approvers = settings.approvers ?? {};
|
|
10803
|
+
approvers[channel] = enabled;
|
|
10804
|
+
settings.approvers = approvers;
|
|
10805
|
+
raw.settings = settings;
|
|
10806
|
+
writeGlobalConfigRaw(raw);
|
|
10807
|
+
const currentApprovers = getConfig().settings.approvers;
|
|
10808
|
+
const anyEnabled = APPROVER_CHANNELS.some(
|
|
10809
|
+
(ch) => ch === channel ? enabled : currentApprovers[ch]
|
|
10810
|
+
);
|
|
10811
|
+
const suffix = anyEnabled ? "" : "\nWARNING: all approver channels are now disabled \u2014 node9 cannot prompt for approval.";
|
|
10812
|
+
return `Approver channel "${channel}" ${enabled ? "enabled" : "disabled"} in ~/.node9/config.json.${suffix}`;
|
|
10813
|
+
}
|
|
10814
|
+
function handleUndoList() {
|
|
10815
|
+
const history = getSnapshotHistory();
|
|
10816
|
+
if (history.length === 0) {
|
|
10817
|
+
return "No snapshots found. Node9 captures snapshots automatically before file edits.";
|
|
10818
|
+
}
|
|
10819
|
+
const lines = history.slice().reverse().map((entry, i) => {
|
|
10820
|
+
const date = new Date(entry.timestamp).toLocaleString();
|
|
10821
|
+
const files = entry.files?.length ? `${entry.files.length} file(s)` : "unknown files";
|
|
10822
|
+
const summary = entry.argsSummary ? ` \u2014 ${entry.argsSummary}` : "";
|
|
10823
|
+
return `[${i + 1}] ${entry.hash.slice(0, 7)} ${date} ${entry.tool}${summary} (${files}) cwd: ${entry.cwd}
|
|
10824
|
+
full hash: ${entry.hash}`;
|
|
10825
|
+
});
|
|
10826
|
+
return lines.join("\n\n");
|
|
10827
|
+
}
|
|
10828
|
+
function handleUndoRevert(args) {
|
|
10829
|
+
const hash = args.hash;
|
|
10830
|
+
if (typeof hash !== "string" || !hash) {
|
|
10831
|
+
throw new Error("hash is required and must be a non-empty string");
|
|
10832
|
+
}
|
|
10833
|
+
if (!/^[0-9a-f]{7,40}$/i.test(hash)) {
|
|
10834
|
+
throw new Error(`Invalid hash format: ${hash}`);
|
|
10835
|
+
}
|
|
10836
|
+
const cwd = typeof args.cwd === "string" && args.cwd ? args.cwd : process.cwd();
|
|
10837
|
+
const success = applyUndo(hash, cwd);
|
|
10838
|
+
if (!success) {
|
|
10839
|
+
throw new Error(
|
|
10840
|
+
`Revert failed for hash ${hash}. The snapshot may not exist for this directory, or git encountered an error.`
|
|
10841
|
+
);
|
|
10842
|
+
}
|
|
10843
|
+
return `Successfully reverted to snapshot ${hash.slice(0, 7)} in ${cwd}.`;
|
|
10844
|
+
}
|
|
10845
|
+
function runMcpServer() {
|
|
10846
|
+
const rl = readline4.createInterface({ input: process.stdin, terminal: false });
|
|
10847
|
+
rl.on("line", (line) => {
|
|
10848
|
+
let msg;
|
|
10849
|
+
try {
|
|
10850
|
+
msg = JSON.parse(line);
|
|
10851
|
+
} catch {
|
|
10852
|
+
process.stdout.write(err(null, -32700, "Parse error") + "\n");
|
|
10853
|
+
return;
|
|
10854
|
+
}
|
|
10855
|
+
const { method, id, params } = msg;
|
|
10856
|
+
if (method === "initialize") {
|
|
10857
|
+
process.stdout.write(
|
|
10858
|
+
ok(id, {
|
|
10859
|
+
protocolVersion: "2024-11-05",
|
|
10860
|
+
serverInfo: { name: "node9", version: "1.0.0" },
|
|
10861
|
+
capabilities: { tools: {} }
|
|
10862
|
+
}) + "\n"
|
|
10863
|
+
);
|
|
10864
|
+
return;
|
|
10865
|
+
}
|
|
10866
|
+
if (id === void 0 || id === null) {
|
|
10867
|
+
return;
|
|
10868
|
+
}
|
|
10869
|
+
if (method === "tools/list") {
|
|
10870
|
+
process.stdout.write(ok(id, { tools: TOOLS }) + "\n");
|
|
10871
|
+
return;
|
|
10872
|
+
}
|
|
10873
|
+
if (method === "tools/call") {
|
|
10874
|
+
const p = params ?? {};
|
|
10875
|
+
const toolName = p.name;
|
|
10876
|
+
const toolArgs = p.arguments ?? {};
|
|
10877
|
+
try {
|
|
10878
|
+
let text;
|
|
10879
|
+
if (toolName === "node9_status") {
|
|
10880
|
+
text = handleStatus();
|
|
10881
|
+
} else if (toolName === "node9_config_get") {
|
|
10882
|
+
text = handleConfigGet();
|
|
10883
|
+
} else if (toolName === "node9_shield_list") {
|
|
10884
|
+
text = handleShieldList();
|
|
10885
|
+
} else if (toolName === "node9_shield_enable") {
|
|
10886
|
+
text = handleShieldEnable(toolArgs);
|
|
10887
|
+
} else if (toolName === "node9_approver_list") {
|
|
10888
|
+
text = handleApproverList();
|
|
10889
|
+
} else if (toolName === "node9_approver_set") {
|
|
10890
|
+
text = handleApproverSet(toolArgs);
|
|
10891
|
+
} else if (toolName === "node9_undo_list") {
|
|
10892
|
+
text = handleUndoList();
|
|
10893
|
+
} else if (toolName === "node9_undo_revert") {
|
|
10894
|
+
text = handleUndoRevert(toolArgs);
|
|
10895
|
+
} else {
|
|
10896
|
+
process.stdout.write(err(id, -32601, `Unknown tool: ${toolName}`) + "\n");
|
|
10897
|
+
return;
|
|
10898
|
+
}
|
|
10899
|
+
process.stdout.write(ok(id, { content: [{ type: "text", text }] }) + "\n");
|
|
10900
|
+
} catch (e) {
|
|
10901
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
10902
|
+
process.stdout.write(
|
|
10903
|
+
ok(id, {
|
|
10904
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
10905
|
+
isError: true
|
|
10906
|
+
}) + "\n"
|
|
10907
|
+
);
|
|
10908
|
+
}
|
|
10909
|
+
return;
|
|
10910
|
+
}
|
|
10911
|
+
process.stdout.write(err(id, -32601, `Method not found: ${method}`) + "\n");
|
|
10912
|
+
});
|
|
10913
|
+
rl.on("close", () => {
|
|
10914
|
+
process.exit(0);
|
|
10915
|
+
});
|
|
10916
|
+
}
|
|
10917
|
+
|
|
10918
|
+
// src/cli/commands/mcp-server.ts
|
|
10919
|
+
function registerMcpServerCommand(program2) {
|
|
10920
|
+
program2.command("mcp-server").description(
|
|
10921
|
+
"Run the Node9 MCP server \u2014 exposes node9 tools (undo, rules, \u2026) to Claude, Cursor, and Gemini"
|
|
10922
|
+
).action(() => {
|
|
10923
|
+
runMcpServer();
|
|
10924
|
+
});
|
|
10925
|
+
}
|
|
10926
|
+
|
|
10315
10927
|
// src/cli/commands/trust.ts
|
|
10316
10928
|
init_trusted_hosts();
|
|
10317
10929
|
import chalk16 from "chalk";
|
|
@@ -10369,20 +10981,20 @@ function registerTrustCommand(program2) {
|
|
|
10369
10981
|
|
|
10370
10982
|
// src/cli.ts
|
|
10371
10983
|
var { version } = JSON.parse(
|
|
10372
|
-
|
|
10984
|
+
fs27.readFileSync(path30.join(__dirname, "../package.json"), "utf-8")
|
|
10373
10985
|
);
|
|
10374
10986
|
var program = new Command();
|
|
10375
10987
|
program.name("node9").description("The Sudo Command for AI Agents").version(version);
|
|
10376
10988
|
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) => {
|
|
10377
10989
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
10378
|
-
const credPath =
|
|
10379
|
-
if (!
|
|
10380
|
-
|
|
10990
|
+
const credPath = path30.join(os23.homedir(), ".node9", "credentials.json");
|
|
10991
|
+
if (!fs27.existsSync(path30.dirname(credPath)))
|
|
10992
|
+
fs27.mkdirSync(path30.dirname(credPath), { recursive: true });
|
|
10381
10993
|
const profileName = options.profile || "default";
|
|
10382
10994
|
let existingCreds = {};
|
|
10383
10995
|
try {
|
|
10384
|
-
if (
|
|
10385
|
-
const raw = JSON.parse(
|
|
10996
|
+
if (fs27.existsSync(credPath)) {
|
|
10997
|
+
const raw = JSON.parse(fs27.readFileSync(credPath, "utf-8"));
|
|
10386
10998
|
if (raw.apiKey) {
|
|
10387
10999
|
existingCreds = {
|
|
10388
11000
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
|
|
@@ -10394,13 +11006,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
10394
11006
|
} catch {
|
|
10395
11007
|
}
|
|
10396
11008
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
10397
|
-
|
|
11009
|
+
fs27.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
10398
11010
|
if (profileName === "default") {
|
|
10399
|
-
const configPath =
|
|
11011
|
+
const configPath = path30.join(os23.homedir(), ".node9", "config.json");
|
|
10400
11012
|
let config = {};
|
|
10401
11013
|
try {
|
|
10402
|
-
if (
|
|
10403
|
-
config = JSON.parse(
|
|
11014
|
+
if (fs27.existsSync(configPath))
|
|
11015
|
+
config = JSON.parse(fs27.readFileSync(configPath, "utf-8"));
|
|
10404
11016
|
} catch {
|
|
10405
11017
|
}
|
|
10406
11018
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -10415,9 +11027,9 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
10415
11027
|
approvers.cloud = false;
|
|
10416
11028
|
}
|
|
10417
11029
|
s.approvers = approvers;
|
|
10418
|
-
if (!
|
|
10419
|
-
|
|
10420
|
-
|
|
11030
|
+
if (!fs27.existsSync(path30.dirname(configPath)))
|
|
11031
|
+
fs27.mkdirSync(path30.dirname(configPath), { recursive: true });
|
|
11032
|
+
fs27.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
10421
11033
|
}
|
|
10422
11034
|
if (options.profile && profileName !== "default") {
|
|
10423
11035
|
console.log(chalk18.green(`\u2705 Profile "${profileName}" saved`));
|
|
@@ -10477,8 +11089,8 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
|
|
|
10477
11089
|
`));
|
|
10478
11090
|
try {
|
|
10479
11091
|
fn();
|
|
10480
|
-
} catch (
|
|
10481
|
-
console.error(chalk18.red(` \u26A0\uFE0F Failed: ${
|
|
11092
|
+
} catch (err2) {
|
|
11093
|
+
console.error(chalk18.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
10482
11094
|
process.exit(1);
|
|
10483
11095
|
}
|
|
10484
11096
|
console.log(chalk18.gray("\n Restart the agent for changes to take effect."));
|
|
@@ -10501,25 +11113,25 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
10501
11113
|
]) {
|
|
10502
11114
|
try {
|
|
10503
11115
|
fn();
|
|
10504
|
-
} catch (
|
|
11116
|
+
} catch (err2) {
|
|
10505
11117
|
teardownFailed = true;
|
|
10506
11118
|
console.error(
|
|
10507
11119
|
chalk18.red(
|
|
10508
|
-
` \u26A0\uFE0F Failed to remove ${label} hooks: ${
|
|
11120
|
+
` \u26A0\uFE0F Failed to remove ${label} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
10509
11121
|
)
|
|
10510
11122
|
);
|
|
10511
11123
|
}
|
|
10512
11124
|
}
|
|
10513
11125
|
if (options.purge) {
|
|
10514
|
-
const node9Dir =
|
|
10515
|
-
if (
|
|
11126
|
+
const node9Dir = path30.join(os23.homedir(), ".node9");
|
|
11127
|
+
if (fs27.existsSync(node9Dir)) {
|
|
10516
11128
|
const confirmed = await confirm2({
|
|
10517
11129
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
10518
11130
|
default: false
|
|
10519
11131
|
});
|
|
10520
11132
|
if (confirmed) {
|
|
10521
|
-
|
|
10522
|
-
if (
|
|
11133
|
+
fs27.rmSync(node9Dir, { recursive: true });
|
|
11134
|
+
if (fs27.existsSync(node9Dir)) {
|
|
10523
11135
|
console.error(
|
|
10524
11136
|
chalk18.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
10525
11137
|
);
|
|
@@ -10626,13 +11238,14 @@ program.command("tail").description("Stream live agent activity to the terminal"
|
|
|
10626
11238
|
const { startTail: startTail2 } = await Promise.resolve().then(() => (init_tail(), tail_exports));
|
|
10627
11239
|
try {
|
|
10628
11240
|
await startTail2(options);
|
|
10629
|
-
} catch (
|
|
10630
|
-
console.error(chalk18.red(`\u274C ${
|
|
11241
|
+
} catch (err2) {
|
|
11242
|
+
console.error(chalk18.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
10631
11243
|
process.exit(1);
|
|
10632
11244
|
}
|
|
10633
11245
|
});
|
|
10634
11246
|
registerWatchCommand(program);
|
|
10635
11247
|
registerMcpGatewayCommand(program);
|
|
11248
|
+
registerMcpServerCommand(program);
|
|
10636
11249
|
registerCheckCommand(program);
|
|
10637
11250
|
registerLogCommand(program);
|
|
10638
11251
|
program.command("hud").description("Render node9 security statusline (spawned by Claude Code statusLine)").action(async () => {
|
|
@@ -10735,9 +11348,9 @@ if (process.argv[2] !== "daemon") {
|
|
|
10735
11348
|
const isCheckHook = process.argv[2] === "check";
|
|
10736
11349
|
if (isCheckHook) {
|
|
10737
11350
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
10738
|
-
const logPath =
|
|
11351
|
+
const logPath = path30.join(os23.homedir(), ".node9", "hook-debug.log");
|
|
10739
11352
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
10740
|
-
|
|
11353
|
+
fs27.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
10741
11354
|
`);
|
|
10742
11355
|
}
|
|
10743
11356
|
process.exit(0);
|