@node9/proxy 1.5.2 → 1.5.3
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 +60 -7
- package/dist/cli.js +945 -190
- package/dist/cli.mjs +942 -187
- package/dist/index.js +156 -47
- package/dist/index.mjs +156 -47
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -191,12 +191,21 @@ var init_config_schema = __esm({
|
|
|
191
191
|
verdict: z.enum(["allow", "review", "block"], {
|
|
192
192
|
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
193
193
|
}),
|
|
194
|
-
reason: z.string().optional()
|
|
194
|
+
reason: z.string().optional(),
|
|
195
|
+
// Unknown predicate names are filtered out rather than failing the whole rule.
|
|
196
|
+
// Failing the whole z.array() would cause sanitizeConfig to drop the entire
|
|
197
|
+
// `policy` top-level key, silently disabling ALL smart rules in the config.
|
|
198
|
+
dependsOnState: z.array(z.string()).transform(
|
|
199
|
+
(arr) => arr.filter(
|
|
200
|
+
(p) => p === "no_test_passed_since_last_edit"
|
|
201
|
+
)
|
|
202
|
+
).optional(),
|
|
203
|
+
recoveryCommand: z.string().optional()
|
|
195
204
|
});
|
|
196
205
|
ConfigFileSchema = z.object({
|
|
197
206
|
version: z.string().optional(),
|
|
198
207
|
settings: z.object({
|
|
199
|
-
mode: z.enum(["standard", "strict", "audit"]).optional(),
|
|
208
|
+
mode: z.enum(["standard", "strict", "audit", "observe"]).optional(),
|
|
200
209
|
autoStartDaemon: z.boolean().optional(),
|
|
201
210
|
enableUndo: z.boolean().optional(),
|
|
202
211
|
enableHookLogDebug: z.boolean().optional(),
|
|
@@ -626,12 +635,17 @@ function getConfig(cwd) {
|
|
|
626
635
|
if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
|
|
627
636
|
mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
|
|
628
637
|
if (s.environment !== void 0) mergedSettings.environment = s.environment;
|
|
638
|
+
if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
|
|
629
639
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
630
640
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
631
641
|
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
632
642
|
if (p.toolInspection)
|
|
633
643
|
mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
634
|
-
if (p.smartRules)
|
|
644
|
+
if (p.smartRules) {
|
|
645
|
+
const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
|
|
646
|
+
const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
|
|
647
|
+
mergedPolicy.smartRules = [...defaultBlocks, ...p.smartRules, ...defaultNonBlocks];
|
|
648
|
+
}
|
|
635
649
|
if (p.snapshot) {
|
|
636
650
|
const s2 = p.snapshot;
|
|
637
651
|
if (s2.tools) mergedPolicy.snapshot.tools.push(...s2.tools);
|
|
@@ -1893,7 +1907,13 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1893
1907
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
1894
1908
|
reason: matchedRule.reason,
|
|
1895
1909
|
tier: 2,
|
|
1896
|
-
ruleName: matchedRule.name ?? matchedRule.tool
|
|
1910
|
+
ruleName: matchedRule.name ?? matchedRule.tool,
|
|
1911
|
+
...matchedRule.verdict === "block" && matchedRule.dependsOnState?.length && {
|
|
1912
|
+
dependsOnStatePredicates: matchedRule.dependsOnState
|
|
1913
|
+
},
|
|
1914
|
+
...matchedRule.verdict === "block" && matchedRule.recoveryCommand && {
|
|
1915
|
+
recoveryCommand: matchedRule.recoveryCommand
|
|
1916
|
+
}
|
|
1897
1917
|
};
|
|
1898
1918
|
}
|
|
1899
1919
|
}
|
|
@@ -2416,9 +2436,38 @@ var init_state = __esm({
|
|
|
2416
2436
|
|
|
2417
2437
|
// src/auth/daemon.ts
|
|
2418
2438
|
import fs9 from "fs";
|
|
2439
|
+
import net from "net";
|
|
2419
2440
|
import path10 from "path";
|
|
2420
2441
|
import os8 from "os";
|
|
2421
2442
|
import { spawnSync } from "child_process";
|
|
2443
|
+
function notifyActivitySocket(data) {
|
|
2444
|
+
return new Promise((resolve) => {
|
|
2445
|
+
try {
|
|
2446
|
+
const payload = JSON.stringify(data);
|
|
2447
|
+
const sock = net.createConnection(ACTIVITY_SOCKET_PATH);
|
|
2448
|
+
sock.on("connect", () => {
|
|
2449
|
+
sock.on("close", resolve);
|
|
2450
|
+
sock.end(payload);
|
|
2451
|
+
});
|
|
2452
|
+
sock.on("error", resolve);
|
|
2453
|
+
} catch {
|
|
2454
|
+
resolve();
|
|
2455
|
+
}
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
async function checkStatePredicates(predicates) {
|
|
2459
|
+
if (predicates.length === 0) return {};
|
|
2460
|
+
try {
|
|
2461
|
+
const qs = predicates.map(encodeURIComponent).join(",");
|
|
2462
|
+
const res = await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/state/check?predicates=${qs}`, {
|
|
2463
|
+
signal: AbortSignal.timeout(100)
|
|
2464
|
+
});
|
|
2465
|
+
if (!res.ok) return null;
|
|
2466
|
+
return await res.json();
|
|
2467
|
+
} catch {
|
|
2468
|
+
return null;
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2422
2471
|
function getInternalToken() {
|
|
2423
2472
|
try {
|
|
2424
2473
|
const pidFile = path10.join(os8.homedir(), ".node9", "daemon.pid");
|
|
@@ -2452,7 +2501,7 @@ function isDaemonRunning() {
|
|
|
2452
2501
|
return false;
|
|
2453
2502
|
}
|
|
2454
2503
|
}
|
|
2455
|
-
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd) {
|
|
2504
|
+
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly) {
|
|
2456
2505
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2457
2506
|
const ctrl = new AbortController();
|
|
2458
2507
|
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
@@ -2470,7 +2519,10 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
2470
2519
|
// activity-result as the CLI used for the pending activity event.
|
|
2471
2520
|
activityId,
|
|
2472
2521
|
...riskMetadata && { riskMetadata },
|
|
2473
|
-
...cwd && { cwd }
|
|
2522
|
+
...cwd && { cwd },
|
|
2523
|
+
...recoveryCommand && { recoveryCommand },
|
|
2524
|
+
...skipBackgroundAuth && { skipBackgroundAuth: true },
|
|
2525
|
+
...viewOnly && { viewOnly: true }
|
|
2474
2526
|
}),
|
|
2475
2527
|
signal: ctrl.signal
|
|
2476
2528
|
});
|
|
@@ -2490,10 +2542,10 @@ async function waitForDaemonDecision(id, signal) {
|
|
|
2490
2542
|
try {
|
|
2491
2543
|
const waitRes = await fetch(`${base}/wait/${id}`, { signal: waitCtrl.signal });
|
|
2492
2544
|
if (!waitRes.ok) return { decision: "deny" };
|
|
2493
|
-
const { decision, source } = await waitRes.json();
|
|
2545
|
+
const { decision, source, reason } = await waitRes.json();
|
|
2494
2546
|
if (decision === "allow") return { decision: "allow", source };
|
|
2495
2547
|
if (decision === "abandoned") return { decision: "abandoned", source };
|
|
2496
|
-
return { decision: "deny", source };
|
|
2548
|
+
return { decision: "deny", source, reason };
|
|
2497
2549
|
} finally {
|
|
2498
2550
|
clearTimeout(waitTimer);
|
|
2499
2551
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
@@ -2579,10 +2631,11 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
|
2579
2631
|
signal: AbortSignal.timeout(3e3)
|
|
2580
2632
|
});
|
|
2581
2633
|
}
|
|
2582
|
-
var DAEMON_PORT, DAEMON_HOST;
|
|
2634
|
+
var ACTIVITY_SOCKET_PATH, DAEMON_PORT, DAEMON_HOST;
|
|
2583
2635
|
var init_daemon = __esm({
|
|
2584
2636
|
"src/auth/daemon.ts"() {
|
|
2585
2637
|
"use strict";
|
|
2638
|
+
ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path10.join(os8.tmpdir(), "node9-activity.sock");
|
|
2586
2639
|
DAEMON_PORT = 7391;
|
|
2587
2640
|
DAEMON_HOST = "127.0.0.1";
|
|
2588
2641
|
}
|
|
@@ -3036,9 +3089,6 @@ var init_cloud = __esm({
|
|
|
3036
3089
|
});
|
|
3037
3090
|
|
|
3038
3091
|
// src/auth/orchestrator.ts
|
|
3039
|
-
import net from "net";
|
|
3040
|
-
import path13 from "path";
|
|
3041
|
-
import os10 from "os";
|
|
3042
3092
|
import { randomUUID } from "crypto";
|
|
3043
3093
|
function isWriteTool(toolName) {
|
|
3044
3094
|
const t = toolName.toLowerCase().replace(/[^a-z_]/g, "_");
|
|
@@ -3075,19 +3125,7 @@ function isNetworkTool(toolName, args) {
|
|
|
3075
3125
|
return false;
|
|
3076
3126
|
}
|
|
3077
3127
|
function notifyActivity(data) {
|
|
3078
|
-
return
|
|
3079
|
-
try {
|
|
3080
|
-
const payload = JSON.stringify(data);
|
|
3081
|
-
const sock = net.createConnection(ACTIVITY_SOCKET_PATH);
|
|
3082
|
-
sock.on("connect", () => {
|
|
3083
|
-
sock.on("close", resolve);
|
|
3084
|
-
sock.end(payload);
|
|
3085
|
-
});
|
|
3086
|
-
sock.on("error", resolve);
|
|
3087
|
-
} catch {
|
|
3088
|
-
resolve();
|
|
3089
|
-
}
|
|
3090
|
-
});
|
|
3128
|
+
return notifyActivitySocket(data);
|
|
3091
3129
|
}
|
|
3092
3130
|
async function authorizeHeadless(toolName, args, meta, options) {
|
|
3093
3131
|
if (!options?.calledFromDaemon) {
|
|
@@ -3104,7 +3142,9 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
3104
3142
|
tool: toolName,
|
|
3105
3143
|
ts: actTs,
|
|
3106
3144
|
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
|
|
3107
|
-
label: result.blockedByLabel
|
|
3145
|
+
label: result.blockedByLabel,
|
|
3146
|
+
ruleHit: result.ruleHit,
|
|
3147
|
+
observeWouldBlock: result.observeWouldBlock
|
|
3108
3148
|
});
|
|
3109
3149
|
}
|
|
3110
3150
|
return result;
|
|
@@ -3131,10 +3171,12 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3131
3171
|
appendHookDebug(toolName, args, meta, hashAuditArgs);
|
|
3132
3172
|
}
|
|
3133
3173
|
const isManual = meta?.agent === "Terminal";
|
|
3174
|
+
const isObserveMode = config.settings.mode === "observe";
|
|
3134
3175
|
let explainableLabel = "Local Config";
|
|
3135
3176
|
let policyMatchedField;
|
|
3136
3177
|
let policyMatchedWord;
|
|
3137
3178
|
let riskMetadata;
|
|
3179
|
+
let statefulRecoveryCommand;
|
|
3138
3180
|
let taintWarning = null;
|
|
3139
3181
|
if (isNetworkTool(toolName, args)) {
|
|
3140
3182
|
const filePaths = extractFilePaths(toolName, args);
|
|
@@ -3155,10 +3197,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3155
3197
|
if (dlpMatch) {
|
|
3156
3198
|
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
3157
3199
|
if (dlpMatch.severity === "block") {
|
|
3158
|
-
if (!isManual)
|
|
3200
|
+
if (!isManual)
|
|
3201
|
+
appendLocalAudit(
|
|
3202
|
+
toolName,
|
|
3203
|
+
args,
|
|
3204
|
+
"deny",
|
|
3205
|
+
isObserveMode ? "observe-mode-dlp-would-block" : "dlp-block",
|
|
3206
|
+
meta,
|
|
3207
|
+
true
|
|
3208
|
+
);
|
|
3159
3209
|
if (isWriteTool(toolName) && filePath) {
|
|
3160
3210
|
await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`);
|
|
3161
3211
|
}
|
|
3212
|
+
if (isObserveMode) {
|
|
3213
|
+
return {
|
|
3214
|
+
approved: true,
|
|
3215
|
+
checkedBy: "audit",
|
|
3216
|
+
observeWouldBlock: true,
|
|
3217
|
+
blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
|
|
3218
|
+
};
|
|
3219
|
+
}
|
|
3162
3220
|
return {
|
|
3163
3221
|
approved: false,
|
|
3164
3222
|
reason: dlpReason,
|
|
@@ -3171,6 +3229,31 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3171
3229
|
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
3172
3230
|
}
|
|
3173
3231
|
}
|
|
3232
|
+
if (isObserveMode) {
|
|
3233
|
+
if (!isIgnoredTool(toolName)) {
|
|
3234
|
+
const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
|
|
3235
|
+
const wouldBlock = policyResult.decision === "block";
|
|
3236
|
+
if (!isManual)
|
|
3237
|
+
appendLocalAudit(
|
|
3238
|
+
toolName,
|
|
3239
|
+
args,
|
|
3240
|
+
"allow",
|
|
3241
|
+
wouldBlock ? "observe-mode-would-block" : "observe-mode",
|
|
3242
|
+
meta,
|
|
3243
|
+
hashAuditArgs
|
|
3244
|
+
);
|
|
3245
|
+
return {
|
|
3246
|
+
approved: true,
|
|
3247
|
+
checkedBy: "audit",
|
|
3248
|
+
...wouldBlock && {
|
|
3249
|
+
observeWouldBlock: true,
|
|
3250
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
3251
|
+
ruleHit: policyResult.ruleName
|
|
3252
|
+
}
|
|
3253
|
+
};
|
|
3254
|
+
}
|
|
3255
|
+
return { approved: true, checkedBy: "audit" };
|
|
3256
|
+
}
|
|
3174
3257
|
if (config.settings.mode === "audit") {
|
|
3175
3258
|
if (!isIgnoredTool(toolName)) {
|
|
3176
3259
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
|
|
@@ -3198,14 +3281,34 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3198
3281
|
return { approved: true, checkedBy: "local-policy" };
|
|
3199
3282
|
}
|
|
3200
3283
|
if (policyResult.decision === "block") {
|
|
3201
|
-
if (
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3284
|
+
if (policyResult.dependsOnStatePredicates?.length) {
|
|
3285
|
+
const stateResults = await checkStatePredicates(policyResult.dependsOnStatePredicates);
|
|
3286
|
+
const predicatesMet = stateResults !== null && policyResult.dependsOnStatePredicates.every((p) => stateResults[p] === true);
|
|
3287
|
+
if (stateResults === null && !isManual) {
|
|
3288
|
+
appendToLog(HOOK_DEBUG_LOG, {
|
|
3289
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3290
|
+
event: "state-check-fail-open",
|
|
3291
|
+
tool: toolName,
|
|
3292
|
+
rule: policyResult.ruleName,
|
|
3293
|
+
predicates: policyResult.dependsOnStatePredicates,
|
|
3294
|
+
reason: "daemon unreachable or /state/check timed out \u2014 block rule downgraded to review"
|
|
3295
|
+
});
|
|
3296
|
+
}
|
|
3297
|
+
if (predicatesMet && policyResult.recoveryCommand) {
|
|
3298
|
+
statefulRecoveryCommand = policyResult.recoveryCommand;
|
|
3299
|
+
}
|
|
3300
|
+
} else {
|
|
3301
|
+
if (!isManual)
|
|
3302
|
+
appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
|
|
3303
|
+
return {
|
|
3304
|
+
approved: false,
|
|
3305
|
+
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
3306
|
+
blockedBy: "local-config",
|
|
3307
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
3308
|
+
ruleHit: policyResult.ruleName,
|
|
3309
|
+
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand }
|
|
3310
|
+
};
|
|
3311
|
+
}
|
|
3209
3312
|
}
|
|
3210
3313
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
3211
3314
|
policyMatchedField = policyResult.matchedField;
|
|
@@ -3312,7 +3415,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3312
3415
|
meta,
|
|
3313
3416
|
riskMetadata,
|
|
3314
3417
|
options?.activityId,
|
|
3315
|
-
options?.cwd
|
|
3418
|
+
options?.cwd,
|
|
3419
|
+
statefulRecoveryCommand
|
|
3316
3420
|
);
|
|
3317
3421
|
daemonEntryId = entry.id;
|
|
3318
3422
|
daemonAllowCount = entry.allowCount;
|
|
@@ -3373,20 +3477,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3373
3477
|
if (daemonEntryId && (approvers.browser || approvers.terminal)) {
|
|
3374
3478
|
racePromises.push(
|
|
3375
3479
|
(async () => {
|
|
3376
|
-
const {
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3480
|
+
const {
|
|
3481
|
+
decision: daemonDecision,
|
|
3482
|
+
source: decisionSource,
|
|
3483
|
+
reason: daemonReason
|
|
3484
|
+
} = await waitForDaemonDecision(daemonEntryId, signal);
|
|
3380
3485
|
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
3381
3486
|
const isApproved = daemonDecision === "allow";
|
|
3382
|
-
const
|
|
3487
|
+
const isRedirect = decisionSource === "terminal-redirect";
|
|
3488
|
+
const src = decisionSource === "terminal" || decisionSource === "terminal-redirect" || decisionSource === "browser" ? decisionSource === "browser" ? "browser" : "terminal" : approvers.browser ? "browser" : "terminal";
|
|
3383
3489
|
const via = src === "terminal" ? "Terminal (node9 tail)" : "Browser Dashboard";
|
|
3384
3490
|
return {
|
|
3385
3491
|
approved: isApproved,
|
|
3386
|
-
reason: isApproved ? void 0 :
|
|
3492
|
+
reason: isApproved ? void 0 : (
|
|
3493
|
+
// Use the redirect reason from the tail when choice [2] was selected;
|
|
3494
|
+
// otherwise fall back to the generic rejection message.
|
|
3495
|
+
isRedirect && daemonReason || `The human user rejected this action via the Node9 ${via}.`
|
|
3496
|
+
),
|
|
3387
3497
|
checkedBy: isApproved ? "daemon" : void 0,
|
|
3388
3498
|
blockedBy: isApproved ? void 0 : "local-decision",
|
|
3389
|
-
blockedByLabel: `User Decision (${via})`,
|
|
3499
|
+
blockedByLabel: isRedirect ? "Steered Redirect (Terminal)" : `User Decision (${via})`,
|
|
3390
3500
|
decisionSource: src
|
|
3391
3501
|
};
|
|
3392
3502
|
})()
|
|
@@ -3461,7 +3571,7 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
3461
3571
|
}
|
|
3462
3572
|
return finalResult;
|
|
3463
3573
|
}
|
|
3464
|
-
var WRITE_TOOLS
|
|
3574
|
+
var WRITE_TOOLS;
|
|
3465
3575
|
var init_orchestrator = __esm({
|
|
3466
3576
|
"src/auth/orchestrator.ts"() {
|
|
3467
3577
|
"use strict";
|
|
@@ -3485,7 +3595,6 @@ var init_orchestrator = __esm({
|
|
|
3485
3595
|
"notebook_edit",
|
|
3486
3596
|
"notebookedit"
|
|
3487
3597
|
]);
|
|
3488
|
-
ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path13.join(os10.tmpdir(), "node9-activity.sock");
|
|
3489
3598
|
}
|
|
3490
3599
|
});
|
|
3491
3600
|
|
|
@@ -5213,7 +5322,7 @@ var init_suggestion_tracker = __esm({
|
|
|
5213
5322
|
|
|
5214
5323
|
// src/daemon/taint-store.ts
|
|
5215
5324
|
import fs12 from "fs";
|
|
5216
|
-
import
|
|
5325
|
+
import path14 from "path";
|
|
5217
5326
|
var DEFAULT_TTL_MS, TaintStore;
|
|
5218
5327
|
var init_taint_store = __esm({
|
|
5219
5328
|
"src/daemon/taint-store.ts"() {
|
|
@@ -5282,20 +5391,121 @@ var init_taint_store = __esm({
|
|
|
5282
5391
|
/** Resolve to absolute path, falling back to path.resolve if file doesn't exist yet. */
|
|
5283
5392
|
_resolve(filePath) {
|
|
5284
5393
|
try {
|
|
5285
|
-
return fs12.realpathSync.native(
|
|
5394
|
+
return fs12.realpathSync.native(path14.resolve(filePath));
|
|
5286
5395
|
} catch {
|
|
5287
|
-
return
|
|
5396
|
+
return path14.resolve(filePath);
|
|
5288
5397
|
}
|
|
5289
5398
|
}
|
|
5290
5399
|
};
|
|
5291
5400
|
}
|
|
5292
5401
|
});
|
|
5293
5402
|
|
|
5403
|
+
// src/daemon/session-counters.ts
|
|
5404
|
+
var SessionCounters, sessionCounters;
|
|
5405
|
+
var init_session_counters = __esm({
|
|
5406
|
+
"src/daemon/session-counters.ts"() {
|
|
5407
|
+
"use strict";
|
|
5408
|
+
SessionCounters = class {
|
|
5409
|
+
_allowed = 0;
|
|
5410
|
+
_blocked = 0;
|
|
5411
|
+
_dlpHits = 0;
|
|
5412
|
+
_wouldBlock = 0;
|
|
5413
|
+
_lastRuleHit = null;
|
|
5414
|
+
_lastBlockedTool = null;
|
|
5415
|
+
incrementAllowed() {
|
|
5416
|
+
this._allowed++;
|
|
5417
|
+
}
|
|
5418
|
+
incrementBlocked() {
|
|
5419
|
+
this._blocked++;
|
|
5420
|
+
}
|
|
5421
|
+
incrementDlpHits() {
|
|
5422
|
+
this._dlpHits++;
|
|
5423
|
+
}
|
|
5424
|
+
incrementWouldBlock() {
|
|
5425
|
+
this._wouldBlock++;
|
|
5426
|
+
}
|
|
5427
|
+
recordRuleHit(label) {
|
|
5428
|
+
this._lastRuleHit = label;
|
|
5429
|
+
}
|
|
5430
|
+
recordBlockedTool(toolName) {
|
|
5431
|
+
this._lastBlockedTool = toolName;
|
|
5432
|
+
}
|
|
5433
|
+
get() {
|
|
5434
|
+
return {
|
|
5435
|
+
allowed: this._allowed,
|
|
5436
|
+
blocked: this._blocked,
|
|
5437
|
+
dlpHits: this._dlpHits,
|
|
5438
|
+
wouldBlock: this._wouldBlock,
|
|
5439
|
+
lastRuleHit: this._lastRuleHit,
|
|
5440
|
+
lastBlockedTool: this._lastBlockedTool
|
|
5441
|
+
};
|
|
5442
|
+
}
|
|
5443
|
+
reset() {
|
|
5444
|
+
this._allowed = 0;
|
|
5445
|
+
this._blocked = 0;
|
|
5446
|
+
this._dlpHits = 0;
|
|
5447
|
+
this._wouldBlock = 0;
|
|
5448
|
+
this._lastRuleHit = null;
|
|
5449
|
+
this._lastBlockedTool = null;
|
|
5450
|
+
}
|
|
5451
|
+
};
|
|
5452
|
+
sessionCounters = new SessionCounters();
|
|
5453
|
+
}
|
|
5454
|
+
});
|
|
5455
|
+
|
|
5456
|
+
// src/daemon/session-history.ts
|
|
5457
|
+
var SessionHistory, sessionHistory;
|
|
5458
|
+
var init_session_history = __esm({
|
|
5459
|
+
"src/daemon/session-history.ts"() {
|
|
5460
|
+
"use strict";
|
|
5461
|
+
SessionHistory = class {
|
|
5462
|
+
lastEditAt = null;
|
|
5463
|
+
lastTestPassAt = null;
|
|
5464
|
+
lastTestFailAt = null;
|
|
5465
|
+
recordEdit(ts = Date.now()) {
|
|
5466
|
+
this.lastEditAt = ts;
|
|
5467
|
+
}
|
|
5468
|
+
recordTestPass(ts = Date.now()) {
|
|
5469
|
+
this.lastTestPassAt = ts;
|
|
5470
|
+
}
|
|
5471
|
+
recordTestFail(ts = Date.now()) {
|
|
5472
|
+
this.lastTestFailAt = ts;
|
|
5473
|
+
}
|
|
5474
|
+
/**
|
|
5475
|
+
* Returns true when the named predicate is currently satisfied.
|
|
5476
|
+
* Unknown predicates always return false (fail-open: don't block on unknown state).
|
|
5477
|
+
*/
|
|
5478
|
+
checkPredicate(name) {
|
|
5479
|
+
switch (name) {
|
|
5480
|
+
case "no_test_passed_since_last_edit":
|
|
5481
|
+
if (this.lastEditAt === null) return false;
|
|
5482
|
+
return this.lastTestPassAt === null || this.lastTestPassAt < this.lastEditAt;
|
|
5483
|
+
default:
|
|
5484
|
+
return false;
|
|
5485
|
+
}
|
|
5486
|
+
}
|
|
5487
|
+
getSnapshot() {
|
|
5488
|
+
return {
|
|
5489
|
+
lastEditAt: this.lastEditAt,
|
|
5490
|
+
lastTestPassAt: this.lastTestPassAt,
|
|
5491
|
+
lastTestFailAt: this.lastTestFailAt
|
|
5492
|
+
};
|
|
5493
|
+
}
|
|
5494
|
+
reset() {
|
|
5495
|
+
this.lastEditAt = null;
|
|
5496
|
+
this.lastTestPassAt = null;
|
|
5497
|
+
this.lastTestFailAt = null;
|
|
5498
|
+
}
|
|
5499
|
+
};
|
|
5500
|
+
sessionHistory = new SessionHistory();
|
|
5501
|
+
}
|
|
5502
|
+
});
|
|
5503
|
+
|
|
5294
5504
|
// src/daemon/state.ts
|
|
5295
5505
|
import net2 from "net";
|
|
5296
5506
|
import fs13 from "fs";
|
|
5297
|
-
import
|
|
5298
|
-
import
|
|
5507
|
+
import path15 from "path";
|
|
5508
|
+
import os11 from "os";
|
|
5299
5509
|
import { spawn as spawn2 } from "child_process";
|
|
5300
5510
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
5301
5511
|
function loadInsightCounts() {
|
|
@@ -5340,7 +5550,7 @@ function markRejectionHandlerRegistered() {
|
|
|
5340
5550
|
daemonRejectionHandlerRegistered = true;
|
|
5341
5551
|
}
|
|
5342
5552
|
function atomicWriteSync2(filePath, data, options) {
|
|
5343
|
-
const dir =
|
|
5553
|
+
const dir = path15.dirname(filePath);
|
|
5344
5554
|
if (!fs13.existsSync(dir)) fs13.mkdirSync(dir, { recursive: true });
|
|
5345
5555
|
const tmpPath = `${filePath}.${randomUUID3()}.tmp`;
|
|
5346
5556
|
try {
|
|
@@ -5380,7 +5590,7 @@ function appendAuditLog(data) {
|
|
|
5380
5590
|
decision: data.decision,
|
|
5381
5591
|
source: "daemon"
|
|
5382
5592
|
};
|
|
5383
|
-
const dir =
|
|
5593
|
+
const dir = path15.dirname(AUDIT_LOG_FILE);
|
|
5384
5594
|
if (!fs13.existsSync(dir)) fs13.mkdirSync(dir, { recursive: true });
|
|
5385
5595
|
fs13.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
|
|
5386
5596
|
} catch {
|
|
@@ -5529,6 +5739,14 @@ function startActivitySocket() {
|
|
|
5529
5739
|
socket.on("end", () => {
|
|
5530
5740
|
try {
|
|
5531
5741
|
const data = JSON.parse(Buffer.concat(chunks).toString());
|
|
5742
|
+
if (data.status === "test_pass") {
|
|
5743
|
+
sessionHistory.recordTestPass(data.ts);
|
|
5744
|
+
return;
|
|
5745
|
+
}
|
|
5746
|
+
if (data.status === "test_fail") {
|
|
5747
|
+
sessionHistory.recordTestFail(data.ts);
|
|
5748
|
+
return;
|
|
5749
|
+
}
|
|
5532
5750
|
if (data.status === "pending") {
|
|
5533
5751
|
broadcast("activity", {
|
|
5534
5752
|
id: data.id,
|
|
@@ -5538,6 +5756,24 @@ function startActivitySocket() {
|
|
|
5538
5756
|
status: "pending"
|
|
5539
5757
|
});
|
|
5540
5758
|
} else {
|
|
5759
|
+
if (data.status === "allow") {
|
|
5760
|
+
sessionCounters.incrementAllowed();
|
|
5761
|
+
if (data.observeWouldBlock) sessionCounters.incrementWouldBlock();
|
|
5762
|
+
if (WRITE_TOOL_NAMES.has(data.tool.toLowerCase().replace(/[^a-z_]/g, "_"))) {
|
|
5763
|
+
sessionHistory.recordEdit(data.ts);
|
|
5764
|
+
}
|
|
5765
|
+
} else if (data.status === "block") {
|
|
5766
|
+
sessionCounters.incrementBlocked();
|
|
5767
|
+
sessionCounters.recordBlockedTool(data.tool);
|
|
5768
|
+
if (data.ruleHit) sessionCounters.recordRuleHit(data.ruleHit);
|
|
5769
|
+
} else if (data.status === "dlp") {
|
|
5770
|
+
sessionCounters.incrementBlocked();
|
|
5771
|
+
sessionCounters.incrementDlpHits();
|
|
5772
|
+
sessionCounters.recordBlockedTool(data.tool);
|
|
5773
|
+
} else if (data.status === "taint") {
|
|
5774
|
+
sessionCounters.incrementBlocked();
|
|
5775
|
+
sessionCounters.recordBlockedTool(data.tool);
|
|
5776
|
+
}
|
|
5541
5777
|
broadcast("activity-result", {
|
|
5542
5778
|
id: data.id,
|
|
5543
5779
|
status: data.status,
|
|
@@ -5558,21 +5794,23 @@ function startActivitySocket() {
|
|
|
5558
5794
|
}
|
|
5559
5795
|
});
|
|
5560
5796
|
}
|
|
5561
|
-
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;
|
|
5797
|
+
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;
|
|
5562
5798
|
var init_state2 = __esm({
|
|
5563
5799
|
"src/daemon/state.ts"() {
|
|
5564
5800
|
"use strict";
|
|
5565
5801
|
init_daemon();
|
|
5566
5802
|
init_suggestion_tracker();
|
|
5567
5803
|
init_taint_store();
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5804
|
+
init_session_counters();
|
|
5805
|
+
init_session_history();
|
|
5806
|
+
homeDir = os11.homedir();
|
|
5807
|
+
DAEMON_PID_FILE = path15.join(homeDir, ".node9", "daemon.pid");
|
|
5808
|
+
DECISIONS_FILE = path15.join(homeDir, ".node9", "decisions.json");
|
|
5809
|
+
AUDIT_LOG_FILE = path15.join(homeDir, ".node9", "audit.log");
|
|
5810
|
+
TRUST_FILE2 = path15.join(homeDir, ".node9", "trust.json");
|
|
5811
|
+
GLOBAL_CONFIG_FILE = path15.join(homeDir, ".node9", "config.json");
|
|
5812
|
+
CREDENTIALS_FILE = path15.join(homeDir, ".node9", "credentials.json");
|
|
5813
|
+
INSIGHT_COUNTS_FILE = path15.join(homeDir, ".node9", "insight-counts.json");
|
|
5576
5814
|
pending = /* @__PURE__ */ new Map();
|
|
5577
5815
|
sseClients = /* @__PURE__ */ new Set();
|
|
5578
5816
|
suggestionTracker = new SuggestionTracker(3);
|
|
@@ -5590,17 +5828,28 @@ var init_state2 = __esm({
|
|
|
5590
5828
|
"2h": 2 * 60 * 6e4
|
|
5591
5829
|
};
|
|
5592
5830
|
autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
5593
|
-
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" :
|
|
5831
|
+
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path15.join(os11.tmpdir(), "node9-activity.sock");
|
|
5594
5832
|
ACTIVITY_RING_SIZE = 100;
|
|
5595
5833
|
activityRing = [];
|
|
5596
5834
|
SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
5835
|
+
WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
5836
|
+
"write",
|
|
5837
|
+
"write_file",
|
|
5838
|
+
"create_file",
|
|
5839
|
+
"edit",
|
|
5840
|
+
"multiedit",
|
|
5841
|
+
"str_replace_based_edit_tool",
|
|
5842
|
+
"replace",
|
|
5843
|
+
"notebook_edit",
|
|
5844
|
+
"notebookedit"
|
|
5845
|
+
]);
|
|
5597
5846
|
}
|
|
5598
5847
|
});
|
|
5599
5848
|
|
|
5600
5849
|
// src/config/patch.ts
|
|
5601
5850
|
import fs14 from "fs";
|
|
5602
|
-
import
|
|
5603
|
-
import
|
|
5851
|
+
import path16 from "path";
|
|
5852
|
+
import os12 from "os";
|
|
5604
5853
|
function patchConfig(configPath, patch) {
|
|
5605
5854
|
let config = {};
|
|
5606
5855
|
try {
|
|
@@ -5624,7 +5873,7 @@ function patchConfig(configPath, patch) {
|
|
|
5624
5873
|
ignored.push(patch.toolName);
|
|
5625
5874
|
}
|
|
5626
5875
|
}
|
|
5627
|
-
const dir =
|
|
5876
|
+
const dir = path16.dirname(configPath);
|
|
5628
5877
|
fs14.mkdirSync(dir, { recursive: true });
|
|
5629
5878
|
const tmp = configPath + ".node9-tmp";
|
|
5630
5879
|
try {
|
|
@@ -5650,14 +5899,14 @@ var GLOBAL_CONFIG_PATH;
|
|
|
5650
5899
|
var init_patch = __esm({
|
|
5651
5900
|
"src/config/patch.ts"() {
|
|
5652
5901
|
"use strict";
|
|
5653
|
-
GLOBAL_CONFIG_PATH =
|
|
5902
|
+
GLOBAL_CONFIG_PATH = path16.join(os12.homedir(), ".node9", "config.json");
|
|
5654
5903
|
}
|
|
5655
5904
|
});
|
|
5656
5905
|
|
|
5657
5906
|
// src/daemon/server.ts
|
|
5658
5907
|
import http from "http";
|
|
5659
5908
|
import fs15 from "fs";
|
|
5660
|
-
import
|
|
5909
|
+
import path17 from "path";
|
|
5661
5910
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
5662
5911
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
5663
5912
|
import chalk2 from "chalk";
|
|
@@ -5723,6 +5972,8 @@ data: ${JSON.stringify({
|
|
|
5723
5972
|
toolName: e.toolName,
|
|
5724
5973
|
args: e.args,
|
|
5725
5974
|
riskMetadata: e.riskMetadata,
|
|
5975
|
+
...e.recoveryCommand && { recoveryCommand: e.recoveryCommand },
|
|
5976
|
+
...e.viewOnly && { viewOnly: true },
|
|
5726
5977
|
slackDelegated: e.slackDelegated,
|
|
5727
5978
|
timestamp: e.timestamp,
|
|
5728
5979
|
agent: e.agent,
|
|
@@ -5788,6 +6039,9 @@ data: ${JSON.stringify(item.data)}
|
|
|
5788
6039
|
agent,
|
|
5789
6040
|
mcpServer,
|
|
5790
6041
|
riskMetadata,
|
|
6042
|
+
recoveryCommand,
|
|
6043
|
+
skipBackgroundAuth = false,
|
|
6044
|
+
viewOnly = false,
|
|
5791
6045
|
fromCLI = false,
|
|
5792
6046
|
activityId,
|
|
5793
6047
|
cwd
|
|
@@ -5798,6 +6052,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
5798
6052
|
toolName,
|
|
5799
6053
|
args,
|
|
5800
6054
|
riskMetadata: riskMetadata ?? void 0,
|
|
6055
|
+
...typeof recoveryCommand === "string" && recoveryCommand && { recoveryCommand },
|
|
6056
|
+
...viewOnly && { viewOnly: true },
|
|
5801
6057
|
agent: typeof agent === "string" ? agent : void 0,
|
|
5802
6058
|
mcpServer: typeof mcpServer === "string" ? mcpServer : void 0,
|
|
5803
6059
|
slackDelegated: !!slackDelegated,
|
|
@@ -5832,7 +6088,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5832
6088
|
status: "pending"
|
|
5833
6089
|
});
|
|
5834
6090
|
}
|
|
5835
|
-
const projectCwd = typeof cwd === "string" &&
|
|
6091
|
+
const projectCwd = typeof cwd === "string" && path17.isAbsolute(cwd) ? cwd : void 0;
|
|
5836
6092
|
const projectConfig = getConfig(projectCwd);
|
|
5837
6093
|
const browserEnabled = projectConfig.settings.approvers?.browser !== false;
|
|
5838
6094
|
const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
|
|
@@ -5842,6 +6098,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
5842
6098
|
toolName,
|
|
5843
6099
|
args,
|
|
5844
6100
|
riskMetadata: entry.riskMetadata,
|
|
6101
|
+
...entry.recoveryCommand && { recoveryCommand: entry.recoveryCommand },
|
|
6102
|
+
...entry.viewOnly && { viewOnly: true },
|
|
5845
6103
|
slackDelegated: entry.slackDelegated,
|
|
5846
6104
|
agent: entry.agent,
|
|
5847
6105
|
mcpServer: entry.mcpServer,
|
|
@@ -5858,7 +6116,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5858
6116
|
}
|
|
5859
6117
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5860
6118
|
res.end(JSON.stringify({ id, allowCount: (insightCounts.get(toolName) ?? 0) + 1 }));
|
|
5861
|
-
if (slackDelegated) return;
|
|
6119
|
+
if (slackDelegated || skipBackgroundAuth) return;
|
|
5862
6120
|
authorizeHeadless(
|
|
5863
6121
|
toolName,
|
|
5864
6122
|
args,
|
|
@@ -5997,7 +6255,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
5997
6255
|
saveInsightCounts();
|
|
5998
6256
|
suggestionTracker.resetTool(entry.toolName);
|
|
5999
6257
|
}
|
|
6000
|
-
const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
|
|
6258
|
+
const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native", "terminal-redirect"]);
|
|
6001
6259
|
if (source && VALID_SOURCES.has(source)) entry.decisionSource = source;
|
|
6002
6260
|
if (entry.waiter) {
|
|
6003
6261
|
entry.waiter(resolvedDecision, reason);
|
|
@@ -6026,6 +6284,41 @@ data: ${JSON.stringify(item.data)}
|
|
|
6026
6284
|
return res.end(JSON.stringify({ error: "internal" }));
|
|
6027
6285
|
}
|
|
6028
6286
|
}
|
|
6287
|
+
if (req.method === "GET" && pathname === "/status") {
|
|
6288
|
+
try {
|
|
6289
|
+
const s = getGlobalSettings();
|
|
6290
|
+
const counters = sessionCounters.get();
|
|
6291
|
+
const mode = s.mode ?? "standard";
|
|
6292
|
+
const status = {
|
|
6293
|
+
mode,
|
|
6294
|
+
session: {
|
|
6295
|
+
allowed: counters.allowed,
|
|
6296
|
+
blocked: counters.blocked,
|
|
6297
|
+
dlpHits: counters.dlpHits,
|
|
6298
|
+
wouldBlock: counters.wouldBlock
|
|
6299
|
+
},
|
|
6300
|
+
taintedCount: taintStore.list().length,
|
|
6301
|
+
lastRuleHit: counters.lastRuleHit,
|
|
6302
|
+
lastBlockedTool: counters.lastBlockedTool
|
|
6303
|
+
};
|
|
6304
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6305
|
+
return res.end(JSON.stringify(status));
|
|
6306
|
+
} catch (err) {
|
|
6307
|
+
console.error(chalk2.red("[node9 daemon] GET /status failed:"), err);
|
|
6308
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
6309
|
+
return res.end(JSON.stringify({ error: "internal" }));
|
|
6310
|
+
}
|
|
6311
|
+
}
|
|
6312
|
+
if (req.method === "GET" && pathname === "/state/check") {
|
|
6313
|
+
const predicatesParam = reqUrl.searchParams.get("predicates") ?? "";
|
|
6314
|
+
const predicates = predicatesParam.split(",").filter(Boolean);
|
|
6315
|
+
const results = {};
|
|
6316
|
+
for (const p of predicates) {
|
|
6317
|
+
results[p] = sessionHistory.checkPredicate(p);
|
|
6318
|
+
}
|
|
6319
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
6320
|
+
return res.end(JSON.stringify(results));
|
|
6321
|
+
}
|
|
6029
6322
|
if (req.method === "POST" && pathname === "/settings") {
|
|
6030
6323
|
if (!validToken(req)) return res.writeHead(403).end();
|
|
6031
6324
|
try {
|
|
@@ -6183,8 +6476,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
6183
6476
|
const body = await readBody(req);
|
|
6184
6477
|
const data = body ? JSON.parse(body) : {};
|
|
6185
6478
|
const configPath = data.configPath ?? GLOBAL_CONFIG_PATH;
|
|
6186
|
-
const node9Dir =
|
|
6187
|
-
if (!
|
|
6479
|
+
const node9Dir = path17.dirname(GLOBAL_CONFIG_PATH);
|
|
6480
|
+
if (!path17.resolve(configPath).startsWith(node9Dir + path17.sep)) {
|
|
6188
6481
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6189
6482
|
return res.end(
|
|
6190
6483
|
JSON.stringify({ error: "configPath must be within the node9 config directory" })
|
|
@@ -6431,8 +6724,8 @@ __export(tail_exports, {
|
|
|
6431
6724
|
import http2 from "http";
|
|
6432
6725
|
import chalk16 from "chalk";
|
|
6433
6726
|
import fs24 from "fs";
|
|
6434
|
-
import
|
|
6435
|
-
import
|
|
6727
|
+
import os20 from "os";
|
|
6728
|
+
import path25 from "path";
|
|
6436
6729
|
import readline3 from "readline";
|
|
6437
6730
|
import { spawn as spawn9, execSync as execSync3 } from "child_process";
|
|
6438
6731
|
function getIcon(tool) {
|
|
@@ -6510,9 +6803,10 @@ async function ensureDaemon() {
|
|
|
6510
6803
|
}
|
|
6511
6804
|
function postDecisionHttp(id, decision, csrfToken, port, opts) {
|
|
6512
6805
|
return new Promise((resolve, reject) => {
|
|
6513
|
-
const bodyObj = { decision, source: "terminal" };
|
|
6806
|
+
const bodyObj = { decision, source: opts?.source ?? "terminal" };
|
|
6514
6807
|
if (opts?.persist) bodyObj.persist = true;
|
|
6515
6808
|
if (opts?.trustDuration) bodyObj.trustDuration = opts.trustDuration;
|
|
6809
|
+
if (opts?.reason) bodyObj.reason = opts.reason;
|
|
6516
6810
|
const body = JSON.stringify(bodyObj);
|
|
6517
6811
|
const req = http2.request(
|
|
6518
6812
|
{
|
|
@@ -6537,6 +6831,9 @@ function postDecisionHttp(id, decision, csrfToken, port, opts) {
|
|
|
6537
6831
|
});
|
|
6538
6832
|
}
|
|
6539
6833
|
function buildCardLines(req, localCount = 0) {
|
|
6834
|
+
if (req.recoveryCommand) {
|
|
6835
|
+
return buildRecoveryCardLines(req);
|
|
6836
|
+
}
|
|
6540
6837
|
const argsStr = JSON.stringify(req.args ?? {}).replace(/\s+/g, " ");
|
|
6541
6838
|
const argsPreview = argsStr.length > 60 ? argsStr.slice(0, 60) + "\u2026" : argsStr;
|
|
6542
6839
|
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`;
|
|
@@ -6564,6 +6861,31 @@ function buildCardLines(req, localCount = 0) {
|
|
|
6564
6861
|
);
|
|
6565
6862
|
return lines;
|
|
6566
6863
|
}
|
|
6864
|
+
function buildRecoveryCardLines(req) {
|
|
6865
|
+
const argsObj = req.args;
|
|
6866
|
+
const command = typeof argsObj?.command === "string" ? argsObj.command : JSON.stringify(req.args ?? {}).replace(/\s+/g, " ").slice(0, 60);
|
|
6867
|
+
const ruleName = req.riskMetadata?.ruleName?.replace(/^Smart Rule:\s*/i, "") ?? "policy rule";
|
|
6868
|
+
const recoveryCommand = req.recoveryCommand;
|
|
6869
|
+
const interactiveLines = req.viewOnly ? [` ${GRAY}\u2192 Awaiting decision from interactive terminal...${RESET}`] : [
|
|
6870
|
+
` ${BOLD}${GREEN}[1]${RESET} Allow anyway ${GRAY}(override policy)${RESET}`,
|
|
6871
|
+
` ${BOLD}${YELLOW}[2]${RESET} Redirect AI: "Run '${recoveryCommand}' first, then retry"`,
|
|
6872
|
+
` ${BOLD}${RED}[3]${RESET} Deny & stop ${GRAY}(hard block)${RESET}`,
|
|
6873
|
+
``,
|
|
6874
|
+
` ${GRAY}[Timeout: auto-deny]${RESET}`,
|
|
6875
|
+
` Select [1-3]: `
|
|
6876
|
+
];
|
|
6877
|
+
return [
|
|
6878
|
+
``,
|
|
6879
|
+
`${BOLD}${CYAN}${DIVIDER}${RESET}`,
|
|
6880
|
+
`\u{1F6E1}\uFE0F ${BOLD}NODE9 STATE GUARD:${RESET} '${BOLD}${command}${RESET}'`,
|
|
6881
|
+
`${YELLOW}\u26A0\uFE0F Rule: ${ruleName}${RESET}`,
|
|
6882
|
+
`${CYAN}${DIVIDER}${RESET}`,
|
|
6883
|
+
...!req.viewOnly ? [`${BOLD}What would you like to do?${RESET}`, ``] : [],
|
|
6884
|
+
...interactiveLines,
|
|
6885
|
+
`${CYAN}${DIVIDER}${RESET}`,
|
|
6886
|
+
``
|
|
6887
|
+
];
|
|
6888
|
+
}
|
|
6567
6889
|
async function startTail(options = {}) {
|
|
6568
6890
|
const port = await ensureDaemon();
|
|
6569
6891
|
if (options.clear) {
|
|
@@ -6662,14 +6984,14 @@ async function startTail(options = {}) {
|
|
|
6662
6984
|
localAllowCounts.get(req2.toolName) ?? 0
|
|
6663
6985
|
)
|
|
6664
6986
|
);
|
|
6665
|
-
const decisionStamp = action === "always-allow" ? chalk16.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk16.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk16.green("\u2713 ALLOWED") : chalk16.red("\u2717 DENIED");
|
|
6987
|
+
const decisionStamp = action === "always-allow" ? chalk16.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk16.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk16.green("\u2713 ALLOWED") : action === "redirect" ? chalk16.yellow("\u21A9 REDIRECT AI") : chalk16.red("\u2717 DENIED");
|
|
6666
6988
|
stampedLines.push(` ${BOLD}\u2192${RESET} ${decisionStamp} ${GRAY}(terminal)${RESET}`, ``);
|
|
6667
6989
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
6668
6990
|
process.stdout.write(SHOW_CURSOR);
|
|
6669
6991
|
cardLineCount = 0;
|
|
6670
6992
|
if (action === "allow" || action === "always-allow" || action === "trust") {
|
|
6671
6993
|
localAllowCounts.set(req2.toolName, (localAllowCounts.get(req2.toolName) ?? 0) + 1);
|
|
6672
|
-
} else if (action === "deny") {
|
|
6994
|
+
} else if (action === "deny" || action === "redirect") {
|
|
6673
6995
|
localAllowCounts.delete(req2.toolName);
|
|
6674
6996
|
}
|
|
6675
6997
|
let httpDecision;
|
|
@@ -6680,13 +7002,18 @@ async function startTail(options = {}) {
|
|
|
6680
7002
|
} else if (action === "trust") {
|
|
6681
7003
|
httpDecision = "trust";
|
|
6682
7004
|
httpOpts = { trustDuration: "30m" };
|
|
7005
|
+
} else if (action === "redirect") {
|
|
7006
|
+
httpDecision = "deny";
|
|
7007
|
+
const recoveryCommand = req2.recoveryCommand ?? "the required pre-condition";
|
|
7008
|
+
const redirectReason = `USER INTERVENTION: I am blocking this ${req2.toolName} because the required pre-condition has not been met. Please execute \`${recoveryCommand}\`. If it passes, you are then authorized to run \`${req2.toolName}\`.`;
|
|
7009
|
+
httpOpts = { reason: redirectReason, source: "terminal-redirect" };
|
|
6683
7010
|
} else {
|
|
6684
7011
|
httpDecision = action;
|
|
6685
7012
|
}
|
|
6686
7013
|
postDecisionHttp(req2.id, httpDecision, csrfToken, port, httpOpts).catch((err) => {
|
|
6687
7014
|
try {
|
|
6688
7015
|
fs24.appendFileSync(
|
|
6689
|
-
|
|
7016
|
+
path25.join(os20.homedir(), ".node9", "hook-debug.log"),
|
|
6690
7017
|
`[tail] POST /decision failed: ${String(err)}
|
|
6691
7018
|
`
|
|
6692
7019
|
);
|
|
@@ -6718,17 +7045,34 @@ async function startTail(options = {}) {
|
|
|
6718
7045
|
cardActive = false;
|
|
6719
7046
|
showNextCard();
|
|
6720
7047
|
};
|
|
7048
|
+
if (req2.viewOnly) {
|
|
7049
|
+
process.stdin.resume();
|
|
7050
|
+
onKeypress = () => {
|
|
7051
|
+
};
|
|
7052
|
+
process.stdin.on("keypress", onKeypress);
|
|
7053
|
+
return;
|
|
7054
|
+
}
|
|
6721
7055
|
process.stdin.resume();
|
|
6722
7056
|
onKeypress = (_str, key) => {
|
|
6723
7057
|
const name = key?.name ?? "";
|
|
6724
|
-
if (
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
7058
|
+
if (req2.recoveryCommand) {
|
|
7059
|
+
if (name === "1") {
|
|
7060
|
+
settle("allow");
|
|
7061
|
+
} else if (name === "2") {
|
|
7062
|
+
settle("redirect");
|
|
7063
|
+
} else if (name === "3" || key?.ctrl && name === "c") {
|
|
7064
|
+
settle("deny");
|
|
7065
|
+
}
|
|
7066
|
+
} else {
|
|
7067
|
+
if (name === "y" || name === "return") {
|
|
7068
|
+
settle("allow");
|
|
7069
|
+
} else if (name === "n" || name === "d" || key?.ctrl && name === "c") {
|
|
7070
|
+
settle("deny");
|
|
7071
|
+
} else if (name === "a") {
|
|
7072
|
+
settle("always-allow");
|
|
7073
|
+
} else if (name === "t") {
|
|
7074
|
+
settle("trust");
|
|
7075
|
+
}
|
|
6732
7076
|
}
|
|
6733
7077
|
};
|
|
6734
7078
|
process.stdin.on("keypress", onKeypress);
|
|
@@ -6898,14 +7242,14 @@ async function startTail(options = {}) {
|
|
|
6898
7242
|
process.exit(1);
|
|
6899
7243
|
});
|
|
6900
7244
|
}
|
|
6901
|
-
var PID_FILE, ICONS, RESET, BOLD, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN;
|
|
7245
|
+
var PID_FILE, ICONS, RESET, BOLD, RED, YELLOW, CYAN, GRAY, GREEN, HIDE_CURSOR, SHOW_CURSOR, ERASE_DOWN, DIVIDER;
|
|
6902
7246
|
var init_tail = __esm({
|
|
6903
7247
|
"src/tui/tail.ts"() {
|
|
6904
7248
|
"use strict";
|
|
6905
7249
|
init_daemon2();
|
|
6906
7250
|
init_daemon();
|
|
6907
7251
|
init_core();
|
|
6908
|
-
PID_FILE =
|
|
7252
|
+
PID_FILE = path25.join(os20.homedir(), ".node9", "daemon.pid");
|
|
6909
7253
|
ICONS = {
|
|
6910
7254
|
bash: "\u{1F4BB}",
|
|
6911
7255
|
shell: "\u{1F4BB}",
|
|
@@ -6933,6 +7277,326 @@ var init_tail = __esm({
|
|
|
6933
7277
|
HIDE_CURSOR = "\x1B[?25l";
|
|
6934
7278
|
SHOW_CURSOR = "\x1B[?25h";
|
|
6935
7279
|
ERASE_DOWN = "\x1B[J";
|
|
7280
|
+
DIVIDER = "\u2500".repeat(60);
|
|
7281
|
+
}
|
|
7282
|
+
});
|
|
7283
|
+
|
|
7284
|
+
// src/cli/hud.ts
|
|
7285
|
+
var hud_exports = {};
|
|
7286
|
+
__export(hud_exports, {
|
|
7287
|
+
countConfigs: () => countConfigs,
|
|
7288
|
+
main: () => main,
|
|
7289
|
+
renderEnvironmentLine: () => renderEnvironmentLine
|
|
7290
|
+
});
|
|
7291
|
+
import fs25 from "fs";
|
|
7292
|
+
import path26 from "path";
|
|
7293
|
+
import os21 from "os";
|
|
7294
|
+
import http3 from "http";
|
|
7295
|
+
async function readStdin() {
|
|
7296
|
+
const chunks = [];
|
|
7297
|
+
for await (const chunk of process.stdin) {
|
|
7298
|
+
chunks.push(chunk);
|
|
7299
|
+
}
|
|
7300
|
+
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
7301
|
+
if (!raw) return {};
|
|
7302
|
+
try {
|
|
7303
|
+
return JSON.parse(raw);
|
|
7304
|
+
} catch {
|
|
7305
|
+
return {};
|
|
7306
|
+
}
|
|
7307
|
+
}
|
|
7308
|
+
function queryDaemon() {
|
|
7309
|
+
return new Promise((resolve) => {
|
|
7310
|
+
const timeout = setTimeout(() => resolve(null), 50);
|
|
7311
|
+
try {
|
|
7312
|
+
const req = http3.get(
|
|
7313
|
+
`http://${DAEMON_HOST}:${DAEMON_PORT}/status`,
|
|
7314
|
+
{ timeout: 50 },
|
|
7315
|
+
(res) => {
|
|
7316
|
+
const chunks = [];
|
|
7317
|
+
res.on("data", (c) => chunks.push(c));
|
|
7318
|
+
res.on("end", () => {
|
|
7319
|
+
clearTimeout(timeout);
|
|
7320
|
+
try {
|
|
7321
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString()));
|
|
7322
|
+
} catch {
|
|
7323
|
+
resolve(null);
|
|
7324
|
+
}
|
|
7325
|
+
});
|
|
7326
|
+
}
|
|
7327
|
+
);
|
|
7328
|
+
req.on("error", () => {
|
|
7329
|
+
clearTimeout(timeout);
|
|
7330
|
+
resolve(null);
|
|
7331
|
+
});
|
|
7332
|
+
req.on("timeout", () => {
|
|
7333
|
+
clearTimeout(timeout);
|
|
7334
|
+
req.destroy();
|
|
7335
|
+
resolve(null);
|
|
7336
|
+
});
|
|
7337
|
+
} catch {
|
|
7338
|
+
clearTimeout(timeout);
|
|
7339
|
+
resolve(null);
|
|
7340
|
+
}
|
|
7341
|
+
});
|
|
7342
|
+
}
|
|
7343
|
+
function dim(s) {
|
|
7344
|
+
return `${DIM}${s}${RESET2}`;
|
|
7345
|
+
}
|
|
7346
|
+
function bold(s) {
|
|
7347
|
+
return `${BOLD2}${s}${RESET2}`;
|
|
7348
|
+
}
|
|
7349
|
+
function color(c, s) {
|
|
7350
|
+
return `${c}${s}${RESET2}`;
|
|
7351
|
+
}
|
|
7352
|
+
function progressBar(pct, warnAt = 70, critAt = 85) {
|
|
7353
|
+
const filled = Math.round(Math.min(pct, 100) / 100 * BAR_WIDTH);
|
|
7354
|
+
const bar = BAR_FILLED.repeat(filled) + BAR_EMPTY.repeat(BAR_WIDTH - filled);
|
|
7355
|
+
const c = pct >= critAt ? RED2 : pct >= warnAt ? YELLOW2 : GREEN2;
|
|
7356
|
+
return `${c}${bar}${RESET2}`;
|
|
7357
|
+
}
|
|
7358
|
+
function formatTimeLeft(resetsAt) {
|
|
7359
|
+
if (!resetsAt) return "";
|
|
7360
|
+
const ms = new Date(resetsAt).getTime() - Date.now();
|
|
7361
|
+
if (ms <= 0) return "";
|
|
7362
|
+
const totalMin = Math.ceil(ms / 6e4);
|
|
7363
|
+
const h = Math.floor(totalMin / 60);
|
|
7364
|
+
const m = totalMin % 60;
|
|
7365
|
+
if (h > 0) return ` (${h}h ${m}m left)`;
|
|
7366
|
+
return ` (${m}m left)`;
|
|
7367
|
+
}
|
|
7368
|
+
function safeReadJson(filePath) {
|
|
7369
|
+
if (!fs25.existsSync(filePath)) return null;
|
|
7370
|
+
try {
|
|
7371
|
+
return JSON.parse(fs25.readFileSync(filePath, "utf-8"));
|
|
7372
|
+
} catch {
|
|
7373
|
+
return null;
|
|
7374
|
+
}
|
|
7375
|
+
}
|
|
7376
|
+
function getMcpServerNames(filePath) {
|
|
7377
|
+
const cfg = safeReadJson(filePath);
|
|
7378
|
+
if (!cfg || typeof cfg.mcpServers !== "object" || cfg.mcpServers === null) return /* @__PURE__ */ new Set();
|
|
7379
|
+
return new Set(Object.keys(cfg.mcpServers));
|
|
7380
|
+
}
|
|
7381
|
+
function getDisabledMcpServers(filePath, key) {
|
|
7382
|
+
const cfg = safeReadJson(filePath);
|
|
7383
|
+
if (!cfg || !Array.isArray(cfg[key])) return /* @__PURE__ */ new Set();
|
|
7384
|
+
return new Set(cfg[key].filter((s) => typeof s === "string"));
|
|
7385
|
+
}
|
|
7386
|
+
function countHooksInFile(filePath) {
|
|
7387
|
+
const cfg = safeReadJson(filePath);
|
|
7388
|
+
if (!cfg || typeof cfg.hooks !== "object" || cfg.hooks === null) return 0;
|
|
7389
|
+
return Object.keys(cfg.hooks).length;
|
|
7390
|
+
}
|
|
7391
|
+
function countRulesInDir(rulesDir) {
|
|
7392
|
+
if (!fs25.existsSync(rulesDir)) return 0;
|
|
7393
|
+
let count = 0;
|
|
7394
|
+
try {
|
|
7395
|
+
for (const entry of fs25.readdirSync(rulesDir, { withFileTypes: true })) {
|
|
7396
|
+
if (entry.isDirectory()) {
|
|
7397
|
+
count += countRulesInDir(path26.join(rulesDir, entry.name));
|
|
7398
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
7399
|
+
count++;
|
|
7400
|
+
}
|
|
7401
|
+
}
|
|
7402
|
+
} catch {
|
|
7403
|
+
}
|
|
7404
|
+
return count;
|
|
7405
|
+
}
|
|
7406
|
+
function isSamePath(a, b) {
|
|
7407
|
+
try {
|
|
7408
|
+
return path26.resolve(a) === path26.resolve(b);
|
|
7409
|
+
} catch {
|
|
7410
|
+
return false;
|
|
7411
|
+
}
|
|
7412
|
+
}
|
|
7413
|
+
function countConfigs(cwd) {
|
|
7414
|
+
const homeDir2 = os21.homedir();
|
|
7415
|
+
const claudeDir = path26.join(homeDir2, ".claude");
|
|
7416
|
+
let claudeMdCount = 0;
|
|
7417
|
+
let rulesCount = 0;
|
|
7418
|
+
let hooksCount = 0;
|
|
7419
|
+
const userMcpServers = /* @__PURE__ */ new Set();
|
|
7420
|
+
const projectMcpServers = /* @__PURE__ */ new Set();
|
|
7421
|
+
if (fs25.existsSync(path26.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
7422
|
+
rulesCount += countRulesInDir(path26.join(claudeDir, "rules"));
|
|
7423
|
+
const userSettings = path26.join(claudeDir, "settings.json");
|
|
7424
|
+
for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
|
|
7425
|
+
hooksCount += countHooksInFile(userSettings);
|
|
7426
|
+
const userClaudeJson = path26.join(homeDir2, ".claude.json");
|
|
7427
|
+
for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
|
|
7428
|
+
for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
|
|
7429
|
+
userMcpServers.delete(name);
|
|
7430
|
+
}
|
|
7431
|
+
if (cwd) {
|
|
7432
|
+
if (fs25.existsSync(path26.join(cwd, "CLAUDE.md"))) claudeMdCount++;
|
|
7433
|
+
if (fs25.existsSync(path26.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
|
|
7434
|
+
const projectClaudeDir = path26.join(cwd, ".claude");
|
|
7435
|
+
const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
|
|
7436
|
+
if (!overlapsUserScope) {
|
|
7437
|
+
if (fs25.existsSync(path26.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
7438
|
+
rulesCount += countRulesInDir(path26.join(projectClaudeDir, "rules"));
|
|
7439
|
+
const projSettings = path26.join(projectClaudeDir, "settings.json");
|
|
7440
|
+
for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
|
|
7441
|
+
hooksCount += countHooksInFile(projSettings);
|
|
7442
|
+
}
|
|
7443
|
+
if (fs25.existsSync(path26.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
|
|
7444
|
+
const localSettings = path26.join(projectClaudeDir, "settings.local.json");
|
|
7445
|
+
for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
|
|
7446
|
+
hooksCount += countHooksInFile(localSettings);
|
|
7447
|
+
const mcpJsonServers = getMcpServerNames(path26.join(cwd, ".mcp.json"));
|
|
7448
|
+
const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
|
|
7449
|
+
for (const name of disabledMcpJson) mcpJsonServers.delete(name);
|
|
7450
|
+
for (const name of mcpJsonServers) projectMcpServers.add(name);
|
|
7451
|
+
}
|
|
7452
|
+
return {
|
|
7453
|
+
claudeMdCount,
|
|
7454
|
+
rulesCount,
|
|
7455
|
+
mcpCount: userMcpServers.size + projectMcpServers.size,
|
|
7456
|
+
hooksCount
|
|
7457
|
+
};
|
|
7458
|
+
}
|
|
7459
|
+
function renderEnvironmentLine(counts) {
|
|
7460
|
+
const { claudeMdCount, rulesCount, mcpCount, hooksCount } = counts;
|
|
7461
|
+
if (claudeMdCount === 0 && rulesCount === 0 && mcpCount === 0 && hooksCount === 0) return null;
|
|
7462
|
+
const parts = [
|
|
7463
|
+
`${claudeMdCount} CLAUDE.md`,
|
|
7464
|
+
`${rulesCount} rules`,
|
|
7465
|
+
`${mcpCount} MCPs`,
|
|
7466
|
+
`${hooksCount} hooks`
|
|
7467
|
+
];
|
|
7468
|
+
return color(DIM, parts.join(` ${dim("|")} `));
|
|
7469
|
+
}
|
|
7470
|
+
function renderOffline() {
|
|
7471
|
+
process.stdout.write(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")} ${dim("|")} ${dim("offline")}
|
|
7472
|
+
`);
|
|
7473
|
+
}
|
|
7474
|
+
function renderSecurityLine(status) {
|
|
7475
|
+
const parts = [];
|
|
7476
|
+
parts.push(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")}`);
|
|
7477
|
+
const modeColors = {
|
|
7478
|
+
standard: GREEN2,
|
|
7479
|
+
strict: RED2,
|
|
7480
|
+
observe: MAGENTA,
|
|
7481
|
+
audit: YELLOW2
|
|
7482
|
+
};
|
|
7483
|
+
const modeIcon = {
|
|
7484
|
+
standard: "",
|
|
7485
|
+
strict: "",
|
|
7486
|
+
observe: "\u{1F441} ",
|
|
7487
|
+
audit: ""
|
|
7488
|
+
};
|
|
7489
|
+
const mc = modeColors[status.mode] ?? WHITE;
|
|
7490
|
+
parts.push(`${dim("|")} ${color(mc, modeIcon[status.mode] ?? "")}${color(mc, status.mode)}`);
|
|
7491
|
+
if (status.mode === "observe") {
|
|
7492
|
+
parts.push(`${dim("|")} ${color(GREEN2, `\u2705 ${status.session.allowed} passed`)}`);
|
|
7493
|
+
if (status.session.wouldBlock > 0) {
|
|
7494
|
+
parts.push(color(YELLOW2, `\u26A0 ${status.session.wouldBlock} would-block`));
|
|
7495
|
+
}
|
|
7496
|
+
} else {
|
|
7497
|
+
parts.push(`${dim("|")} ${color(GREEN2, `\u2705 ${status.session.allowed} allowed`)}`);
|
|
7498
|
+
if (status.session.blocked > 0) {
|
|
7499
|
+
parts.push(color(RED2, `\u{1F6D1} ${status.session.blocked} blocked`));
|
|
7500
|
+
}
|
|
7501
|
+
if (status.session.dlpHits > 0) {
|
|
7502
|
+
parts.push(color(RED2, `\u{1F6A8} ${status.session.dlpHits} dlp`));
|
|
7503
|
+
}
|
|
7504
|
+
}
|
|
7505
|
+
if (status.taintedCount > 0) {
|
|
7506
|
+
parts.push(color(YELLOW2, `\u{1F4A7} ${status.taintedCount} tainted`));
|
|
7507
|
+
}
|
|
7508
|
+
if (status.lastRuleHit) {
|
|
7509
|
+
const ruleName = status.lastRuleHit.replace(/^Smart Rule:\s*/i, "");
|
|
7510
|
+
parts.push(color(CYAN2, `\u26A1 ${ruleName}`));
|
|
7511
|
+
}
|
|
7512
|
+
return parts.join(" ");
|
|
7513
|
+
}
|
|
7514
|
+
function renderContextLine(stdin) {
|
|
7515
|
+
const cw = stdin.context_window;
|
|
7516
|
+
if (!cw) return null;
|
|
7517
|
+
const parts = [];
|
|
7518
|
+
const modelName = typeof stdin.model === "string" ? stdin.model : stdin.model?.display_name ?? "";
|
|
7519
|
+
if (modelName) {
|
|
7520
|
+
parts.push(color(CYAN2, modelName));
|
|
7521
|
+
}
|
|
7522
|
+
const usedPct = cw.used_percentage ?? (cw.current_usage && cw.context_window_size ? Math.round(
|
|
7523
|
+
((cw.current_usage.input_tokens ?? 0) + (cw.current_usage.output_tokens ?? 0)) / cw.context_window_size * 100
|
|
7524
|
+
) : null);
|
|
7525
|
+
if (usedPct !== null) {
|
|
7526
|
+
const bar = progressBar(usedPct);
|
|
7527
|
+
parts.push(`${dim("\u2502")} ctx ${bar} ${usedPct}%`);
|
|
7528
|
+
}
|
|
7529
|
+
const rl = stdin.rate_limits;
|
|
7530
|
+
if (rl?.five_hour?.used_percentage !== void 0) {
|
|
7531
|
+
const pct = Math.round(rl.five_hour.used_percentage);
|
|
7532
|
+
const bar = progressBar(pct, 60, 80);
|
|
7533
|
+
const left = formatTimeLeft(rl.five_hour.resets_at);
|
|
7534
|
+
parts.push(`${dim("\u2502")} 5h ${bar} ${pct}%${left}`);
|
|
7535
|
+
}
|
|
7536
|
+
if (rl?.seven_day?.used_percentage !== void 0) {
|
|
7537
|
+
const pct = Math.round(rl.seven_day.used_percentage);
|
|
7538
|
+
const bar = progressBar(pct, 60, 80);
|
|
7539
|
+
parts.push(`${dim("\u2502")} 7d ${bar} ${pct}%`);
|
|
7540
|
+
}
|
|
7541
|
+
if (parts.length === 0) return null;
|
|
7542
|
+
return parts.join(" ");
|
|
7543
|
+
}
|
|
7544
|
+
async function main() {
|
|
7545
|
+
try {
|
|
7546
|
+
const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
|
|
7547
|
+
if (!daemonStatus2) {
|
|
7548
|
+
renderOffline();
|
|
7549
|
+
return;
|
|
7550
|
+
}
|
|
7551
|
+
process.stdout.write(renderSecurityLine(daemonStatus2) + "\n");
|
|
7552
|
+
const ctxLine = renderContextLine(stdin);
|
|
7553
|
+
if (ctxLine) {
|
|
7554
|
+
process.stdout.write(ctxLine + "\n");
|
|
7555
|
+
}
|
|
7556
|
+
const showEnvCounts = (() => {
|
|
7557
|
+
try {
|
|
7558
|
+
const cwd = stdin.cwd ?? process.cwd();
|
|
7559
|
+
for (const configPath of [
|
|
7560
|
+
path26.join(cwd, "node9.config.json"),
|
|
7561
|
+
path26.join(os21.homedir(), ".node9", "config.json")
|
|
7562
|
+
]) {
|
|
7563
|
+
if (!fs25.existsSync(configPath)) continue;
|
|
7564
|
+
const cfg = JSON.parse(fs25.readFileSync(configPath, "utf-8"));
|
|
7565
|
+
const hud = cfg.settings?.hud;
|
|
7566
|
+
if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
|
|
7567
|
+
}
|
|
7568
|
+
} catch {
|
|
7569
|
+
}
|
|
7570
|
+
return true;
|
|
7571
|
+
})();
|
|
7572
|
+
if (showEnvCounts) {
|
|
7573
|
+
const envLine = renderEnvironmentLine(countConfigs(stdin.cwd));
|
|
7574
|
+
if (envLine) {
|
|
7575
|
+
process.stdout.write(envLine + "\n");
|
|
7576
|
+
}
|
|
7577
|
+
}
|
|
7578
|
+
} catch {
|
|
7579
|
+
renderOffline();
|
|
7580
|
+
}
|
|
7581
|
+
}
|
|
7582
|
+
var RESET2, BOLD2, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH;
|
|
7583
|
+
var init_hud = __esm({
|
|
7584
|
+
"src/cli/hud.ts"() {
|
|
7585
|
+
"use strict";
|
|
7586
|
+
init_daemon();
|
|
7587
|
+
RESET2 = "\x1B[0m";
|
|
7588
|
+
BOLD2 = "\x1B[1m";
|
|
7589
|
+
DIM = "\x1B[2m";
|
|
7590
|
+
RED2 = "\x1B[31m";
|
|
7591
|
+
GREEN2 = "\x1B[32m";
|
|
7592
|
+
YELLOW2 = "\x1B[33m";
|
|
7593
|
+
BLUE = "\x1B[34m";
|
|
7594
|
+
MAGENTA = "\x1B[35m";
|
|
7595
|
+
CYAN2 = "\x1B[36m";
|
|
7596
|
+
WHITE = "\x1B[37m";
|
|
7597
|
+
BAR_FILLED = "\u2588";
|
|
7598
|
+
BAR_EMPTY = "\u2591";
|
|
7599
|
+
BAR_WIDTH = 10;
|
|
6936
7600
|
}
|
|
6937
7601
|
});
|
|
6938
7602
|
|
|
@@ -6942,8 +7606,8 @@ import { Command } from "commander";
|
|
|
6942
7606
|
|
|
6943
7607
|
// src/setup.ts
|
|
6944
7608
|
import fs11 from "fs";
|
|
6945
|
-
import
|
|
6946
|
-
import
|
|
7609
|
+
import path13 from "path";
|
|
7610
|
+
import os10 from "os";
|
|
6947
7611
|
import chalk from "chalk";
|
|
6948
7612
|
import { confirm } from "@inquirer/prompts";
|
|
6949
7613
|
function printDaemonTip() {
|
|
@@ -6968,7 +7632,7 @@ function readJson(filePath) {
|
|
|
6968
7632
|
return null;
|
|
6969
7633
|
}
|
|
6970
7634
|
function writeJson(filePath, data) {
|
|
6971
|
-
const dir =
|
|
7635
|
+
const dir = path13.dirname(filePath);
|
|
6972
7636
|
if (!fs11.existsSync(dir)) fs11.mkdirSync(dir, { recursive: true });
|
|
6973
7637
|
fs11.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
6974
7638
|
}
|
|
@@ -6977,9 +7641,9 @@ function isNode9Hook(cmd) {
|
|
|
6977
7641
|
return /(?:^|[\s/\\])node9 (?:check|log)/.test(cmd) || /(?:^|[\s/\\])cli\.js (?:check|log)/.test(cmd);
|
|
6978
7642
|
}
|
|
6979
7643
|
function teardownClaude() {
|
|
6980
|
-
const homeDir2 =
|
|
6981
|
-
const hooksPath =
|
|
6982
|
-
const mcpPath =
|
|
7644
|
+
const homeDir2 = os10.homedir();
|
|
7645
|
+
const hooksPath = path13.join(homeDir2, ".claude", "settings.json");
|
|
7646
|
+
const mcpPath = path13.join(homeDir2, ".claude.json");
|
|
6983
7647
|
let changed = false;
|
|
6984
7648
|
const settings = readJson(hooksPath);
|
|
6985
7649
|
if (settings?.hooks) {
|
|
@@ -7027,8 +7691,8 @@ function teardownClaude() {
|
|
|
7027
7691
|
}
|
|
7028
7692
|
}
|
|
7029
7693
|
function teardownGemini() {
|
|
7030
|
-
const homeDir2 =
|
|
7031
|
-
const settingsPath =
|
|
7694
|
+
const homeDir2 = os10.homedir();
|
|
7695
|
+
const settingsPath = path13.join(homeDir2, ".gemini", "settings.json");
|
|
7032
7696
|
const settings = readJson(settingsPath);
|
|
7033
7697
|
if (!settings) {
|
|
7034
7698
|
console.log(chalk.blue(" \u2139\uFE0F ~/.gemini/settings.json not found \u2014 nothing to remove"));
|
|
@@ -7066,8 +7730,8 @@ function teardownGemini() {
|
|
|
7066
7730
|
}
|
|
7067
7731
|
}
|
|
7068
7732
|
function teardownCursor() {
|
|
7069
|
-
const homeDir2 =
|
|
7070
|
-
const mcpPath =
|
|
7733
|
+
const homeDir2 = os10.homedir();
|
|
7734
|
+
const mcpPath = path13.join(homeDir2, ".cursor", "mcp.json");
|
|
7071
7735
|
const mcpConfig = readJson(mcpPath);
|
|
7072
7736
|
if (!mcpConfig?.mcpServers) {
|
|
7073
7737
|
console.log(chalk.blue(" \u2139\uFE0F ~/.cursor/mcp.json not found \u2014 nothing to remove"));
|
|
@@ -7093,9 +7757,9 @@ function teardownCursor() {
|
|
|
7093
7757
|
}
|
|
7094
7758
|
}
|
|
7095
7759
|
async function setupClaude() {
|
|
7096
|
-
const homeDir2 =
|
|
7097
|
-
const mcpPath =
|
|
7098
|
-
const hooksPath =
|
|
7760
|
+
const homeDir2 = os10.homedir();
|
|
7761
|
+
const mcpPath = path13.join(homeDir2, ".claude.json");
|
|
7762
|
+
const hooksPath = path13.join(homeDir2, ".claude", "settings.json");
|
|
7099
7763
|
const claudeConfig = readJson(mcpPath) ?? {};
|
|
7100
7764
|
const settings = readJson(hooksPath) ?? {};
|
|
7101
7765
|
const servers = claudeConfig.mcpServers ?? {};
|
|
@@ -7169,8 +7833,8 @@ async function setupClaude() {
|
|
|
7169
7833
|
}
|
|
7170
7834
|
}
|
|
7171
7835
|
async function setupGemini() {
|
|
7172
|
-
const homeDir2 =
|
|
7173
|
-
const settingsPath =
|
|
7836
|
+
const homeDir2 = os10.homedir();
|
|
7837
|
+
const settingsPath = path13.join(homeDir2, ".gemini", "settings.json");
|
|
7174
7838
|
const settings = readJson(settingsPath) ?? {};
|
|
7175
7839
|
const servers = settings.mcpServers ?? {};
|
|
7176
7840
|
let anythingChanged = false;
|
|
@@ -7251,7 +7915,7 @@ async function setupGemini() {
|
|
|
7251
7915
|
printDaemonTip();
|
|
7252
7916
|
}
|
|
7253
7917
|
}
|
|
7254
|
-
function detectAgents(homeDir2 =
|
|
7918
|
+
function detectAgents(homeDir2 = os10.homedir()) {
|
|
7255
7919
|
const exists = (p) => {
|
|
7256
7920
|
try {
|
|
7257
7921
|
return fs11.existsSync(p);
|
|
@@ -7265,14 +7929,14 @@ function detectAgents(homeDir2 = os11.homedir()) {
|
|
|
7265
7929
|
}
|
|
7266
7930
|
};
|
|
7267
7931
|
return {
|
|
7268
|
-
claude: exists(
|
|
7269
|
-
gemini: exists(
|
|
7270
|
-
cursor: exists(
|
|
7932
|
+
claude: exists(path13.join(homeDir2, ".claude")) || exists(path13.join(homeDir2, ".claude.json")),
|
|
7933
|
+
gemini: exists(path13.join(homeDir2, ".gemini")),
|
|
7934
|
+
cursor: exists(path13.join(homeDir2, ".cursor"))
|
|
7271
7935
|
};
|
|
7272
7936
|
}
|
|
7273
7937
|
async function setupCursor() {
|
|
7274
|
-
const homeDir2 =
|
|
7275
|
-
const mcpPath =
|
|
7938
|
+
const homeDir2 = os10.homedir();
|
|
7939
|
+
const mcpPath = path13.join(homeDir2, ".cursor", "mcp.json");
|
|
7276
7940
|
const mcpConfig = readJson(mcpPath) ?? {};
|
|
7277
7941
|
const servers = mcpConfig.mcpServers ?? {};
|
|
7278
7942
|
let anythingChanged = false;
|
|
@@ -7325,11 +7989,57 @@ async function setupCursor() {
|
|
|
7325
7989
|
printDaemonTip();
|
|
7326
7990
|
}
|
|
7327
7991
|
}
|
|
7992
|
+
function setupHud() {
|
|
7993
|
+
const homeDir2 = os10.homedir();
|
|
7994
|
+
const hooksPath = path13.join(homeDir2, ".claude", "settings.json");
|
|
7995
|
+
const settings = readJson(hooksPath) ?? {};
|
|
7996
|
+
const hudCommand = fullPathCommand("hud");
|
|
7997
|
+
const statusLineObj = { type: "command", command: hudCommand };
|
|
7998
|
+
const existing = settings.statusLine;
|
|
7999
|
+
const existingCommand = typeof existing === "object" ? existing?.command : existing;
|
|
8000
|
+
if (existingCommand === hudCommand) {
|
|
8001
|
+
console.log(chalk.blue("\u2139\uFE0F node9 HUD is already configured in ~/.claude/settings.json"));
|
|
8002
|
+
console.log(chalk.gray(" Restart Claude Code to activate."));
|
|
8003
|
+
return;
|
|
8004
|
+
}
|
|
8005
|
+
if (existing && existingCommand !== hudCommand) {
|
|
8006
|
+
console.log(
|
|
8007
|
+
chalk.yellow(
|
|
8008
|
+
` \u26A0\uFE0F statusLine is already set to: "${existingCommand}"
|
|
8009
|
+
Overwriting with node9 HUD.`
|
|
8010
|
+
)
|
|
8011
|
+
);
|
|
8012
|
+
}
|
|
8013
|
+
settings.statusLine = statusLineObj;
|
|
8014
|
+
writeJson(hooksPath, settings);
|
|
8015
|
+
console.log(chalk.green.bold("\u2705 node9 HUD added to Claude Code statusline"));
|
|
8016
|
+
console.log(chalk.gray(" Settings: ~/.claude/settings.json"));
|
|
8017
|
+
console.log(chalk.gray(" Restart Claude Code to activate."));
|
|
8018
|
+
}
|
|
8019
|
+
function teardownHud() {
|
|
8020
|
+
const homeDir2 = os10.homedir();
|
|
8021
|
+
const hooksPath = path13.join(homeDir2, ".claude", "settings.json");
|
|
8022
|
+
const settings = readJson(hooksPath);
|
|
8023
|
+
if (!settings) {
|
|
8024
|
+
console.log(chalk.blue(" \u2139\uFE0F ~/.claude/settings.json not found \u2014 nothing to remove"));
|
|
8025
|
+
return;
|
|
8026
|
+
}
|
|
8027
|
+
const existing = settings.statusLine;
|
|
8028
|
+
const existingCommand = typeof existing === "object" ? existing?.command : existing;
|
|
8029
|
+
if (!existingCommand || !String(existingCommand).includes("node9")) {
|
|
8030
|
+
console.log(chalk.blue(" \u2139\uFE0F node9 HUD not found in ~/.claude/settings.json"));
|
|
8031
|
+
return;
|
|
8032
|
+
}
|
|
8033
|
+
delete settings.statusLine;
|
|
8034
|
+
writeJson(hooksPath, settings);
|
|
8035
|
+
console.log(chalk.green(" \u2705 node9 HUD removed from ~/.claude/settings.json"));
|
|
8036
|
+
console.log(chalk.gray(" Restart Claude Code for changes to take effect."));
|
|
8037
|
+
}
|
|
7328
8038
|
|
|
7329
8039
|
// src/cli.ts
|
|
7330
8040
|
init_daemon2();
|
|
7331
8041
|
import chalk17 from "chalk";
|
|
7332
|
-
import
|
|
8042
|
+
import fs26 from "fs";
|
|
7333
8043
|
import path27 from "path";
|
|
7334
8044
|
import os22 from "os";
|
|
7335
8045
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
@@ -7362,7 +8072,7 @@ import { execa } from "execa";
|
|
|
7362
8072
|
import { parseCommandString } from "execa";
|
|
7363
8073
|
|
|
7364
8074
|
// src/policy/negotiation.ts
|
|
7365
|
-
function buildNegotiationMessage(blockedByLabel, isHumanDecision, humanReason) {
|
|
8075
|
+
function buildNegotiationMessage(blockedByLabel, isHumanDecision, humanReason, recoveryCommand) {
|
|
7366
8076
|
if (isHumanDecision) {
|
|
7367
8077
|
return `NODE9: The human user rejected this action.
|
|
7368
8078
|
REASON: ${humanReason || "No specific reason provided."}
|
|
@@ -7418,10 +8128,11 @@ INSTRUCTION: Inform the user this action is pending approval. Wait for them to a
|
|
|
7418
8128
|
INSTRUCTION: Do NOT use "${rule}". Find a read-only or non-destructive alternative.
|
|
7419
8129
|
Do NOT attempt to bypass this rule.`;
|
|
7420
8130
|
}
|
|
8131
|
+
const recovery = recoveryCommand ? `
|
|
8132
|
+
REQUIRED ACTION: Run \`${recoveryCommand}\` first, then retry your original command.` : "\n- Pivot to a non-destructive or read-only alternative.";
|
|
7421
8133
|
return `NODE9: Action blocked by security policy [${blockedByLabel}].
|
|
7422
8134
|
INSTRUCTIONS:
|
|
7423
|
-
- Do NOT retry this exact command or attempt to bypass the rule
|
|
7424
|
-
- Pivot to a non-destructive or read-only alternative.
|
|
8135
|
+
- Do NOT retry this exact command or attempt to bypass the rule.${recovery}
|
|
7425
8136
|
- Inform the user which security rule was triggered and ask how to proceed.`;
|
|
7426
8137
|
}
|
|
7427
8138
|
|
|
@@ -7557,17 +8268,17 @@ init_config();
|
|
|
7557
8268
|
init_policy();
|
|
7558
8269
|
import chalk5 from "chalk";
|
|
7559
8270
|
import fs18 from "fs";
|
|
7560
|
-
import
|
|
7561
|
-
import
|
|
8271
|
+
import path19 from "path";
|
|
8272
|
+
import os14 from "os";
|
|
7562
8273
|
|
|
7563
8274
|
// src/undo.ts
|
|
7564
8275
|
import { spawnSync as spawnSync4, spawn as spawn5 } from "child_process";
|
|
7565
8276
|
import crypto2 from "crypto";
|
|
7566
8277
|
import fs17 from "fs";
|
|
7567
|
-
import
|
|
7568
|
-
import
|
|
7569
|
-
var SNAPSHOT_STACK_PATH =
|
|
7570
|
-
var UNDO_LATEST_PATH =
|
|
8278
|
+
import path18 from "path";
|
|
8279
|
+
import os13 from "os";
|
|
8280
|
+
var SNAPSHOT_STACK_PATH = path18.join(os13.homedir(), ".node9", "snapshots.json");
|
|
8281
|
+
var UNDO_LATEST_PATH = path18.join(os13.homedir(), ".node9", "undo_latest.txt");
|
|
7571
8282
|
var MAX_SNAPSHOTS = 10;
|
|
7572
8283
|
var GIT_TIMEOUT = 15e3;
|
|
7573
8284
|
function readStack() {
|
|
@@ -7579,7 +8290,7 @@ function readStack() {
|
|
|
7579
8290
|
return [];
|
|
7580
8291
|
}
|
|
7581
8292
|
function writeStack(stack) {
|
|
7582
|
-
const dir =
|
|
8293
|
+
const dir = path18.dirname(SNAPSHOT_STACK_PATH);
|
|
7583
8294
|
if (!fs17.existsSync(dir)) fs17.mkdirSync(dir, { recursive: true });
|
|
7584
8295
|
fs17.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
7585
8296
|
}
|
|
@@ -7607,14 +8318,14 @@ function normalizeCwdForHash(cwd) {
|
|
|
7607
8318
|
}
|
|
7608
8319
|
function getShadowRepoDir(cwd) {
|
|
7609
8320
|
const hash = crypto2.createHash("sha256").update(normalizeCwdForHash(cwd)).digest("hex").slice(0, 16);
|
|
7610
|
-
return
|
|
8321
|
+
return path18.join(os13.homedir(), ".node9", "snapshots", hash);
|
|
7611
8322
|
}
|
|
7612
8323
|
function cleanOrphanedIndexFiles(shadowDir) {
|
|
7613
8324
|
try {
|
|
7614
8325
|
const cutoff = Date.now() - 6e4;
|
|
7615
8326
|
for (const f of fs17.readdirSync(shadowDir)) {
|
|
7616
8327
|
if (f.startsWith("index_")) {
|
|
7617
|
-
const fp =
|
|
8328
|
+
const fp = path18.join(shadowDir, f);
|
|
7618
8329
|
try {
|
|
7619
8330
|
if (fs17.statSync(fp).mtimeMs < cutoff) fs17.unlinkSync(fp);
|
|
7620
8331
|
} catch {
|
|
@@ -7628,7 +8339,7 @@ function writeShadowExcludes(shadowDir, ignorePaths) {
|
|
|
7628
8339
|
const hardcoded = [".git", ".node9"];
|
|
7629
8340
|
const lines = [...hardcoded, ...ignorePaths].join("\n");
|
|
7630
8341
|
try {
|
|
7631
|
-
fs17.writeFileSync(
|
|
8342
|
+
fs17.writeFileSync(path18.join(shadowDir, "info", "exclude"), lines + "\n", "utf8");
|
|
7632
8343
|
} catch {
|
|
7633
8344
|
}
|
|
7634
8345
|
}
|
|
@@ -7641,7 +8352,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
7641
8352
|
timeout: 3e3
|
|
7642
8353
|
});
|
|
7643
8354
|
if (check.status === 0) {
|
|
7644
|
-
const ptPath =
|
|
8355
|
+
const ptPath = path18.join(shadowDir, "project-path.txt");
|
|
7645
8356
|
try {
|
|
7646
8357
|
const stored = fs17.readFileSync(ptPath, "utf8").trim();
|
|
7647
8358
|
if (stored === normalizedCwd) return true;
|
|
@@ -7668,7 +8379,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
7668
8379
|
console.error("[Node9] git init --bare failed:", init.stderr?.toString());
|
|
7669
8380
|
return false;
|
|
7670
8381
|
}
|
|
7671
|
-
const configFile =
|
|
8382
|
+
const configFile = path18.join(shadowDir, "config");
|
|
7672
8383
|
spawnSync4("git", ["config", "--file", configFile, "core.untrackedCache", "true"], {
|
|
7673
8384
|
timeout: 3e3
|
|
7674
8385
|
});
|
|
@@ -7676,7 +8387,7 @@ function ensureShadowRepo(shadowDir, cwd) {
|
|
|
7676
8387
|
timeout: 3e3
|
|
7677
8388
|
});
|
|
7678
8389
|
try {
|
|
7679
|
-
fs17.writeFileSync(
|
|
8390
|
+
fs17.writeFileSync(path18.join(shadowDir, "project-path.txt"), normalizedCwd, "utf8");
|
|
7680
8391
|
} catch {
|
|
7681
8392
|
}
|
|
7682
8393
|
return true;
|
|
@@ -7699,7 +8410,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}, ignorePaths = [
|
|
|
7699
8410
|
const shadowDir = getShadowRepoDir(cwd);
|
|
7700
8411
|
if (!ensureShadowRepo(shadowDir, cwd)) return null;
|
|
7701
8412
|
writeShadowExcludes(shadowDir, ignorePaths);
|
|
7702
|
-
indexFile =
|
|
8413
|
+
indexFile = path18.join(shadowDir, `index_${process.pid}_${Date.now()}`);
|
|
7703
8414
|
const shadowEnv = {
|
|
7704
8415
|
...process.env,
|
|
7705
8416
|
GIT_DIR: shadowDir,
|
|
@@ -7808,7 +8519,7 @@ function applyUndo(hash, cwd) {
|
|
|
7808
8519
|
timeout: GIT_TIMEOUT
|
|
7809
8520
|
}).stdout?.toString().trim().split("\n").filter(Boolean) ?? [];
|
|
7810
8521
|
for (const file of [...tracked, ...untracked]) {
|
|
7811
|
-
const fullPath =
|
|
8522
|
+
const fullPath = path18.join(dir, file);
|
|
7812
8523
|
if (!snapshotFiles.has(file) && fs17.existsSync(fullPath)) {
|
|
7813
8524
|
fs17.unlinkSync(fullPath);
|
|
7814
8525
|
}
|
|
@@ -7834,7 +8545,7 @@ function registerCheckCommand(program2) {
|
|
|
7834
8545
|
} catch (err) {
|
|
7835
8546
|
const tempConfig = getConfig();
|
|
7836
8547
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
7837
|
-
const logPath =
|
|
8548
|
+
const logPath = path19.join(os14.homedir(), ".node9", "hook-debug.log");
|
|
7838
8549
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
7839
8550
|
fs18.appendFileSync(
|
|
7840
8551
|
logPath,
|
|
@@ -7847,9 +8558,9 @@ RAW: ${raw}
|
|
|
7847
8558
|
}
|
|
7848
8559
|
const config = getConfig(payload.cwd || void 0);
|
|
7849
8560
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
7850
|
-
const logPath =
|
|
7851
|
-
if (!fs18.existsSync(
|
|
7852
|
-
fs18.mkdirSync(
|
|
8561
|
+
const logPath = path19.join(os14.homedir(), ".node9", "hook-debug.log");
|
|
8562
|
+
if (!fs18.existsSync(path19.dirname(logPath)))
|
|
8563
|
+
fs18.mkdirSync(path19.dirname(logPath), { recursive: true });
|
|
7853
8564
|
fs18.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
7854
8565
|
`);
|
|
7855
8566
|
}
|
|
@@ -7875,6 +8586,8 @@ RAW: ${raw}
|
|
|
7875
8586
|
}
|
|
7876
8587
|
writeTty(chalk5.gray(` Triggered by: ${blockedByContext}`));
|
|
7877
8588
|
if (result2?.changeHint) writeTty(chalk5.cyan(` To change: ${result2.changeHint}`));
|
|
8589
|
+
if (result2?.recoveryCommand)
|
|
8590
|
+
writeTty(chalk5.green(` \u{1F4A1} Run: ${result2.recoveryCommand}`));
|
|
7878
8591
|
writeTty("");
|
|
7879
8592
|
} catch {
|
|
7880
8593
|
} finally {
|
|
@@ -7887,7 +8600,8 @@ RAW: ${raw}
|
|
|
7887
8600
|
const aiFeedbackMessage = buildNegotiationMessage(
|
|
7888
8601
|
blockedByContext,
|
|
7889
8602
|
isHumanDecision,
|
|
7890
|
-
msg
|
|
8603
|
+
msg,
|
|
8604
|
+
result2?.recoveryCommand
|
|
7891
8605
|
);
|
|
7892
8606
|
process.stdout.write(
|
|
7893
8607
|
JSON.stringify({
|
|
@@ -7911,7 +8625,7 @@ RAW: ${raw}
|
|
|
7911
8625
|
if (shouldSnapshot(toolName, toolInput, config)) {
|
|
7912
8626
|
await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
|
|
7913
8627
|
}
|
|
7914
|
-
const safeCwdForAuth = typeof payload.cwd === "string" &&
|
|
8628
|
+
const safeCwdForAuth = typeof payload.cwd === "string" && path19.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
7915
8629
|
const result = await authorizeHeadless(toolName, toolInput, meta, {
|
|
7916
8630
|
cwd: safeCwdForAuth
|
|
7917
8631
|
});
|
|
@@ -7955,7 +8669,7 @@ RAW: ${raw}
|
|
|
7955
8669
|
});
|
|
7956
8670
|
} catch (err) {
|
|
7957
8671
|
if (process.env.NODE9_DEBUG === "1") {
|
|
7958
|
-
const logPath =
|
|
8672
|
+
const logPath = path19.join(os14.homedir(), ".node9", "hook-debug.log");
|
|
7959
8673
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
7960
8674
|
fs18.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
7961
8675
|
`);
|
|
@@ -7995,8 +8709,8 @@ init_audit();
|
|
|
7995
8709
|
init_config();
|
|
7996
8710
|
init_policy();
|
|
7997
8711
|
import fs19 from "fs";
|
|
7998
|
-
import
|
|
7999
|
-
import
|
|
8712
|
+
import path20 from "path";
|
|
8713
|
+
import os15 from "os";
|
|
8000
8714
|
init_daemon();
|
|
8001
8715
|
|
|
8002
8716
|
// src/utils/cp-mv-parser.ts
|
|
@@ -8037,6 +8751,20 @@ function containsShellMetachar(token) {
|
|
|
8037
8751
|
}
|
|
8038
8752
|
|
|
8039
8753
|
// src/cli/commands/log.ts
|
|
8754
|
+
var TEST_COMMAND_RE = /(?:^|\s)(npm\s+(?:run\s+)?test|npx\s+(?:vitest|jest|mocha)|yarn\s+(?:run\s+)?test|pnpm\s+(?:run\s+)?test|vitest|jest|mocha|pytest|py\.test|cargo\s+test|go\s+test|bundle\s+exec\s+rspec|rspec|phpunit|dotnet\s+test)\b/i;
|
|
8755
|
+
function detectTestResult(command, output) {
|
|
8756
|
+
if (!TEST_COMMAND_RE.test(command)) return null;
|
|
8757
|
+
const out = output.toLowerCase();
|
|
8758
|
+
if (/\b(tests?\s+passed|all\s+tests?\s+passed|passing|test\s+suites?.*passed|ok\b|\d+\s+passed)/i.test(
|
|
8759
|
+
out
|
|
8760
|
+
) && !/\b(fail|error|failed)\b/.test(out)) {
|
|
8761
|
+
return "pass";
|
|
8762
|
+
}
|
|
8763
|
+
if (/\b(tests?\s+failed|failing|failed|error|assertion\s+error|\d+\s+failed)\b/i.test(out)) {
|
|
8764
|
+
return "fail";
|
|
8765
|
+
}
|
|
8766
|
+
return null;
|
|
8767
|
+
}
|
|
8040
8768
|
function sanitize3(value) {
|
|
8041
8769
|
return value.replace(/[\x00-\x1F\x7F]/g, "");
|
|
8042
8770
|
}
|
|
@@ -8055,9 +8783,9 @@ function registerLogCommand(program2) {
|
|
|
8055
8783
|
decision: "allowed",
|
|
8056
8784
|
source: "post-hook"
|
|
8057
8785
|
};
|
|
8058
|
-
const logPath =
|
|
8059
|
-
if (!fs19.existsSync(
|
|
8060
|
-
fs19.mkdirSync(
|
|
8786
|
+
const logPath = path20.join(os15.homedir(), ".node9", "audit.log");
|
|
8787
|
+
if (!fs19.existsSync(path20.dirname(logPath)))
|
|
8788
|
+
fs19.mkdirSync(path20.dirname(logPath), { recursive: true });
|
|
8061
8789
|
fs19.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
8062
8790
|
if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
|
|
8063
8791
|
const command = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
|
|
@@ -8068,7 +8796,22 @@ function registerLogCommand(program2) {
|
|
|
8068
8796
|
}
|
|
8069
8797
|
}
|
|
8070
8798
|
}
|
|
8071
|
-
|
|
8799
|
+
if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
|
|
8800
|
+
const bashCommand = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
|
|
8801
|
+
const output = payload.tool_response?.output ?? "";
|
|
8802
|
+
if (bashCommand && output) {
|
|
8803
|
+
const testResult = detectTestResult(bashCommand, output);
|
|
8804
|
+
if (testResult) {
|
|
8805
|
+
await notifyActivitySocket({
|
|
8806
|
+
id: "test-result",
|
|
8807
|
+
ts: Date.now(),
|
|
8808
|
+
tool,
|
|
8809
|
+
status: testResult === "pass" ? "test_pass" : "test_fail"
|
|
8810
|
+
});
|
|
8811
|
+
}
|
|
8812
|
+
}
|
|
8813
|
+
}
|
|
8814
|
+
const safeCwd = typeof payload.cwd === "string" && path20.isAbsolute(payload.cwd) ? payload.cwd : void 0;
|
|
8072
8815
|
const config = getConfig(safeCwd);
|
|
8073
8816
|
if (shouldSnapshot(tool, {}, config)) {
|
|
8074
8817
|
await createShadowSnapshot("unknown", {}, config.policy.snapshot.ignorePaths);
|
|
@@ -8077,7 +8820,7 @@ function registerLogCommand(program2) {
|
|
|
8077
8820
|
const msg = err instanceof Error ? err.message : String(err);
|
|
8078
8821
|
process.stderr.write(`[Node9] audit log error: ${msg}
|
|
8079
8822
|
`);
|
|
8080
|
-
const debugPath =
|
|
8823
|
+
const debugPath = path20.join(os15.homedir(), ".node9", "hook-debug.log");
|
|
8081
8824
|
try {
|
|
8082
8825
|
fs19.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
|
|
8083
8826
|
`);
|
|
@@ -8385,12 +9128,12 @@ function registerConfigShowCommand(program2) {
|
|
|
8385
9128
|
init_daemon();
|
|
8386
9129
|
import chalk7 from "chalk";
|
|
8387
9130
|
import fs20 from "fs";
|
|
8388
|
-
import
|
|
8389
|
-
import
|
|
9131
|
+
import path21 from "path";
|
|
9132
|
+
import os16 from "os";
|
|
8390
9133
|
import { execSync as execSync2 } from "child_process";
|
|
8391
9134
|
function registerDoctorCommand(program2, version2) {
|
|
8392
9135
|
program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
8393
|
-
const homeDir2 =
|
|
9136
|
+
const homeDir2 = os16.homedir();
|
|
8394
9137
|
let failures = 0;
|
|
8395
9138
|
function pass(msg) {
|
|
8396
9139
|
console.log(chalk7.green(" \u2705 ") + msg);
|
|
@@ -8439,7 +9182,7 @@ function registerDoctorCommand(program2, version2) {
|
|
|
8439
9182
|
);
|
|
8440
9183
|
}
|
|
8441
9184
|
section("Configuration");
|
|
8442
|
-
const globalConfigPath =
|
|
9185
|
+
const globalConfigPath = path21.join(homeDir2, ".node9", "config.json");
|
|
8443
9186
|
if (fs20.existsSync(globalConfigPath)) {
|
|
8444
9187
|
try {
|
|
8445
9188
|
JSON.parse(fs20.readFileSync(globalConfigPath, "utf-8"));
|
|
@@ -8450,7 +9193,7 @@ function registerDoctorCommand(program2, version2) {
|
|
|
8450
9193
|
} else {
|
|
8451
9194
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
8452
9195
|
}
|
|
8453
|
-
const projectConfigPath =
|
|
9196
|
+
const projectConfigPath = path21.join(process.cwd(), "node9.config.json");
|
|
8454
9197
|
if (fs20.existsSync(projectConfigPath)) {
|
|
8455
9198
|
try {
|
|
8456
9199
|
JSON.parse(fs20.readFileSync(projectConfigPath, "utf-8"));
|
|
@@ -8462,7 +9205,7 @@ function registerDoctorCommand(program2, version2) {
|
|
|
8462
9205
|
);
|
|
8463
9206
|
}
|
|
8464
9207
|
}
|
|
8465
|
-
const credsPath =
|
|
9208
|
+
const credsPath = path21.join(homeDir2, ".node9", "credentials.json");
|
|
8466
9209
|
if (fs20.existsSync(credsPath)) {
|
|
8467
9210
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
8468
9211
|
} else {
|
|
@@ -8472,7 +9215,7 @@ function registerDoctorCommand(program2, version2) {
|
|
|
8472
9215
|
);
|
|
8473
9216
|
}
|
|
8474
9217
|
section("Agent Hooks");
|
|
8475
|
-
const claudeSettingsPath =
|
|
9218
|
+
const claudeSettingsPath = path21.join(homeDir2, ".claude", "settings.json");
|
|
8476
9219
|
if (fs20.existsSync(claudeSettingsPath)) {
|
|
8477
9220
|
try {
|
|
8478
9221
|
const cs = JSON.parse(fs20.readFileSync(claudeSettingsPath, "utf-8"));
|
|
@@ -8491,7 +9234,7 @@ function registerDoctorCommand(program2, version2) {
|
|
|
8491
9234
|
} else {
|
|
8492
9235
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
8493
9236
|
}
|
|
8494
|
-
const geminiSettingsPath =
|
|
9237
|
+
const geminiSettingsPath = path21.join(homeDir2, ".gemini", "settings.json");
|
|
8495
9238
|
if (fs20.existsSync(geminiSettingsPath)) {
|
|
8496
9239
|
try {
|
|
8497
9240
|
const gs = JSON.parse(fs20.readFileSync(geminiSettingsPath, "utf-8"));
|
|
@@ -8510,7 +9253,7 @@ function registerDoctorCommand(program2, version2) {
|
|
|
8510
9253
|
} else {
|
|
8511
9254
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
8512
9255
|
}
|
|
8513
|
-
const cursorHooksPath =
|
|
9256
|
+
const cursorHooksPath = path21.join(homeDir2, ".cursor", "hooks.json");
|
|
8514
9257
|
if (fs20.existsSync(cursorHooksPath)) {
|
|
8515
9258
|
try {
|
|
8516
9259
|
const cur = JSON.parse(fs20.readFileSync(cursorHooksPath, "utf-8"));
|
|
@@ -8552,8 +9295,8 @@ function registerDoctorCommand(program2, version2) {
|
|
|
8552
9295
|
// src/cli/commands/audit.ts
|
|
8553
9296
|
import chalk8 from "chalk";
|
|
8554
9297
|
import fs21 from "fs";
|
|
8555
|
-
import
|
|
8556
|
-
import
|
|
9298
|
+
import path22 from "path";
|
|
9299
|
+
import os17 from "os";
|
|
8557
9300
|
function formatRelativeTime(timestamp) {
|
|
8558
9301
|
const diff = Date.now() - new Date(timestamp).getTime();
|
|
8559
9302
|
const sec = Math.floor(diff / 1e3);
|
|
@@ -8566,7 +9309,7 @@ function formatRelativeTime(timestamp) {
|
|
|
8566
9309
|
}
|
|
8567
9310
|
function registerAuditCommand(program2) {
|
|
8568
9311
|
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) => {
|
|
8569
|
-
const logPath =
|
|
9312
|
+
const logPath = path22.join(os17.homedir(), ".node9", "audit.log");
|
|
8570
9313
|
if (!fs21.existsSync(logPath)) {
|
|
8571
9314
|
console.log(
|
|
8572
9315
|
chalk8.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
@@ -8694,8 +9437,8 @@ init_core();
|
|
|
8694
9437
|
init_daemon();
|
|
8695
9438
|
import chalk10 from "chalk";
|
|
8696
9439
|
import fs22 from "fs";
|
|
8697
|
-
import
|
|
8698
|
-
import
|
|
9440
|
+
import path23 from "path";
|
|
9441
|
+
import os18 from "os";
|
|
8699
9442
|
function readJson2(filePath) {
|
|
8700
9443
|
try {
|
|
8701
9444
|
if (fs22.existsSync(filePath)) return JSON.parse(fs22.readFileSync(filePath, "utf-8"));
|
|
@@ -8763,8 +9506,8 @@ function registerStatusCommand(program2) {
|
|
|
8763
9506
|
console.log("");
|
|
8764
9507
|
const modeLabel = settings.mode === "audit" ? chalk10.blue("audit") : settings.mode === "strict" ? chalk10.red("strict") : chalk10.white("standard");
|
|
8765
9508
|
console.log(` Mode: ${modeLabel}`);
|
|
8766
|
-
const projectConfig =
|
|
8767
|
-
const globalConfig =
|
|
9509
|
+
const projectConfig = path23.join(process.cwd(), "node9.config.json");
|
|
9510
|
+
const globalConfig = path23.join(os18.homedir(), ".node9", "config.json");
|
|
8768
9511
|
console.log(
|
|
8769
9512
|
` Local: ${fs22.existsSync(projectConfig) ? chalk10.green("Active (node9.config.json)") : chalk10.gray("Not present")}`
|
|
8770
9513
|
);
|
|
@@ -8776,15 +9519,15 @@ function registerStatusCommand(program2) {
|
|
|
8776
9519
|
` Sandbox: ${chalk10.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
8777
9520
|
);
|
|
8778
9521
|
}
|
|
8779
|
-
const homeDir2 =
|
|
9522
|
+
const homeDir2 = os18.homedir();
|
|
8780
9523
|
const claudeSettings = readJson2(
|
|
8781
|
-
|
|
9524
|
+
path23.join(homeDir2, ".claude", "settings.json")
|
|
8782
9525
|
);
|
|
8783
|
-
const claudeConfig = readJson2(
|
|
9526
|
+
const claudeConfig = readJson2(path23.join(homeDir2, ".claude.json"));
|
|
8784
9527
|
const geminiSettings = readJson2(
|
|
8785
|
-
|
|
9528
|
+
path23.join(homeDir2, ".gemini", "settings.json")
|
|
8786
9529
|
);
|
|
8787
|
-
const cursorConfig = readJson2(
|
|
9530
|
+
const cursorConfig = readJson2(path23.join(homeDir2, ".cursor", "mcp.json"));
|
|
8788
9531
|
const agentFound = claudeSettings || claudeConfig || geminiSettings || cursorConfig;
|
|
8789
9532
|
if (agentFound) {
|
|
8790
9533
|
console.log("");
|
|
@@ -8845,12 +9588,12 @@ function registerStatusCommand(program2) {
|
|
|
8845
9588
|
init_core();
|
|
8846
9589
|
import chalk11 from "chalk";
|
|
8847
9590
|
import fs23 from "fs";
|
|
8848
|
-
import
|
|
8849
|
-
import
|
|
9591
|
+
import path24 from "path";
|
|
9592
|
+
import os19 from "os";
|
|
8850
9593
|
function registerInitCommand(program2) {
|
|
8851
9594
|
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) => {
|
|
8852
9595
|
console.log(chalk11.cyan.bold("\n\u{1F6E1}\uFE0F Node9 Init\n"));
|
|
8853
|
-
const configPath =
|
|
9596
|
+
const configPath = path24.join(os19.homedir(), ".node9", "config.json");
|
|
8854
9597
|
if (fs23.existsSync(configPath) && !options.force) {
|
|
8855
9598
|
console.log(chalk11.blue(`\u2139\uFE0F Config already exists: ${configPath}`));
|
|
8856
9599
|
} else {
|
|
@@ -8860,7 +9603,7 @@ function registerInitCommand(program2) {
|
|
|
8860
9603
|
...DEFAULT_CONFIG,
|
|
8861
9604
|
settings: { ...DEFAULT_CONFIG.settings, mode: safeMode }
|
|
8862
9605
|
};
|
|
8863
|
-
const dir =
|
|
9606
|
+
const dir = path24.dirname(configPath);
|
|
8864
9607
|
if (!fs23.existsSync(dir)) fs23.mkdirSync(dir, { recursive: true });
|
|
8865
9608
|
fs23.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
8866
9609
|
console.log(chalk11.green(`\u2705 Config created: ${configPath}`));
|
|
@@ -9318,20 +10061,20 @@ function registerTrustCommand(program2) {
|
|
|
9318
10061
|
|
|
9319
10062
|
// src/cli.ts
|
|
9320
10063
|
var { version } = JSON.parse(
|
|
9321
|
-
|
|
10064
|
+
fs26.readFileSync(path27.join(__dirname, "../package.json"), "utf-8")
|
|
9322
10065
|
);
|
|
9323
10066
|
var program = new Command();
|
|
9324
10067
|
program.name("node9").description("The Sudo Command for AI Agents").version(version);
|
|
9325
10068
|
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) => {
|
|
9326
10069
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
9327
10070
|
const credPath = path27.join(os22.homedir(), ".node9", "credentials.json");
|
|
9328
|
-
if (!
|
|
9329
|
-
|
|
10071
|
+
if (!fs26.existsSync(path27.dirname(credPath)))
|
|
10072
|
+
fs26.mkdirSync(path27.dirname(credPath), { recursive: true });
|
|
9330
10073
|
const profileName = options.profile || "default";
|
|
9331
10074
|
let existingCreds = {};
|
|
9332
10075
|
try {
|
|
9333
|
-
if (
|
|
9334
|
-
const raw = JSON.parse(
|
|
10076
|
+
if (fs26.existsSync(credPath)) {
|
|
10077
|
+
const raw = JSON.parse(fs26.readFileSync(credPath, "utf-8"));
|
|
9335
10078
|
if (raw.apiKey) {
|
|
9336
10079
|
existingCreds = {
|
|
9337
10080
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
|
|
@@ -9343,13 +10086,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
9343
10086
|
} catch {
|
|
9344
10087
|
}
|
|
9345
10088
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
9346
|
-
|
|
10089
|
+
fs26.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
9347
10090
|
if (profileName === "default") {
|
|
9348
10091
|
const configPath = path27.join(os22.homedir(), ".node9", "config.json");
|
|
9349
10092
|
let config = {};
|
|
9350
10093
|
try {
|
|
9351
|
-
if (
|
|
9352
|
-
config = JSON.parse(
|
|
10094
|
+
if (fs26.existsSync(configPath))
|
|
10095
|
+
config = JSON.parse(fs26.readFileSync(configPath, "utf-8"));
|
|
9353
10096
|
} catch {
|
|
9354
10097
|
}
|
|
9355
10098
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -9364,9 +10107,9 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
9364
10107
|
approvers.cloud = false;
|
|
9365
10108
|
}
|
|
9366
10109
|
s.approvers = approvers;
|
|
9367
|
-
if (!
|
|
9368
|
-
|
|
9369
|
-
|
|
10110
|
+
if (!fs26.existsSync(path27.dirname(configPath)))
|
|
10111
|
+
fs26.mkdirSync(path27.dirname(configPath), { recursive: true });
|
|
10112
|
+
fs26.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
9370
10113
|
}
|
|
9371
10114
|
if (options.profile && profileName !== "default") {
|
|
9372
10115
|
console.log(chalk17.green(`\u2705 Profile "${profileName}" saved`));
|
|
@@ -9379,14 +10122,15 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
9379
10122
|
console.log(chalk17.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
9380
10123
|
}
|
|
9381
10124
|
});
|
|
9382
|
-
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) => {
|
|
10125
|
+
program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor hud").argument("<target>", "The agent to protect: claude | gemini | cursor | hud").action(async (target) => {
|
|
9383
10126
|
if (target === "gemini") return await setupGemini();
|
|
9384
10127
|
if (target === "claude") return await setupClaude();
|
|
9385
10128
|
if (target === "cursor") return await setupCursor();
|
|
9386
|
-
|
|
10129
|
+
if (target === "hud") return setupHud();
|
|
10130
|
+
console.error(chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
|
|
9387
10131
|
process.exit(1);
|
|
9388
10132
|
});
|
|
9389
|
-
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) => {
|
|
10133
|
+
program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor hud").argument("[target]", "The agent to protect: claude | gemini | cursor | hud").action(async (target) => {
|
|
9390
10134
|
if (!target) {
|
|
9391
10135
|
console.log(chalk17.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
9392
10136
|
console.log(" Usage: " + chalk17.white("node9 setup <target>") + "\n");
|
|
@@ -9394,6 +10138,9 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
9394
10138
|
console.log(" " + chalk17.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
9395
10139
|
console.log(" " + chalk17.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
9396
10140
|
console.log(" " + chalk17.green("cursor") + " \u2014 Cursor (hook mode)");
|
|
10141
|
+
process.stdout.write(
|
|
10142
|
+
" " + chalk17.green("hud") + " \u2014 Claude Code security statusline\n"
|
|
10143
|
+
);
|
|
9397
10144
|
console.log("");
|
|
9398
10145
|
return;
|
|
9399
10146
|
}
|
|
@@ -9401,7 +10148,8 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
9401
10148
|
if (t === "gemini") return await setupGemini();
|
|
9402
10149
|
if (t === "claude") return await setupClaude();
|
|
9403
10150
|
if (t === "cursor") return await setupCursor();
|
|
9404
|
-
|
|
10151
|
+
if (t === "hud") return setupHud();
|
|
10152
|
+
console.error(chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`));
|
|
9405
10153
|
process.exit(1);
|
|
9406
10154
|
});
|
|
9407
10155
|
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) => {
|
|
@@ -9409,8 +10157,11 @@ program.command("removefrom").description("Remove Node9 hooks from an AI agent c
|
|
|
9409
10157
|
if (target === "claude") fn = teardownClaude;
|
|
9410
10158
|
else if (target === "gemini") fn = teardownGemini;
|
|
9411
10159
|
else if (target === "cursor") fn = teardownCursor;
|
|
10160
|
+
else if (target === "hud") fn = teardownHud;
|
|
9412
10161
|
else {
|
|
9413
|
-
console.error(
|
|
10162
|
+
console.error(
|
|
10163
|
+
chalk17.red(`Unknown target: "${target}". Supported: claude, gemini, cursor, hud`)
|
|
10164
|
+
);
|
|
9414
10165
|
process.exit(1);
|
|
9415
10166
|
}
|
|
9416
10167
|
console.log(chalk17.cyan(`
|
|
@@ -9453,14 +10204,14 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
9453
10204
|
}
|
|
9454
10205
|
if (options.purge) {
|
|
9455
10206
|
const node9Dir = path27.join(os22.homedir(), ".node9");
|
|
9456
|
-
if (
|
|
10207
|
+
if (fs26.existsSync(node9Dir)) {
|
|
9457
10208
|
const confirmed = await confirm3({
|
|
9458
10209
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
9459
10210
|
default: false
|
|
9460
10211
|
});
|
|
9461
10212
|
if (confirmed) {
|
|
9462
|
-
|
|
9463
|
-
if (
|
|
10213
|
+
fs26.rmSync(node9Dir, { recursive: true });
|
|
10214
|
+
if (fs26.existsSync(node9Dir)) {
|
|
9464
10215
|
console.error(
|
|
9465
10216
|
chalk17.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
9466
10217
|
);
|
|
@@ -9576,6 +10327,10 @@ registerWatchCommand(program);
|
|
|
9576
10327
|
registerMcpGatewayCommand(program);
|
|
9577
10328
|
registerCheckCommand(program);
|
|
9578
10329
|
registerLogCommand(program);
|
|
10330
|
+
program.command("hud").description("Render node9 security statusline (spawned by Claude Code statusLine)").action(async () => {
|
|
10331
|
+
const { main: main2 } = await Promise.resolve().then(() => (init_hud(), hud_exports));
|
|
10332
|
+
await main2();
|
|
10333
|
+
});
|
|
9579
10334
|
program.command("pause").description("Temporarily disable Node9 protection for a set duration").option("-d, --duration <duration>", "How long to pause (e.g. 15m, 1h, 30s)", "15m").action((options) => {
|
|
9580
10335
|
const ms = parseDuration(options.duration);
|
|
9581
10336
|
if (ms === null) {
|
|
@@ -9674,7 +10429,7 @@ if (process.argv[2] !== "daemon") {
|
|
|
9674
10429
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
9675
10430
|
const logPath = path27.join(os22.homedir(), ".node9", "hook-debug.log");
|
|
9676
10431
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
9677
|
-
|
|
10432
|
+
fs26.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
9678
10433
|
`);
|
|
9679
10434
|
}
|
|
9680
10435
|
process.exit(0);
|